Improve overview: named chips for stopped services, view-all navigation

Services card: running services shown as green dots, stopped/unknown
services promoted to named chips with colored borders for visibility.
Section headers (Services, Deployments) get "View all >" links that
navigate to the corresponding tab.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-03 22:57:59 +01:00
parent 059d9037b3
commit 4024005319

View File

@@ -43,6 +43,33 @@ const projects: ProjectDef[] = [
},
];
function SectionHeader({ icon, title, badge, tabTarget, onNavigate }: {
icon: string;
title: string;
badge?: React.ReactNode;
tabTarget?: string;
onNavigate?: (tab: string) => void;
}) {
return (
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 flex items-center gap-2">
<Icon name={icon} size={16} className="text-slate-400 dark:text-stone-500" />
{title}
{badge}
</h2>
{tabTarget && onNavigate && (
<button
onClick={() => onNavigate(tabTarget)}
className="text-xs text-slate-400 dark:text-stone-600 hover:text-slate-600 dark:hover:text-stone-400 transition-colors flex items-center gap-1"
>
View all
<Icon name="chevron-right" size={12} />
</button>
)}
</div>
);
}
function ProjectCard({ project, services, healthStatus }: {
project: ProjectDef;
services: Service[];
@@ -87,10 +114,15 @@ export function OverviewTab() {
deployments,
deploymentsLoading,
discoveredServices,
setActiveTab,
} = usePortal();
const runningCount = services.filter(s => healthStatus[s.name] === 'running').length;
const stoppedCount = services.filter(s => healthStatus[s.name] === 'stopped').length;
const runningServices = services.filter(s => healthStatus[s.name] === 'running');
const stoppedServices = services.filter(s => healthStatus[s.name] === 'stopped');
const unknownServices = services.filter(s => {
const st = healthStatus[s.name];
return st !== 'running' && st !== 'stopped';
});
const totalCount = services.length;
const recentDeployments = deployments.slice(0, 5);
@@ -109,61 +141,81 @@ export function OverviewTab() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
{/* Services */}
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-3 flex items-center gap-2">
<Icon name="server" size={16} className="text-slate-400 dark:text-stone-500" />
Services
{isDiscovered && (
<SectionHeader
icon="server"
title="Services"
tabTarget="services"
onNavigate={setActiveTab}
badge={isDiscovered ? (
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-cyan-50 dark:bg-cyan-900/20 text-cyan-600 dark:text-cyan-400 font-normal">
Auto-discovered
</span>
)}
</h2>
) : undefined}
/>
<div className="flex items-baseline gap-3 mb-3">
<span className="text-3xl font-bold text-slate-900 dark:text-stone-100 tabular-nums">{runningCount}</span>
<span className="text-3xl font-bold text-slate-900 dark:text-stone-100 tabular-nums">{runningServices.length}</span>
<span className="text-sm text-slate-500 dark:text-stone-500">of {totalCount} running</span>
</div>
<div className="flex gap-3 mb-3">
<div className="flex items-center gap-1.5 text-sm">
<span className="w-2 h-2 rounded-full bg-emerald-500" />
<span className="text-slate-600 dark:text-stone-400">{runningCount} running</span>
{/* Stopped services as named chips */}
{stoppedServices.length > 0 && (
<div className="mb-3">
<p className="text-[11px] font-medium text-red-500/70 dark:text-red-400/70 uppercase tracking-wider mb-1.5">Stopped</p>
<div className="flex flex-wrap gap-1.5">
{stoppedServices.map(s => (
<span
key={s.name}
className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border border-red-100 dark:border-red-800/30"
>
<span className="w-1.5 h-1.5 rounded-full bg-red-500 flex-shrink-0" />
{s.name}
</span>
))}
</div>
{stoppedCount > 0 && (
<div className="flex items-center gap-1.5 text-sm">
<span className="w-2 h-2 rounded-full bg-red-500" />
<span className="text-slate-600 dark:text-stone-400">{stoppedCount} stopped</span>
</div>
)}
{totalCount - runningCount - stoppedCount > 0 && (
<div className="flex items-center gap-1.5 text-sm">
<span className="w-2 h-2 rounded-full bg-slate-400" />
<span className="text-slate-600 dark:text-stone-400">{totalCount - runningCount - stoppedCount} unknown</span>
</div>
)}
</div>
{/* Unknown services as named chips */}
{unknownServices.length > 0 && (
<div className="mb-3">
<p className="text-[11px] font-medium text-slate-400/70 dark:text-stone-500/70 uppercase tracking-wider mb-1.5">Unknown</p>
<div className="flex flex-wrap gap-1.5">
{unknownServices.map(s => (
<span
key={s.name}
className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium bg-slate-50 dark:bg-stone-800/50 text-slate-500 dark:text-stone-500 border border-slate-200 dark:border-stone-700/50"
>
<span className="w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-stone-600 flex-shrink-0" />
{s.name}
</span>
))}
</div>
</div>
)}
{/* Running services as green dots */}
{runningServices.length > 0 && (
<div className="flex flex-wrap gap-1">
{services.map(s => {
const status = healthStatus[s.name];
const color = status === 'running' ? 'bg-emerald-500' : status === 'stopped' ? 'bg-red-500' : 'bg-slate-400';
return (
{runningServices.map(s => (
<div
key={s.name}
title={`${s.name}: ${status || 'unknown'}`}
className={`w-2.5 h-2.5 rounded-sm ${color}`}
title={s.name}
className="w-2.5 h-2.5 rounded-sm bg-emerald-500"
/>
);
})}
))}
</div>
)}
</div>
{/* Recent Deployments */}
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-3 flex items-center gap-2">
<Icon name="rocket" size={16} className="text-slate-400 dark:text-stone-500" />
Recent Deployments
</h2>
<SectionHeader
icon="rocket"
title="Recent Deployments"
tabTarget="deployments"
onNavigate={setActiveTab}
/>
{deploymentsLoading && deployments.length === 0 ? (
<div className="space-y-3">