feat(multi-server): add docker containers view to servers

This commit is contained in:
Mauricio Siu
2024-09-21 15:16:15 -06:00
parent 698ff9e918
commit c03c154fc4
11 changed files with 344 additions and 20 deletions

View File

@@ -11,12 +11,14 @@ import { api } from "@/utils/api";
interface Props {
containerId: string;
serverId?: string | null;
}
export const ShowContainerConfig = ({ containerId }: Props) => {
export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
const { data } = api.docker.getConfig.useQuery(
{
containerId,
serverId,
},
{
enabled: !!containerId,

View File

@@ -8,7 +8,7 @@ import "@xterm/xterm/css/xterm.css";
interface Props {
id: string;
containerId: string;
serverId?: string;
serverId?: string | null;
}
export const DockerLogsId: React.FC<Props> = ({

View File

@@ -22,9 +22,14 @@ export const DockerLogsId = dynamic(
interface Props {
containerId: string;
children?: React.ReactNode;
serverId?: string | null;
}
export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
export const ShowDockerModalLogs = ({
containerId,
children,
serverId,
}: Props) => {
return (
<Dialog>
<DialogTrigger asChild>
@@ -41,7 +46,11 @@ export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
<DialogDescription>View the logs for {containerId}</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 pt-2.5">
<DockerLogsId id="terminal" containerId={containerId || ""} />
<DockerLogsId
id="terminal"
containerId={containerId || ""}
serverId={serverId}
/>
</div>
</DialogContent>
</Dialog>

View File

@@ -114,10 +114,16 @@ export const columns: ColumnDef<Container>[] = [
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<ShowDockerModalLogs containerId={container.containerId}>
<ShowDockerModalLogs
containerId={container.containerId}
serverId={container.serverId}
>
View Logs
</ShowDockerModalLogs>
<ShowContainerConfig containerId={container.containerId} />
<ShowContainerConfig
containerId={container.containerId}
serverId={container.serverId}
/>
<DockerTerminalModal containerId={container.containerId}>
Terminal
</DockerTerminalModal>

View File

@@ -34,8 +34,14 @@ export type Container = NonNullable<
RouterOutputs["docker"]["getContainers"]
>[0];
export const ShowContainers = () => {
const { data, isLoading } = api.docker.getContainers.useQuery();
interface Props {
serverId?: string;
}
export const ShowContainers = ({ serverId }: Props) => {
const { data, isLoading } = api.docker.getContainers.useQuery({
serverId,
});
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],

View File

@@ -0,0 +1,50 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { ContainerIcon, FileTextIcon } from "lucide-react";
import { useState } from "react";
import { ShowTraefikSystem } from "../../file-system/show-traefik-system";
import { ShowContainers } from "../../docker/show/show-containers";
interface Props {
serverId: string;
}
export const ShowDockerContainersModal = ({ serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Show Docker Containers
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
<DialogHeader>
<div className="flex flex-col gap-1.5">
<DialogTitle className="flex items-center gap-2">
<ContainerIcon className="size-5" /> Docker Containers
</DialogTitle>
<p className="text-muted-foreground text-sm">
See all the containers of your remote server
</p>
</div>
</DialogHeader>
<div className="grid w-full gap-1">
<ShowContainers serverId={serverId} />
{/* <ShowTraefikSystem serverId={serverId} /> */}
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,78 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TraefikActions } from "./traefik-actions";
interface Props {
serverId: string;
}
export const ShowServer = ({ serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
View Actions
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-6xl overflow-y-auto max-h-screen ">
<DialogHeader>
<div className="flex flex-col gap-1.5">
<DialogTitle className="flex items-center gap-2">
Server Actions
</DialogTitle>
<p className="text-muted-foreground text-sm">
View all the actions you can do with this server remotely
</p>
</div>
</DialogHeader>
<div className="grid grid-cols-3 w-full gap-1">
<Card className="bg-transparent">
<CardHeader>
<CardTitle className="text-xl">Traefik</CardTitle>
<CardDescription>
Deploy your new project in one-click.
</CardDescription>
</CardHeader>
<CardContent>
<TraefikActions serverId={serverId} />
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Deploy</Button>
</CardFooter>
</Card>
{/* <ShowTraefikSystem serverId={serverId} /> */}
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -31,6 +31,8 @@ import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { ShowModalLogs } from "../web-server/show-modal-logs";
import { ToggleTraefikDashboard } from "./toggle-traefik-dashboard";
import { EditTraefikEnv } from "../web-server/edit-traefik-env";
import { ShowServer } from "./show-server";
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
export const ShowServers = () => {
const { data, refetch } = api.server.all.useQuery();
const { mutateAsync } = api.server.remove.useMutation();
@@ -171,6 +173,7 @@ export const ShowServers = () => {
<SetupServer serverId={server.serverId} />
<UpdateServer serverId={server.serverId} />
<ShowServer serverId={server.serverId} />
<DialogAction
title={"Delete Server"}
description="This will delete the server and all associated data"
@@ -196,6 +199,7 @@ export const ShowServers = () => {
Delete Server
</DropdownMenuItem>
</DialogAction>
<DropdownMenuSeparator />
<DropdownMenuLabel>Traefik</DropdownMenuLabel>
<DropdownMenuItem
@@ -216,6 +220,9 @@ export const ShowServers = () => {
<ShowTraefikFileSystemModal
serverId={server.serverId}
/>
<ShowDockerContainersModal
serverId={server.serverId}
/>
<ShowModalLogs
appName="dokploy-traefik"
serverId={server.serverId}

View File

@@ -0,0 +1,135 @@
import { api } from "@/utils/api";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { ShowModalLogs } from "../web-server/show-modal-logs";
import { DockerTerminalModal } from "../web-server/docker-terminal-modal";
import { EditTraefikEnv } from "../web-server/edit-traefik-env";
import { ShowMainTraefikConfig } from "../web-server/show-main-traefik-config";
interface Props {
serverId?: string;
}
export const TraefikActions = ({ serverId }: Props) => {
api.settings.reloadServer.useMutation();
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
api.settings.reloadTraefik.useMutation();
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
api.settings.cleanAll.useMutation();
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
api.settings.toggleDashboard.useMutation();
const {
mutateAsync: cleanStoppedContainers,
isLoading: cleanStoppedContainersIsLoading,
} = api.settings.cleanStoppedContainers.useMutation();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { mutateAsync: updateDockerCleanup } =
api.settings.updateDockerCleanup.useMutation();
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
api.settings.haveTraefikDashboardPortEnabled.useQuery();
return (
<DropdownMenu>
<DropdownMenuTrigger
asChild
disabled={reloadTraefikIsLoading || toggleDashboardIsLoading}
>
<Button
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
variant="outline"
>
Traefik
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={async () => {
await reloadTraefik({
serverId: serverId,
})
.then(async () => {
toast.success("Traefik Reloaded");
})
.catch(() => {
toast.error("Error to reload the traefik");
});
}}
>
<span>Reload</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy-traefik">
<span>Watch logs</span>
</ShowModalLogs>
{!serverId && (
<ShowMainTraefikConfig>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>View Traefik config</span>
</DropdownMenuItem>
</ShowMainTraefikConfig>
)}
<EditTraefikEnv serverId={serverId}>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>Modify Env</span>
</DropdownMenuItem>
</EditTraefikEnv>
<DropdownMenuItem
onClick={async () => {
await toggleDashboard({
enableDashboard: !haveTraefikDashboardPortEnabled,
serverId: serverId,
})
.then(async () => {
toast.success(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
refetchDashboard();
})
.catch(() => {
toast.error(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
});
}}
className="w-full cursor-pointer space-x-3"
>
<span>
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard
</span>
</DropdownMenuItem>
<DockerTerminalModal appName="dokploy-traefik" serverId={serverId}>
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<span>Enter the terminal</span>
</DropdownMenuItem>
</DockerTerminalModal>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -9,9 +9,15 @@ import {
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const dockerRouter = createTRPCRouter({
getContainers: protectedProcedure.query(async () => {
return await getContainers();
}),
getContainers: protectedProcedure
.input(
z.object({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getContainers(input.serverId);
}),
restartContainer: protectedProcedure
.input(
@@ -27,10 +33,11 @@ export const dockerRouter = createTRPCRouter({
.input(
z.object({
containerId: z.string().min(1),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getConfig(input.containerId);
return await getConfig(input.containerId, input.serverId);
}),
getContainersByAppNameMatch: protectedProcedure

View File

@@ -1,11 +1,22 @@
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
export const getContainers = async () => {
export const getContainers = async (serverId?: string | null) => {
try {
const { stdout, stderr } = await execAsync(
"docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | Image: {{.Image}} | Ports: {{.Ports}} | State: {{.State}} | Status: {{.Status}}'",
);
const command =
"docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | Image: {{.Image}} | Ports: {{.Ports}} | State: {{.State}} | Status: {{.Status}}'";
let stdout = "";
let stderr = "";
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
stderr = result.stderr;
} else {
const result = await execAsync(command);
stdout = result.stdout;
stderr = result.stderr;
}
if (stderr) {
console.error(`Error: ${stderr}`);
return;
@@ -41,6 +52,7 @@ export const getContainers = async () => {
ports,
state,
status,
serverId,
};
})
.filter((container) => !container.name.includes("dokploy"));
@@ -49,11 +61,23 @@ export const getContainers = async () => {
} catch (error) {}
};
export const getConfig = async (containerId: string) => {
export const getConfig = async (
containerId: string,
serverId?: string | null,
) => {
try {
const { stdout, stderr } = await execAsync(
`docker inspect ${containerId} --format='{{json .}}'`,
);
const command = `docker inspect ${containerId} --format='{{json .}}'`;
let stdout = "";
let stderr = "";
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
stderr = result.stderr;
} else {
const result = await execAsync(command);
stdout = result.stdout;
stderr = result.stderr;
}
if (stderr) {
console.error(`Error: ${stderr}`);