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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user