From 02019e93475ca2f3667d5a4b853d040c3552cf48 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 4 Apr 2026 21:57:53 +0200 Subject: [PATCH] feat: add dashboard page with tenant overview and KPI stats Replaces placeholder DashboardPage with a full implementation: tenant name + tier badge, KPI strip (environments, total apps, running, stopped), environment list with per-env app counts, and a recent deployments placeholder. Uses EnvApps helper component to fetch per-environment app data without violating hook rules. Co-Authored-By: Claude Sonnet 4.6 --- ui/src/pages/DashboardPage.tsx | 222 ++++++++++++++++++++++++++++++++- 1 file changed, 220 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/DashboardPage.tsx b/ui/src/pages/DashboardPage.tsx index 00ff2a0..62b8a92 100644 --- a/ui/src/pages/DashboardPage.tsx +++ b/ui/src/pages/DashboardPage.tsx @@ -1,3 +1,221 @@ -export function DashboardPage() { - return
Dashboard
; +import React, { useState, useCallback, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Badge, + Button, + Card, + EmptyState, + KpiStrip, + Spinner, +} from '@cameleer/design-system'; +import { useAuthStore } from '../auth/auth-store'; +import { useTenant, useEnvironments, useApps } from '../api/hooks'; +import { RequirePermission } from '../components/RequirePermission'; +import type { EnvironmentResponse, AppResponse } from '../types/api'; + +// Helper: fetches apps for one environment and reports data upward via effect +function EnvApps({ + environment, + onData, +}: { + environment: EnvironmentResponse; + onData: (envId: string, apps: AppResponse[]) => void; +}) { + const { data } = useApps(environment.id); + useEffect(() => { + if (data) { + onData(environment.id, data); + } + }, [data, environment.id, onData]); + return null; } + +function tierColor(tier: string): 'primary' | 'success' | 'warning' | 'error' { + switch (tier?.toLowerCase()) { + case 'enterprise': return 'success'; + case 'pro': return 'primary'; + case 'starter': return 'warning'; + default: return 'primary'; + } +} + +export function DashboardPage() { + const navigate = useNavigate(); + const tenantId = useAuthStore((s) => s.tenantId); + + const { data: tenant, isLoading: tenantLoading } = useTenant(tenantId ?? ''); + const { data: environments, isLoading: envsLoading } = useEnvironments(tenantId ?? ''); + + // Collect apps per environment using a ref-like approach via state + callback + const [appsByEnv, setAppsByEnv] = useState>({}); + + const handleAppsData = useCallback((envId: string, apps: AppResponse[]) => { + setAppsByEnv((prev) => { + if (prev[envId] === apps) return prev; // stable reference, no update + return { ...prev, [envId]: apps }; + }); + }, []); + + const allApps = Object.values(appsByEnv).flat(); + const runningApps = allApps.filter((a) => a.currentDeploymentId !== null); + // "Failed" is apps that have a previous deployment but no current (stopped) — approximate heuristic + const failedApps = allApps.filter( + (a) => a.currentDeploymentId === null && a.previousDeploymentId !== null, + ); + + const isLoading = tenantLoading || envsLoading; + + const kpiItems = [ + { + label: 'Environments', + value: environments?.length ?? 0, + subtitle: 'isolated runtime contexts', + }, + { + label: 'Total Apps', + value: allApps.length, + subtitle: 'across all environments', + }, + { + label: 'Running', + value: runningApps.length, + trend: { + label: 'active deployments', + variant: 'success' as const, + }, + }, + { + label: 'Stopped', + value: failedApps.length, + trend: failedApps.length > 0 + ? { label: 'need attention', variant: 'warning' as const } + : { label: 'none', variant: 'muted' as const }, + }, + ]; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!tenantId) { + return ( + + ); + } + + return ( +
+ {/* Tenant Header */} +
+
+

+ {tenant?.name ?? tenantId} +

+ {tenant?.tier && ( + + )} +
+
+ + + + +
+
+ + {/* KPI Strip */} + + + {/* Environments overview */} + {environments && environments.length > 0 ? ( + + {/* Render hidden data-fetchers for each environment */} + {environments.map((env) => ( + + ))} +
+ {environments.map((env) => { + const envApps = appsByEnv[env.id] ?? []; + const envRunning = envApps.filter((a) => a.currentDeploymentId !== null).length; + return ( +
navigate(`/environments/${env.id}`)} + > +
+ + {env.displayName} + + +
+
+ {envApps.length} apps + {envRunning} running + +
+
+ ); + })} +
+
+ ) : ( + + + + } + /> + )} + + {/* Recent deployments placeholder */} + + {allApps.length === 0 ? ( + + ) : ( +

+ Select an app from an environment to view its deployment history. +

+ )} +
+
+ ); +} +