diff --git a/apps/dokploy/components/dashboard/home/show-home.tsx b/apps/dokploy/components/dashboard/home/show-home.tsx new file mode 100644 index 000000000..f77b71f71 --- /dev/null +++ b/apps/dokploy/components/dashboard/home/show-home.tsx @@ -0,0 +1,291 @@ +import { formatDistanceToNow } from "date-fns"; +import { ArrowRight, Rocket, Server } from "lucide-react"; +import Link from "next/link"; +import { useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { api } from "@/utils/api"; + +type DeploymentStatus = "idle" | "running" | "done" | "error"; + +const statusDotClass: Record = { + done: "bg-emerald-500", + running: "bg-amber-500", + error: "bg-red-500", + idle: "bg-muted-foreground/40", +}; + +function getServiceInfo(d: any) { + const app = d.application; + const comp = d.compose; + const serverName: string = + d.server?.name ?? app?.server?.name ?? comp?.server?.name ?? "Dokploy"; + if (app?.environment?.project && app.environment) { + return { + name: app.name as string, + environment: app.environment.name as string, + projectName: app.environment.project.name as string, + serverName, + href: `/dashboard/project/${app.environment.project.projectId}/environment/${app.environment.environmentId}/services/application/${app.applicationId}`, + }; + } + if (comp?.environment?.project && comp.environment) { + return { + name: comp.name as string, + environment: comp.environment.name as string, + projectName: comp.environment.project.name as string, + serverName, + href: `/dashboard/project/${comp.environment.project.projectId}/environment/${comp.environment.environmentId}/services/compose/${comp.composeId}`, + }; + } + return null; +} + +function StatCard({ + label, + value, + delta, +}: { + label: string; + value: string; + delta?: string; +}) { + return ( +
+ + {label} + +
+ {value} + {delta && ( + {delta} + )} +
+
+ ); +} + +function StatusListCard({ + label, + items, +}: { + label: string; + items: { dotClass: string; label: string; count: number }[]; +}) { + return ( +
+ + {label} + + +
+ ); +} + +export const ShowHome = () => { + const { data: auth } = api.user.get.useQuery(); + const { data: homeStats } = api.project.homeStats.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); + const canReadDeployments = !!permissions?.deployment.read; + const { data: deployments } = api.deployment.allCentralized.useQuery( + undefined, + { + enabled: canReadDeployments, + refetchInterval: 10000, + }, + ); + + const firstName = auth?.user?.firstName?.trim(); + + const totals = homeStats ?? { + projects: 0, + environments: 0, + applications: 0, + compose: 0, + databases: 0, + services: 0, + }; + const statusBreakdown = homeStats?.status ?? { + running: 0, + error: 0, + idle: 0, + }; + + const recentDeployments = useMemo(() => { + if (!deployments) return []; + return [...deployments] + .sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ) + .slice(0, 10); + }, [deployments]); + + const deployStats = useMemo(() => { + const now = Date.now(); + const weekMs = 7 * 24 * 60 * 60 * 1000; + const lastStart = now - weekMs; + const prevStart = now - 2 * weekMs; + + const last: NonNullable = []; + const prev: NonNullable = []; + for (const d of deployments ?? []) { + const t = new Date(d.createdAt).getTime(); + if (t >= lastStart) last.push(d); + else if (t >= prevStart) prev.push(d); + } + + const lastCount = last.length; + const prevCount = prev.length; + let delta: string | undefined; + if (prevCount > 0) { + const pct = Math.round(((lastCount - prevCount) / prevCount) * 100); + delta = `${pct >= 0 ? "+" : ""}${pct}% vs prev 7d`; + } else if (lastCount > 0) { + delta = "no prior data"; + } else { + delta = "no activity yet"; + } + + return { value: String(lastCount), delta }; + }, [deployments]); + + return ( +
+ +
+
+

+ {firstName ? `Welcome back, ${firstName}` : "Welcome back"} +

+ +
+ +
+ + + + +
+ +
+
+
+ +

Recent deployments

+
+ {canReadDeployments && ( + + view all → + + )} +
+ {!canReadDeployments ? ( +
+ + You do not have permission to view deployments. +
+ ) : recentDeployments.length === 0 ? ( +
+ + No deployments yet. +
+ ) : ( +
    + {recentDeployments.map((d) => { + const info = getServiceInfo(d); + if (!info) return null; + const status = (d.status ?? "idle") as DeploymentStatus; + return ( +
  • + + +
    + {info.name} + + {info.projectName} · {info.environment} + +
    + + + {info.serverName} + + + {status} + + + {formatDistanceToNow(new Date(d.createdAt), { + addSuffix: true, + })} + + + logs → + + +
  • + ); + })} +
+ )} +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 1d3055e5e..f04f38bcc 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -166,6 +166,7 @@ export const ShowProjects = () => { return ( total + (env.applications?.length || 0) + + (env.libsql?.length || 0) + (env.mariadb?.length || 0) + (env.mongo?.length || 0) + (env.mysql?.length || 0) + @@ -178,6 +179,7 @@ export const ShowProjects = () => { return ( total + (env.applications?.length || 0) + + (env.libsql?.length || 0) + (env.mariadb?.length || 0) + (env.mongo?.length || 0) + (env.mysql?.length || 0) + diff --git a/apps/dokploy/components/dashboard/search-command.tsx b/apps/dokploy/components/dashboard/search-command.tsx index 9946a54a9..841728dc1 100644 --- a/apps/dokploy/components/dashboard/search-command.tsx +++ b/apps/dokploy/components/dashboard/search-command.tsx @@ -167,7 +167,7 @@ export const SearchCommand = () => {