diff --git a/apps/dokploy/components/dashboard/home/show-home.tsx b/apps/dokploy/components/dashboard/home/show-home.tsx index 4acd30912..2e569af7f 100644 --- a/apps/dokploy/components/dashboard/home/show-home.tsx +++ b/apps/dokploy/components/dashboard/home/show-home.tsx @@ -1,27 +1,31 @@ import { formatDistanceToNow } from "date-fns"; -import { Plus, Rocket } from "lucide-react"; +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-muted-foreground/60", + done: "bg-emerald-500", running: "bg-amber-500", error: "bg-red-500", - idle: "bg-muted-foreground/30", + 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}`, }; } @@ -30,6 +34,7 @@ function getServiceInfo(d: any) { 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}`, }; } @@ -46,7 +51,7 @@ function StatCard({ delta?: string; }) { return ( -
+
{label} @@ -60,6 +65,34 @@ function StatCard({ ); } +function StatusListCard({ + label, + items, +}: { + label: string; + items: { dotClass: string; label: string; count: number }[]; +}) { + return ( +
+ + {label} + +
    + {items.map((item) => ( +
  • + + {item.count} + {item.label} +
  • + ))} +
+
+ ); +} + export const ShowHome = () => { const { data: auth } = api.user.get.useQuery(); const { data: projects } = api.project.all.useQuery(); @@ -76,10 +109,12 @@ export const ShowHome = () => { const firstName = auth?.user?.firstName?.trim(); - const { totals, serverCount } = useMemo(() => { + const { totals, statusBreakdown } = useMemo(() => { let applications = 0; let compose = 0; let databases = 0; + let environments = 0; + const breakdown = { running: 0, error: 0, idle: 0 }; const dbKeys = [ "postgres", "mysql", @@ -88,24 +123,37 @@ export const ShowHome = () => { "redis", "libsql", ] as const; + const bump = (status?: string) => { + if (status === "done") breakdown.running++; + else if (status === "error") breakdown.error++; + else breakdown.idle++; + }; for (const p of projects ?? []) { for (const env of p.environments ?? []) { + environments++; applications += env.applications?.length ?? 0; compose += env.compose?.length ?? 0; + for (const a of env.applications ?? []) bump(a.applicationStatus); + for (const c of env.compose ?? []) bump((c as any).composeStatus); for (const key of dbKeys) { - databases += (env as any)[key]?.length ?? 0; + const list = ((env as any)[key] ?? []) as Array<{ + applicationStatus?: string; + }>; + databases += list.length; + for (const s of list) bump(s.applicationStatus); } } } return { totals: { projects: projects?.length ?? 0, + environments, applications, compose, databases, services: applications + compose + databases, }, - serverCount: servers?.length ?? 0, + statusBreakdown: breakdown, }; }, [projects, servers]); @@ -119,101 +167,160 @@ export const ShowHome = () => { .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"} -

-

- {totals.services} services across {serverCount}{" "} - {serverCount === 1 ? "server" : "servers"} · {totals.projects}{" "} - {totals.projects === 1 ? "project" : "projects"} -

-
- -
+
+ +
+
+

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

+ +
-
- - - - -
+
+ + + + +
-
-
-
- -

Recent deployments

+
+
+
+ +

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 → + + +
  • + ); + })} +
+ )}
- {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} - -
    - - {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/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index 50432d13d..c1d4f0eb8 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -36,7 +36,7 @@ import { AddTemplate } from "@/components/dashboard/project/add-template"; import { AdvancedEnvironmentSelector } from "@/components/dashboard/project/advanced-environment-selector"; import { DuplicateProject } from "@/components/dashboard/project/duplicate-project"; import { EnvironmentVariables } from "@/components/dashboard/project/environment-variables"; -import { ProjectEnvironment } from "@/components/dashboard/home/project-environment"; +import { ProjectEnvironment } from "@/components/dashboard/projects/project-environment"; import { LibsqlIcon, MariadbIcon, @@ -509,6 +509,14 @@ const EnvironmentPage = ( deploy: api.mongo.deploy.useMutation(), }; + const libsqlActions = { + start: api.libsql.start.useMutation(), + stop: api.libsql.stop.useMutation(), + move: api.libsql.move.useMutation(), + delete: api.libsql.remove.useMutation(), + deploy: api.libsql.deploy.useMutation(), + }; + const handleBulkStart = async () => { let success = 0; setIsBulkActionLoading(true); @@ -541,6 +549,9 @@ const EnvironmentPage = ( case "mongo": await mongoActions.start.mutateAsync({ mongoId: serviceId }); break; + case "libsql": + await libsqlActions.start.mutateAsync({ libsqlId: serviceId }); + break; } success++; } catch { @@ -588,6 +599,9 @@ const EnvironmentPage = ( case "mongo": await mongoActions.stop.mutateAsync({ mongoId: serviceId }); break; + case "libsql": + await libsqlActions.stop.mutateAsync({ libsqlId: serviceId }); + break; } success++; } catch { @@ -664,6 +678,12 @@ const EnvironmentPage = ( targetEnvironmentId: selectedTargetEnvironment, }); break; + case "libsql": + await libsqlActions.move.mutateAsync({ + libsqlId: serviceId, + targetEnvironmentId: selectedTargetEnvironment, + }); + break; } await utils.environment.one.invalidate({ environmentId, @@ -733,6 +753,11 @@ const EnvironmentPage = ( mongoId: serviceId, }); break; + case "libsql": + await libsqlActions.delete.mutateAsync({ + libsqlId: serviceId, + }); + break; } await utils.environment.one.invalidate({ environmentId, @@ -799,6 +824,11 @@ const EnvironmentPage = ( mongoId: serviceId, }); break; + case "libsql": + await libsqlActions.deploy.mutateAsync({ + libsqlId: serviceId, + }); + break; } success++; } catch (error) {