Merge pull request #3291 from Dokploy/feat/use-cards-in-remote-servers

Feat/use cards in remote servers
This commit is contained in:
Mauricio Siu
2025-12-16 21:14:26 -06:00
committed by GitHub
5 changed files with 366 additions and 242 deletions

View File

@@ -1,3 +1,4 @@
import { Activity } from "lucide-react";
import { useState } from "react";
import {
Dialog,
@@ -6,6 +7,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { ShowStorageActions } from "./show-storage-actions";
import { ShowTraefikActions } from "./show-traefik-actions";
@@ -13,20 +15,30 @@ import { ToggleDockerCleanup } from "./toggle-docker-cleanup";
interface Props {
serverId: string;
asButton?: boolean;
}
export const ShowServerActions = ({ serverId }: Props) => {
export const ShowServerActions = ({ serverId, asButton = false }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
{asButton ? (
<DialogTrigger asChild>
<Button variant="outline" size="icon" className="h-9 w-9">
<Activity className="h-4 w-4" />
</Button>
</DialogTrigger>
) : (
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => e.preventDefault()}
onSelect={(e) => {
e.preventDefault();
setIsOpen(true);
}}
>
View Actions
</DropdownMenuItem>
</DialogTrigger>
)}
<DialogContent className="sm:max-w-xl">
<div className="flex flex-col gap-1">
<DialogTitle className="text-xl">Web server settings</DialogTitle>

View File

@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { PlusIcon, Pencil } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { useEffect, useState } from "react";
@@ -59,9 +59,10 @@ type Schema = z.infer<typeof Schema>;
interface Props {
serverId?: string;
asButton?: boolean;
}
export const HandleServers = ({ serverId }: Props) => {
export const HandleServers = ({ serverId, asButton = false }: Props) => {
const { t } = useTranslation("settings");
const utils = api.useUtils();
@@ -137,21 +138,32 @@ export const HandleServers = ({ serverId }: Props) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
{serverId ? (
{serverId ? (
asButton ? (
<DialogTrigger asChild>
<Button variant="outline" size="icon" className="h-9 w-9">
<Pencil className="h-4 w-4" />
</Button>
</DialogTrigger>
) : (
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
onSelect={(e) => {
e.preventDefault();
setIsOpen(true);
}}
>
Edit Server
</DropdownMenuItem>
) : (
)
) : (
<DialogTrigger asChild>
<Button className="cursor-pointer space-x-3">
<PlusIcon className="h-4 w-4" />
Create Server
</Button>
)}
</DialogTrigger>
</DialogTrigger>
)}
<DialogContent className="sm:max-w-3xl ">
<DialogHeader>
<DialogTitle>{serverId ? "Edit" : "Create"} Server</DialogTitle>

View File

@@ -1,5 +1,5 @@
import copy from "copy-to-clipboard";
import { CopyIcon, ExternalLinkIcon, ServerIcon } from "lucide-react";
import { CopyIcon, ExternalLinkIcon, ServerIcon, Settings } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
@@ -36,9 +36,10 @@ import { ValidateServer } from "./validate-server";
interface Props {
serverId: string;
asButton?: boolean;
}
export const SetupServer = ({ serverId }: Props) => {
export const SetupServer = ({ serverId, asButton = false }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data: server } = api.server.one.useQuery(
{
@@ -81,14 +82,23 @@ export const SetupServer = ({ serverId }: Props) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
{asButton ? (
<DialogTrigger asChild>
<Button variant="outline" size="icon" className="h-9 w-9">
<Settings className="h-4 w-4" />
</Button>
</DialogTrigger>
) : (
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
onSelect={(e) => {
e.preventDefault();
setIsOpen(true);
}}
>
Setup Server
</DropdownMenuItem>
</DialogTrigger>
)}
<DialogContent className="sm:max-w-4xl ">
<DialogHeader>
<div className="flex flex-col gap-1.5">

View File

@@ -1,5 +1,18 @@
import { format } from "date-fns";
import { KeyIcon, Loader2, MoreHorizontal, ServerIcon } from "lucide-react";
import {
KeyIcon,
Loader2,
MoreHorizontal,
ServerIcon,
Clock,
User,
Key,
Network,
Terminal,
Settings,
Pencil,
Trash2,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
@@ -24,14 +37,11 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
import { TerminalModal } from "../web-server/terminal-modal";
@@ -59,7 +69,7 @@ export const ShowServers = () => {
return (
<div className="w-full">
{query?.success && isCloud && <WelcomeSuscription />}
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<Card className="h-full p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
@@ -114,240 +124,309 @@ export const ShowServers = () => {
<HandleServers />
</div>
) : (
<div className="flex flex-col gap-4 min-h-[25vh]">
<Table>
<TableCaption>
<div className="flex flex-col gap-4">
See all servers
</div>
</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="text-left">Name</TableHead>
{isCloud && (
<TableHead className="text-center">
Status
</TableHead>
)}
<TableHead className="text-center">
Type
</TableHead>
<TableHead className="text-center">
IP Address
</TableHead>
<TableHead className="text-center">
Port
</TableHead>
<TableHead className="text-center">
Username
</TableHead>
<TableHead className="text-center">
SSH Key
</TableHead>
<TableHead className="text-center">
Created
</TableHead>
<TableHead className="text-right">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.map((server) => {
const canDelete = server.totalSum === 0;
const isActive = server.serverStatus === "active";
const isBuildServer =
server.serverType === "build";
return (
<TableRow key={server.serverId}>
<TableCell className="text-left">
{server.name}
</TableCell>
{isCloud && (
<TableHead className="text-center">
<div className="flex flex-col gap-4 min-h-[25vh]">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data?.map((server) => {
const canDelete = server.totalSum === 0;
const isActive = server.serverStatus === "active";
const isBuildServer = server.serverType === "build";
return (
<Card
key={server.serverId}
className="relative hover:shadow-lg transition-shadow flex flex-col bg-transparent"
>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
<ServerIcon className="size-5 text-muted-foreground" />
<CardTitle className="text-lg">
{server.name}
</CardTitle>
</div>
{isActive &&
server.sshKeyId &&
!isBuildServer && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
More options
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>
Advanced
</DropdownMenuLabel>
<ShowTraefikFileSystemModal
serverId={server.serverId}
/>
<ShowDockerContainersModal
serverId={server.serverId}
/>
{isCloud && (
<ShowMonitoringModal
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
token={
server?.metricsConfig?.server
?.token
}
/>
)}
<ShowSwarmOverviewModal
serverId={server.serverId}
/>
<ShowNodesModal
serverId={server.serverId}
/>
<ShowSchedulesModal
serverId={server.serverId}
/>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<TooltipProvider>
<div className="flex gap-2 mt-2 flex-wrap">
{isCloud && (
<>
{server.serverStatus === "active" ? (
<Badge variant="default">
{server.serverStatus}
</Badge>
) : (
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<span className="inline-block">
<Badge
variant="destructive"
className="cursor-help"
>
{server.serverStatus}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent
className="max-w-xs"
side="bottom"
>
<p className="text-sm">
This server is deactivated due
to lack of payment. Please pay
your invoice to reactivate it.
If you think this is an error,
please contact support.
</p>
</TooltipContent>
</Tooltip>
)}
</>
)}
<Badge
variant={
server.serverStatus === "active"
? "default"
: "destructive"
isBuildServer
? "secondary"
: "default"
}
>
{server.serverStatus}
{server.serverType}
</Badge>
</TableHead>
)}
<TableCell className="text-center">
<Badge
variant={
isBuildServer ? "secondary" : "default"
}
>
{server.serverType}
</div>
</TooltipProvider>
</CardHeader>
<CardContent className="space-y-3 flex-1 flex flex-col">
<div className="flex items-center gap-2 text-sm">
<Network className="size-4 text-muted-foreground" />
<span className="text-muted-foreground">
IP:
</span>
<Badge variant="outline">
{server.ipAddress}
</Badge>
</TableCell>
<TableCell className="text-center">
<Badge>{server.ipAddress}</Badge>
</TableCell>
<TableCell className="text-center">
{server.port}
</TableCell>
<TableCell className="text-center">
{server.username}
</TableCell>
<TableCell className="text-right">
<span className="text-sm text-muted-foreground">
<span className="text-muted-foreground">
Port:
</span>
<span className="font-medium">
{server.port}
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<User className="size-4 text-muted-foreground" />
<span className="text-muted-foreground">
User:
</span>
<span className="font-medium">
{server.username}
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Key className="size-4 text-muted-foreground" />
<span className="text-muted-foreground">
SSH Key:
</span>
<span className="font-medium">
{server.sshKeyId ? "Yes" : "No"}
</span>
</TableCell>
<TableCell className="text-right">
<span className="text-sm text-muted-foreground">
</div>
<div className="flex items-center gap-2 text-sm pt-2 border-t">
<Clock className="size-4 text-muted-foreground" />
<span className="text-xs text-muted-foreground">
Created{" "}
{format(
new Date(server.createdAt),
"PPpp",
"PPp",
)}
</span>
</TableCell>
</div>
<TableCell className="text-right flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
Open menu
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>
Actions
</DropdownMenuLabel>
{isActive && (
<>
{server.sshKeyId && (
<TerminalModal
serverId={server.serverId}
>
<span>
{t(
"settings.common.enterTerminal",
)}
</span>
</TerminalModal>
)}
<SetupServer
serverId={server.serverId}
/>
<HandleServers
serverId={server.serverId}
/>
{server.sshKeyId &&
!isBuildServer && (
<ShowServerActions
{/* Compact Actions */}
{isActive && (
<div className="flex items-center gap-2 pt-3 border-t mt-auto">
<TooltipProvider>
{server.sshKeyId && (
<Tooltip>
<TooltipTrigger asChild>
<div>
<TerminalModal
serverId={server.serverId}
/>
)}
</>
asButton={true}
>
<Button
variant="outline"
size="icon"
className="h-9 w-9"
>
<Terminal className="h-4 w-4" />
</Button>
</TerminalModal>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Terminal</p>
</TooltipContent>
</Tooltip>
)}
<DialogAction
disabled={!canDelete}
title={
canDelete
? "Delete Server"
: "Server has active services"
}
description={
canDelete ? (
"This will delete the server and all associated data"
) : (
<div className="flex flex-col gap-2">
You can not delete this server
because it has active services.
<AlertBlock type="warning">
You have active services
associated with this server,
please delete them first.
</AlertBlock>
</div>
)
}
onClick={async () => {
await mutateAsync({
serverId: server.serverId,
})
.then(() => {
refetch();
toast.success(
`Server ${server.name} deleted successfully`,
);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
<DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
onSelect={(e) => e.preventDefault()}
>
Delete Server
</DropdownMenuItem>
</DialogAction>
{isActive &&
server.sshKeyId &&
!isBuildServer && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>
Extra
</DropdownMenuLabel>
<ShowTraefikFileSystemModal
<Tooltip>
<TooltipTrigger asChild>
<div>
<SetupServer
serverId={server.serverId}
asButton={true}
/>
<ShowDockerContainersModal
</div>
</TooltipTrigger>
<TooltipContent>
<p>Setup Server</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div>
<HandleServers
serverId={server.serverId}
asButton={true}
/>
{isCloud && (
<ShowMonitoringModal
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
token={
server?.metricsConfig
?.server?.token
}
</div>
</TooltipTrigger>
<TooltipContent>
<p>Edit Server</p>
</TooltipContent>
</Tooltip>
{server.sshKeyId && !isBuildServer && (
<Tooltip>
<TooltipTrigger asChild>
<div>
<ShowServerActions
serverId={server.serverId}
asButton={true}
/>
)}
</div>
</TooltipTrigger>
<TooltipContent>
<p>Web Server Actions</p>
</TooltipContent>
</Tooltip>
)}
<ShowSwarmOverviewModal
serverId={server.serverId}
/>
<ShowNodesModal
serverId={server.serverId}
/>
<div className="flex-1" />
<ShowSchedulesModal
serverId={server.serverId}
/>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<Tooltip>
<TooltipTrigger asChild>
<div>
<DialogAction
disabled={!canDelete}
title={
canDelete
? "Delete Server"
: "Server has active services"
}
description={
canDelete ? (
"This will delete the server and all associated data"
) : (
<div className="flex flex-col gap-2">
You can not delete this
server because it has
active services.
<AlertBlock type="warning">
You have active services
associated with this
server, please delete
them first.
</AlertBlock>
</div>
)
}
onClick={async () => {
await mutateAsync({
serverId: server.serverId,
})
.then(() => {
refetch();
toast.success(
`Server ${server.name} deleted successfully`,
);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
<Button
variant="ghost"
size="icon"
className={`h-9 w-9 ${canDelete ? "text-destructive hover:text-destructive hover:bg-destructive/10" : "text-muted-foreground hover:bg-muted"}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</DialogAction>
</div>
</TooltipTrigger>
<TooltipContent>
<p>
{canDelete
? "Delete Server"
: "Cannot delete - has active services"}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
</CardContent>
</Card>
);
})}
</div>
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mt-4">
{data && data?.length > 0 && (
<div>
<HandleServers />

View File

@@ -24,10 +24,16 @@ const getTerminalKey = () => {
interface Props {
children?: React.ReactNode;
serverId: string;
asButton?: boolean;
}
export const TerminalModal = ({ children, serverId }: Props) => {
export const TerminalModal = ({
children,
serverId,
asButton = false,
}: Props) => {
const [terminalKey, setTerminalKey] = useState<string>(getTerminalKey());
const [isOpen, setIsOpen] = useState(false);
const isLocalServer = serverId === "local";
const { data } = api.server.one.useQuery(
@@ -43,15 +49,20 @@ export const TerminalModal = ({ children, serverId }: Props) => {
};
return (
<Dialog>
<DialogTrigger asChild>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
{asButton ? (
<DialogTrigger asChild>{children}</DialogTrigger>
) : (
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
onSelect={(e) => {
e.preventDefault();
setIsOpen(true);
}}
>
{children}
</DropdownMenuItem>
</DialogTrigger>
)}
<DialogContent
className="sm:max-w-7xl"
onEscapeKeyDown={(event) => event.preventDefault()}