Docker compose support (#111)

* feat(WIP): compose implementation

* feat: add volumes, networks, services name hash generate

* feat: add compose config test unique

* feat: add tests for each unique config

* feat: implement lodash for docker compose parsing

* feat: add tests for generating compose file

* refactor: implement logs docker compose

* refactor: composeFile set not empty

* feat: implement providers for compose deployments

* feat: add Files volumes to compose

* feat: add stop compose button

* refactor: change strategie of building compose

* feat: create .env file in composepath

* refactor: simplify git and github function

* chore: update deps

* refactor: update migrations and add badge to recognize compose type

* chore: update lock yaml

* refactor: use code editor

* feat: add monitoring for app types

* refactor: reset stats on change appName

* refactor: add option to clean monitoring folder

* feat: show current command that will run

* feat: add prefix

* fix: add missing types

* refactor: add docker provider and expose by default as false

* refactor: customize error page

* refactor: unified deployments to be a single one

* feat: add vitest to ci/cd

* revert: back to initial version

* refactor: add maxconcurrency vitest

* refactor: add pool forks to vitest

* feat: add pocketbase template

* fix: update path resolution compose

* removed

* feat: add template pocketbase

* feat: add pocketbase template

* feat: add support button

* feat: add plausible template

* feat: add calcom template

* feat: add version to each template

* feat: add code editor to enviroment variables and swarm settings json

* refactor: add loader when download the image

* fix: use base64 to generate keys plausible

* feat: add recognized domain names by enviroment compose

* refactor: show alert to redeploy in each card advanced tab

* refactor: add validation to prevent create compose if not have permissions

* chore: add templates section to contributing

* chore: add example contributing
This commit is contained in:
Mauricio Siu
2024-06-02 15:26:28 -06:00
committed by GitHub
parent 1df6db738e
commit 8f9d21c0f8
139 changed files with 16513 additions and 1208 deletions

View File

@@ -1,5 +1,7 @@
import { AddApplication } from "@/components/dashboard/project/add-application";
import { AddCompose } from "@/components/dashboard/project/add-compose";
import { AddDatabase } from "@/components/dashboard/project/add-database";
import { AddTemplate } from "@/components/dashboard/project/add-template";
import {
MariadbIcon,
MongodbIcon,
@@ -31,7 +33,7 @@ import type { findProjectById } from "@/server/api/services/project";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { FolderInput, GlobeIcon, PlusIcon } from "lucide-react";
import { CircuitBoard, FolderInput, GlobeIcon, PlusIcon } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -43,7 +45,14 @@ import superjson from "superjson";
export type Services = {
name: string;
type: "mariadb" | "application" | "postgres" | "mysql" | "mongo" | "redis";
type:
| "mariadb"
| "application"
| "postgres"
| "mysql"
| "mongo"
| "redis"
| "compose";
description?: string | null;
id: string;
createdAt: string;
@@ -113,7 +122,24 @@ export const extractServices = (data: Project | undefined) => {
description: item.description,
})) || [];
applications.push(...mysql, ...redis, ...mongo, ...postgres, ...mariadb);
const compose: Services[] =
data?.compose.map((item) => ({
name: item.name,
type: "compose",
id: item.composeId,
createdAt: item.createdAt,
status: item.composeStatus,
description: item.description,
})) || [];
applications.push(
...mysql,
...redis,
...mongo,
...postgres,
...mariadb,
...compose,
);
applications.sort((a, b) => {
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
@@ -144,7 +170,8 @@ const Project = (
data?.mysql?.length === 0 &&
data?.postgres?.length === 0 &&
data?.redis?.length === 0 &&
data?.applications?.length === 0;
data?.applications?.length === 0 &&
data?.compose?.length === 0;
const applications = extractServices(data);
@@ -185,6 +212,8 @@ const Project = (
<DropdownMenuSeparator />
<AddApplication projectId={projectId} />
<AddDatabase projectId={projectId} />
<AddCompose projectId={projectId} />
<AddTemplate projectId={projectId} />
</DropdownMenuContent>
</DropdownMenu>
)}
@@ -249,6 +278,9 @@ const Project = (
{service.type === "application" && (
<GlobeIcon className="h-6 w-6" />
)}
{service.type === "compose" && (
<CircuitBoard className="h-6 w-6" />
)}
</span>
</div>
</CardTitle>

View File

@@ -14,7 +14,6 @@ import { ShowGeneralApplication } from "@/components/dashboard/application/gener
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { UpdateApplication } from "@/components/dashboard/application/update-application";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import {

View File

@@ -0,0 +1,245 @@
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
import { ShowVolumesCompose } from "@/components/dashboard/compose/advanced/show-volumes";
import { DeleteCompose } from "@/components/dashboard/compose/delete-compose";
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring/show";
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { CircuitBoard } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useState, type ReactElement } from "react";
import superjson from "superjson";
type TabState =
| "projects"
| "settings"
| "advanced"
| "deployments"
| "monitoring";
const Service = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) => {
const { composeId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.compose.one.useQuery(
{ composeId },
{
refetchInterval: 5000,
},
);
const { data: auth } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
return (
<div className="pb-10">
<div className="flex flex-col gap-4">
<Breadcrumb>
<BreadcrumbItem>
<BreadcrumbLink as={Link} href="/dashboard/projects">
Projects
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbLink
as={Link}
href={`/dashboard/project/${data?.project.projectId}`}
>
{data?.project.name}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage>
<BreadcrumbLink>{data?.name}</BreadcrumbLink>
</BreadcrumbItem>
</Breadcrumb>
<header className="mb-6 flex w-full items-center justify-between max-sm:flex-wrap gap-4">
<div className="flex flex-col justify-between w-fit gap-2">
<div className="flex flex-row items-center gap-2 xl:gap-4 flex-wrap">
<h1 className="flex items-center gap-2 text-xl font-bold lg:text-3xl">
{data?.name}
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
{data?.description}
</p>
)}
</div>
<div className="relative flex flex-row gap-4">
<div className="absolute -right-1 -top-2">
<StatusTooltip status={data?.composeStatus} />
</div>
<CircuitBoard className="h-6 w-6 text-muted-foreground" />
</div>
</header>
</div>
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateCompose composeId={composeId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteCompose composeId={composeId} />
)}
</div>
</div>
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironmentCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMonitoringCompose
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogsCompose appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="deployments">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDeploymentsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommandCompose composeId={composeId} />
<ShowVolumesCompose composeId={composeId} />
</div>
</TabsContent>
</Tabs>
</div>
);
};
export default Service;
Service.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{
composeId: string;
activeTab: TabState;
}>,
) {
const { query, params, req, res } = ctx;
const activeTab = query.tab;
const { user, session } = await validateRequest(req, res);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
// Fetch data from external API
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
// Valid project, if not return to initial homepage....
if (typeof params?.composeId === "string") {
try {
await helpers.compose.one.fetch({
composeId: params?.composeId,
});
return {
props: {
trpcState: helpers.dehydrate(),
composeId: params?.composeId,
activeTab: (activeTab || "general") as TabState,
},
};
} catch (error) {
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
},
};
}
}
return {
redirect: {
permanent: false,
destination: "/",
},
};
}