mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(multi-server): add docker containers view to servers
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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> = ({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>(
|
||||
[],
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user