mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-18 03:02:56 +00:00
feat(networks): add network configuration viewer and enhance network actions
- Introduced a new ShowNetworkConfig component for displaying detailed Docker network configuration. - Integrated ShowNetworkConfig into the ShowNetworks component, allowing users to view network details directly. - Updated the layout of network actions for improved user experience and accessibility. - Enhanced the API to support network inspection functionality, providing necessary data for the new component.
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Eye, Loader2 } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
networkId: string;
|
||||||
|
networkName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowNetworkConfig = ({ networkId, networkName }: Props) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { data, isLoading, error } = api.network.inspect.useQuery(
|
||||||
|
{ networkId },
|
||||||
|
{ enabled: open },
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon-sm" aria-label="View network config">
|
||||||
|
<Eye className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Network Config</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
docker network inspect output for "{networkName}"
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{error ? (
|
||||||
|
<AlertBlock type="error">{error.message}</AlertBlock>
|
||||||
|
) : isLoading ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center justify-center py-10 text-sm text-muted-foreground">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Loader2 className="animate-spin size-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-wrap rounded-lg border p-4 overflow-y-auto text-sm bg-card max-h-[80vh]">
|
||||||
|
<code>
|
||||||
|
<pre className="whitespace-pre-wrap wrap-break-word">
|
||||||
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
lineWrapping
|
||||||
|
lineNumbers={false}
|
||||||
|
readOnly
|
||||||
|
value={JSON.stringify(data, null, 2)}
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Loader2, Network, Trash2 } from "lucide-react";
|
import { Loader2, Network, Trash2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { HandleNetwork } from "@/components/dashboard/networks/handle-network";
|
import { HandleNetwork } from "@/components/dashboard/networks/handle-network";
|
||||||
|
import { ShowNetworkConfig } from "@/components/dashboard/networks/show-network-config";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -77,12 +78,11 @@ export const ShowNetworks = () => {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Name</TableHead>
|
<TableHead>Name</TableHead>
|
||||||
<TableHead>Driver</TableHead>
|
<TableHead>Driver</TableHead>
|
||||||
<TableHead>Scope</TableHead>
|
|
||||||
<TableHead>Internal</TableHead>
|
<TableHead>Internal</TableHead>
|
||||||
<TableHead>Attachable</TableHead>
|
<TableHead>Attachable</TableHead>
|
||||||
<TableHead>Server</TableHead>
|
<TableHead>Server</TableHead>
|
||||||
<TableHead>Created</TableHead>
|
<TableHead>Created</TableHead>
|
||||||
<TableHead className="w-[80px] text-right">
|
<TableHead className="w-[100px] text-right">
|
||||||
Actions
|
Actions
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -92,10 +92,12 @@ export const ShowNetworks = () => {
|
|||||||
<TableRow key={n.networkId}>
|
<TableRow key={n.networkId}>
|
||||||
<TableCell className="font-medium">{n.name}</TableCell>
|
<TableCell className="font-medium">{n.name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="outline">{n.driver}</Badge>
|
<div className="flex items-center gap-2">
|
||||||
</TableCell>
|
<Badge variant="outline">{n.driver}</Badge>
|
||||||
<TableCell className="text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{n.driver === "overlay" ? "swarm" : "local"}
|
{n.driver === "overlay" ? "swarm" : "local"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-muted-foreground">
|
<TableCell className="text-muted-foreground">
|
||||||
{n.internal ? "Yes" : "No"}
|
{n.internal ? "Yes" : "No"}
|
||||||
@@ -109,35 +111,41 @@ export const ShowNetworks = () => {
|
|||||||
<TableCell className="text-muted-foreground">
|
<TableCell className="text-muted-foreground">
|
||||||
{new Date(n.createdAt).toLocaleDateString()}
|
{new Date(n.createdAt).toLocaleDateString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell>
|
||||||
<DialogAction
|
<div className="flex items-center justify-end gap-3">
|
||||||
title="Delete network"
|
<ShowNetworkConfig
|
||||||
description={`The network "${n.name}" will be removed from Docker and Dokploy. This action cannot be undone.`}
|
networkId={n.networkId}
|
||||||
onClick={async () => {
|
networkName={n.name}
|
||||||
try {
|
/>
|
||||||
await removeNetwork({
|
<DialogAction
|
||||||
networkId: n.networkId,
|
title="Delete network"
|
||||||
});
|
description={`The network "${n.name}" will be removed from Docker and Dokploy. This action cannot be undone.`}
|
||||||
toast.success("Network deleted");
|
onClick={async () => {
|
||||||
await utils.network.all.invalidate();
|
try {
|
||||||
} catch (error) {
|
await removeNetwork({
|
||||||
toast.error("Error deleting network", {
|
networkId: n.networkId,
|
||||||
description:
|
});
|
||||||
error instanceof Error
|
toast.success("Network deleted");
|
||||||
? error.message
|
await utils.network.all.invalidate();
|
||||||
: "Unknown error",
|
} catch (error) {
|
||||||
});
|
toast.error("Error deleting network", {
|
||||||
}
|
description:
|
||||||
}}
|
error instanceof Error
|
||||||
>
|
? error.message
|
||||||
<Button
|
: "Unknown error",
|
||||||
variant="ghost"
|
});
|
||||||
size="icon-xs"
|
}
|
||||||
aria-label="Delete network"
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-destructive" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
size="icon-sm"
|
||||||
|
aria-label="Delete network"
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type * as React from "react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"group/button inline-flex shrink-0 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { createNetwork, findNetworkById, removeNetwork } from "@dokploy/server";
|
import {
|
||||||
|
createNetwork,
|
||||||
|
findNetworkById,
|
||||||
|
inspectNetwork,
|
||||||
|
removeNetwork,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { desc, eq } from "drizzle-orm";
|
import { desc, eq } from "drizzle-orm";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
@@ -44,6 +49,19 @@ export const networkRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
return createNetwork(input, ctx.session.activeOrganizationId);
|
return createNetwork(input, ctx.session.activeOrganizationId);
|
||||||
}),
|
}),
|
||||||
|
inspect: protectedProcedure
|
||||||
|
.input(apiFindOneNetwork)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const network = await findNetworkById(input.networkId);
|
||||||
|
if (network.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Network not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return inspectNetwork(input.networkId);
|
||||||
|
}),
|
||||||
|
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiRemoveNetwork)
|
.input(apiRemoveNetwork)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
|||||||
@@ -97,6 +97,24 @@ export const createNetwork = async (
|
|||||||
return created;
|
return created;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const inspectNetwork = async (networkId: string) => {
|
||||||
|
const row = await findNetworkById(networkId);
|
||||||
|
|
||||||
|
const docker = await getRemoteDocker(row.serverId ?? null);
|
||||||
|
try {
|
||||||
|
return await docker.getNetwork(row.name).inspect();
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to inspect Docker network",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Docker networks are immutable: there is no update, only create and remove.
|
// Docker networks are immutable: there is no update, only create and remove.
|
||||||
export const removeNetwork = async (networkId: string) => {
|
export const removeNetwork = async (networkId: string) => {
|
||||||
const row = await findNetworkById(networkId);
|
const row = await findNetworkById(networkId);
|
||||||
|
|||||||
Reference in New Issue
Block a user