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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,221 @@
|
|||||||
export function DashboardPage() {
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
return <div>Dashboard</div>;
|
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<Record<string, AppResponse[]>>({});
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tenantId) {
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
title="No tenant associated"
|
||||||
|
description="Your account is not linked to a tenant. Please contact your administrator."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 p-6">
|
||||||
|
{/* Tenant Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<h1 className="text-2xl font-semibold text-white">
|
||||||
|
{tenant?.name ?? tenantId}
|
||||||
|
</h1>
|
||||||
|
{tenant?.tier && (
|
||||||
|
<Badge
|
||||||
|
label={tenant.tier.toUpperCase()}
|
||||||
|
color={tierColor(tenant.tier)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RequirePermission permission="apps:manage">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => navigate('/environments/new')}
|
||||||
|
>
|
||||||
|
New Environment
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => navigate('/dashboard')}
|
||||||
|
>
|
||||||
|
View Observability Dashboard
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* KPI Strip */}
|
||||||
|
<KpiStrip items={kpiItems} />
|
||||||
|
|
||||||
|
{/* Environments overview */}
|
||||||
|
{environments && environments.length > 0 ? (
|
||||||
|
<Card title="Environments">
|
||||||
|
{/* Render hidden data-fetchers for each environment */}
|
||||||
|
{environments.map((env) => (
|
||||||
|
<EnvApps key={env.id} environment={env} onData={handleAppsData} />
|
||||||
|
))}
|
||||||
|
<div className="divide-y divide-white/10">
|
||||||
|
{environments.map((env) => {
|
||||||
|
const envApps = appsByEnv[env.id] ?? [];
|
||||||
|
const envRunning = envApps.filter((a) => a.currentDeploymentId !== null).length;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={env.id}
|
||||||
|
className="flex items-center justify-between py-3 first:pt-0 last:pb-0 cursor-pointer hover:bg-white/5 px-2 rounded"
|
||||||
|
onClick={() => navigate(`/environments/${env.id}`)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm font-medium text-white">
|
||||||
|
{env.displayName}
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
label={env.slug}
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-sm text-white/60">
|
||||||
|
<span>{envApps.length} apps</span>
|
||||||
|
<span className="text-green-400">{envRunning} running</span>
|
||||||
|
<Badge
|
||||||
|
label={env.status}
|
||||||
|
color={env.status === 'ACTIVE' ? 'success' : 'warning'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<EmptyState
|
||||||
|
title="No environments yet"
|
||||||
|
description="Create your first environment to get started deploying Camel applications."
|
||||||
|
action={
|
||||||
|
<RequirePermission permission="apps:manage">
|
||||||
|
<Button variant="primary" onClick={() => navigate('/environments/new')}>
|
||||||
|
Create Environment
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Recent deployments placeholder */}
|
||||||
|
<Card title="Recent Deployments">
|
||||||
|
{allApps.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
title="No deployments yet"
|
||||||
|
description="Deploy your first app to see deployment history here."
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-white/60">
|
||||||
|
Select an app from an environment to view its deployment history.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user