From 8e54e88370b24b262581a6d365d4659255f87a55 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 4 Apr 2026 21:09:16 -0600 Subject: [PATCH] feat: add empty states and summary cards for Swarm containers dashboard - Introduced new components for handling various empty states in the Swarm containers dashboard, including `SwarmNotAvailable`, `ServicesError`, `NoServices`, and `NoRunningContainers`. - Added `SummaryCards` component to display key metrics such as node count, down nodes, service count, and running containers. - Enhanced the `ShowSwarmContainers` component to integrate the new empty states and summary cards, improving user feedback and overall experience. --- .../swarm/containers/empty-states.tsx | 277 ++++++++++ .../containers/show-swarm-containers.tsx | 516 ++++-------------- .../swarm/containers/summary-cards.tsx | 68 +++ 3 files changed, 451 insertions(+), 410 deletions(-) create mode 100644 apps/dokploy/components/dashboard/swarm/containers/empty-states.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/containers/summary-cards.tsx diff --git a/apps/dokploy/components/dashboard/swarm/containers/empty-states.tsx b/apps/dokploy/components/dashboard/swarm/containers/empty-states.tsx new file mode 100644 index 000000000..306b2f808 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/containers/empty-states.tsx @@ -0,0 +1,277 @@ +import { + AlertCircle, + AlertTriangle, + ExternalLink, + Info, + RefreshCw, +} from "lucide-react"; +import Link from "next/link"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import type { ContainerInfo } from "./types"; + +export const DocLinks = () => ( +
+

+ Helpful resources: +

+
+ + Dokploy Documentation + + + + Docker Swarm Guide + + + + Cluster Settings + +
+
+); + +interface SwarmNotAvailableProps { + errorMessage?: string; + onRetry: () => void; +} + +export const SwarmNotAvailable = ({ + errorMessage, + onRetry, +}: SwarmNotAvailableProps) => ( +
+ + + Swarm Not Available + + Could not reach Docker Swarm.{" "} + {errorMessage && ( + {errorMessage} + )} + + +
+

+ This feature requires Docker Swarm to be initialized and active. To get + started: +

+
    +
  1. + Initialize Swarm on your server:{" "} + + docker swarm init + +
  2. +
  3. + Verify it's active:{" "} + + docker info | grep Swarm + +
  4. +
  5. + Check the{" "} + + Cluster Settings + {" "} + page to manage your swarm nodes +
  6. +
+ +
+ +
+); + +interface ServicesErrorProps { + errorMessage?: string; + onRetry: () => void; +} + +export const ServicesError = ({ + errorMessage, + onRetry, +}: ServicesErrorProps) => ( +
+ + + Failed to Load Services + + Swarm is reachable but service listing failed.{" "} + {errorMessage && ( + {errorMessage} + )} + + +
+

This could be caused by:

+
    +
  • Permission issues running Docker commands on the server
  • +
  • Docker daemon not responding
  • +
  • + Network connectivity issues to a remote server — check{" "} + + Cluster Settings + +
  • +
+
+ +
+); + +interface NoServicesProps { + nodeCount: number; + onRefresh: () => void; +} + +export const NoServices = ({ nodeCount, onRefresh }: NoServicesProps) => ( +
+ + + No Swarm Services Found + + Docker Swarm is active with {nodeCount} node(s), but + there are no application services running in the swarm. + + +
+

+ This view shows containers deployed as Swarm services. + Standalone or Docker Compose containers won't appear here. +

+

To see containers in this view, make sure your applications are:

+
    +
  1. + Deployed as Swarm services — Applications in + Dokploy deploy to Swarm by default. Docker Compose projects need to + use{" "} + Stack{" "} + type (not{" "} + + Docker Compose + + ) to run as Swarm services. +
  2. +
  3. + Using a registry (for multi-node setups) — + Worker nodes need to pull images from a shared registry. Configure one + in{" "} + + Cluster Settings + + . +
  4. +
  5. + Successfully built and deployed — Check your + project's deployment logs for errors. +
  6. +
+ +
+ +
+); + +interface NoRunningContainersProps { + serviceCount: number; + containers: ContainerInfo[]; + onRefresh: () => void; +} + +export const NoRunningContainers = ({ + serviceCount, + containers, + onRefresh, +}: NoRunningContainersProps) => { + const hasErrors = containers.some((c) => c.Error && c.Error.trim() !== ""); + return ( +
+ + + No Running Containers + + Found {serviceCount} service(s) in the swarm, but + none have running containers. + + + {hasErrors && ( + + + Container Errors Detected + +
    + {containers + .filter((c) => c.Error && c.Error.trim() !== "") + .slice(0, 5) + .map((c) => ( +
  • + {c.Name}: {c.Error} +
  • + ))} +
+
+
+ )} +
+

This can happen when:

+
    +
  • Services are scaled to 0 replicas
  • +
  • + Containers are failing to start — check deployment logs for + errors +
  • +
  • + Images can't be pulled on worker nodes — verify your{" "} + + registry configuration + +
  • +
  • + Node constraints prevent scheduling — check placement rules in + your app's Cluster settings +
  • +
+ +
+ +
+ ); +}; diff --git a/apps/dokploy/components/dashboard/swarm/containers/show-swarm-containers.tsx b/apps/dokploy/components/dashboard/swarm/containers/show-swarm-containers.tsx index 169d888ba..4c4c5e2be 100644 --- a/apps/dokploy/components/dashboard/swarm/containers/show-swarm-containers.tsx +++ b/apps/dokploy/components/dashboard/swarm/containers/show-swarm-containers.tsx @@ -1,21 +1,24 @@ -import { useEffect, useState } from "react"; -import Link from "next/link"; import { - AlertCircle, AlertTriangle, Container, - Cpu, - ExternalLink, Info, Loader2, RefreshCw, - Server, } from "lucide-react"; +import Link from "next/link"; +import { useEffect, useState } from "react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; +import { + NoRunningContainers, + NoServices, + ServicesError, + SwarmNotAvailable, +} from "./empty-states"; import { NodeSection } from "./node-section"; +import { SummaryCards } from "./summary-cards"; import type { ContainerInfo, ContainerStat, SwarmNode } from "./types"; interface Props { @@ -25,7 +28,6 @@ interface Props { export const ShowSwarmContainers = ({ serverId }: Props) => { const [expandedNodes, setExpandedNodes] = useState>(new Set()); - // 1. Check if swarm is functioning by fetching nodes const { data: nodes, isLoading: nodesLoading, @@ -34,7 +36,6 @@ export const ShowSwarmContainers = ({ serverId }: Props) => { refetch: refetchNodes, } = api.swarm.getNodes.useQuery({ serverId }); - // 2. Fetch services (same endpoint the Overview tab uses) const { data: nodeApps, isLoading: appsLoading, @@ -46,7 +47,6 @@ export const ShowSwarmContainers = ({ serverId }: Props) => { { enabled: !nodesError && nodes !== undefined }, ); - // 3. Fetch task details for each service const applicationList = nodeApps && nodeApps.length > 0 ? nodeApps.map((app: { Name: string }) => app.Name) @@ -61,7 +61,6 @@ export const ShowSwarmContainers = ({ serverId }: Props) => { { enabled: applicationList.length > 0 }, ); - // 4. Fetch container stats for metrics const { data: stats, isLoading: statsLoading } = api.swarm.getContainerStats.useQuery( { serverId }, @@ -111,7 +110,6 @@ export const ShowSwarmContainers = ({ serverId }: Props) => { } } - // Separate running containers from unscheduled services (no tasks / scaled to 0) const runningContainers = containers.filter( (c) => c.Node !== "N/A" && @@ -120,15 +118,12 @@ export const ShowSwarmContainers = ({ serverId }: Props) => { const unscheduledServices = containers.filter((c) => c.Node === "N/A"); - // Detect down or unavailable nodes const downNodes = (nodes ?? []).filter( (n: SwarmNode) => n.Status !== "Ready" || n.Availability !== "Active", ); - // Detect if this is a multi-node swarm (metrics only available on manager) const isMultiNode = (nodes?.length ?? 0) > 1; - // Build node status lookup const nodeStatusMap = new Map(); if (nodes) { for (const node of nodes) { @@ -136,255 +131,6 @@ export const ShowSwarmContainers = ({ serverId }: Props) => { } } - // Auto-expand all nodes on first load - useEffect(() => { - if (runningContainers.length > 0 && expandedNodes.size === 0) { - const nodeNames = new Set(); - for (const c of runningContainers) { - if (c.Node) { - nodeNames.add(c.Node); - } - } - setExpandedNodes(nodeNames); - } - }, [runningContainers.length]); - - // --- Render: loading state --- - if (isLoading) { - return ( -
- Loading containers... - -
- ); - } - - // --- Render: swarm not active / unreachable (tRPC error) --- - if (nodesError) { - return ( - refetchNodes()} - /> - ); - } - - // --- Render: nodes returned undefined (docker command failed silently) --- - if (!nodesError && nodes === undefined) { - return ( - refetchNodes()} - /> - ); - } - - // --- Render: swarm active but getNodeApps failed (real error, not just empty) --- - const isRealAppsError = - appsError && !appsErrorDetail?.message?.includes("data is undefined"); - if (isRealAppsError) { - return ( -
- - - Failed to Load Services - - Swarm is reachable but service listing failed.{" "} - {appsErrorDetail?.message && ( - - {appsErrorDetail.message} - - )} - - -
-

This could be caused by:

-
    -
  • Permission issues running Docker commands on the server
  • -
  • Docker daemon not responding
  • -
  • - Network connectivity issues to a remote server — check{" "} - - Cluster Settings - -
  • -
-
- -
- ); - } - - // --- Render: swarm active, but no services deployed --- - if (!nodeApps || nodeApps.length === 0) { - return ( -
- - - No Swarm Services Found - - Docker Swarm is active with{" "} - {nodes?.length ?? 0} node(s), but there are no - application services running in the swarm. - - -
-

- This view shows containers deployed as{" "} - Swarm services. Standalone or Docker Compose - containers won't appear here. -

-

- To see containers in this view, make sure your applications are: -

-
    -
  1. - Deployed as Swarm services — Applications - in Dokploy deploy to Swarm by default. Docker Compose projects - need to use{" "} - - Stack - {" "} - type (not{" "} - - Docker Compose - - ) to run as Swarm services. -
  2. -
  3. - Using a registry (for multi-node setups) — - Worker nodes need to pull images from a shared registry. Configure - one in{" "} - - Cluster Settings - - . -
  4. -
  5. - Successfully built and deployed — Check - your project's deployment logs for errors. -
  6. -
- -
- -
- ); - } - - // --- Render: services exist but no running containers --- - if (runningContainers.length === 0) { - const hasErrors = containers.some((c) => c.Error && c.Error.trim() !== ""); - return ( -
- - - No Running Containers - - Found {nodeApps.length} service(s) in the swarm, - but none have running containers. - - - {hasErrors && ( - - - Container Errors Detected - -
    - {containers - .filter((c) => c.Error && c.Error.trim() !== "") - .slice(0, 5) - .map((c) => ( -
  • - {c.Name}: {c.Error} -
  • - ))} -
-
-
- )} -
-

This can happen when:

-
    -
  • Services are scaled to 0 replicas
  • -
  • - Containers are failing to start — check deployment logs for - errors -
  • -
  • - Images can't be pulled on worker nodes — verify your{" "} - - registry configuration - -
  • -
  • - Node constraints prevent scheduling — check placement rules - in your app's Cluster settings -
  • -
- -
- -
- ); - } - - // --- Render: main view with data --- - const nodeMap = new Map(); - for (const c of runningContainers) { - const nodeName = c.Node || "Unknown"; - if (!nodeMap.has(nodeName)) { - nodeMap.set(nodeName, []); - } - nodeMap.get(nodeName)!.push(c); - } - - const nodeGroups = []; - for (const [nodeName, nodeContainers] of nodeMap) { - nodeGroups.push({ - nodeName, - containers: nodeContainers, - nodeStatus: nodeStatusMap.get(nodeName), - }); - } - nodeGroups.sort((a, b) => a.nodeName.localeCompare(b.nodeName)); - const statsMap = new Map(); if (stats) { for (const stat of stats) { @@ -403,6 +149,18 @@ export const ShowSwarmContainers = ({ serverId }: Props) => { return undefined; }; + useEffect(() => { + if (runningContainers.length > 0 && expandedNodes.size === 0) { + const nodeNames = new Set(); + for (const c of runningContainers) { + if (c.Node) { + nodeNames.add(c.Node); + } + } + setExpandedNodes(nodeNames); + } + }, [runningContainers.length]); + const toggleNode = (nodeName: string) => { setExpandedNodes((prev: Set) => { const next = new Set(prev); @@ -420,6 +178,83 @@ export const ShowSwarmContainers = ({ serverId }: Props) => { refetchDetails(); }; + // Build node groups + const nodeMap = new Map(); + for (const c of runningContainers) { + const nodeName = c.Node || "Unknown"; + if (!nodeMap.has(nodeName)) { + nodeMap.set(nodeName, []); + } + nodeMap.get(nodeName)!.push(c); + } + + const nodeGroups = []; + for (const [nodeName, nodeContainers] of nodeMap) { + nodeGroups.push({ + nodeName, + containers: nodeContainers, + nodeStatus: nodeStatusMap.get(nodeName), + }); + } + nodeGroups.sort((a, b) => a.nodeName.localeCompare(b.nodeName)); + + if (isLoading) { + return ( +
+ Loading containers... + +
+ ); + } + + if (nodesError) { + return ( + refetchNodes()} + /> + ); + } + + if (!nodesError && nodes === undefined) { + return ( + refetchNodes()} + /> + ); + } + + const isRealAppsError = + appsError && !appsErrorDetail?.message?.includes("data is undefined"); + if (isRealAppsError) { + return ( + refetchApps()} + /> + ); + } + + if (!nodeApps || nodeApps.length === 0) { + return ( + refetchApps()} + /> + ); + } + + if (runningContainers.length === 0) { + return ( + + ); + } + return (
@@ -439,57 +274,14 @@ export const ShowSwarmContainers = ({ serverId }: Props) => {
-
- - - Swarm Nodes -
- -
-
- -
{nodes?.length ?? 0}
- {downNodes.length > 0 && ( -

- {downNodes.length} node(s) down or drained -

- )} -
-
+ - - - Services -
- -
-
- -
{nodeApps?.length ?? 0}
- {unscheduledServices.length > 0 && ( -

- {unscheduledServices.length} with no running tasks -

- )} -
-
- - - - - Running Containers - -
- -
-
- -
{runningContainers.length}
-
-
-
- - {/* Down / drained / unavailable node warning */} {downNodes.length > 0 && ( @@ -521,7 +313,6 @@ export const ShowSwarmContainers = ({ serverId }: Props) => { )} - {/* Multi-node metrics limitation notice */} {isMultiNode && ( @@ -549,7 +340,6 @@ export const ShowSwarmContainers = ({ serverId }: Props) => { ))}
- {/* Unscheduled services note */} {unscheduledServices.length > 0 && ( @@ -579,97 +369,3 @@ export const ShowSwarmContainers = ({ serverId }: Props) => { ); }; - -// --- Shared sub-components --- - -const DocLinks = () => ( -
-

- Helpful resources: -

- -
-); - -interface SwarmNotAvailableProps { - errorMessage?: string; - onRetry: () => void; -} - -const SwarmNotAvailable = ({ - errorMessage, - onRetry, -}: SwarmNotAvailableProps) => ( -
- - - Swarm Not Available - - Could not reach Docker Swarm.{" "} - {errorMessage && ( - {errorMessage} - )} - - -
-

- This feature requires Docker Swarm to be initialized and active. To get - started: -

-
    -
  1. - Initialize Swarm on your server:{" "} - - docker swarm init - -
  2. -
  3. - Verify it's active:{" "} - - docker info | grep Swarm - -
  4. -
  5. - Check the{" "} - - Cluster Settings - {" "} - page to manage your swarm nodes -
  6. -
- -
- -
-); diff --git a/apps/dokploy/components/dashboard/swarm/containers/summary-cards.tsx b/apps/dokploy/components/dashboard/swarm/containers/summary-cards.tsx new file mode 100644 index 000000000..4ae418ad0 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/containers/summary-cards.tsx @@ -0,0 +1,68 @@ +import { Container, Cpu, Server } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +interface SummaryCardsProps { + nodeCount: number; + downNodeCount: number; + serviceCount: number; + unscheduledCount: number; + runningContainerCount: number; +} + +export const SummaryCards = ({ + nodeCount, + downNodeCount, + serviceCount, + unscheduledCount, + runningContainerCount, +}: SummaryCardsProps) => ( +
+ + + Swarm Nodes +
+ +
+
+ +
{nodeCount}
+ {downNodeCount > 0 && ( +

+ {downNodeCount} node(s) down or drained +

+ )} +
+
+ + + + Services +
+ +
+
+ +
{serviceCount}
+ {unscheduledCount > 0 && ( +

+ {unscheduledCount} with no running tasks +

+ )} +
+
+ + + + + Running Containers + +
+ +
+
+ +
{runningContainerCount}
+
+
+
+);