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 }: {
|
function ProjectCard({ project, services, healthStatus }: {
|
||||||
project: ProjectDef;
|
project: ProjectDef;
|
||||||
services: Service[];
|
services: Service[];
|
||||||
@@ -87,10 +114,15 @@ export function OverviewTab() {
|
|||||||
deployments,
|
deployments,
|
||||||
deploymentsLoading,
|
deploymentsLoading,
|
||||||
discoveredServices,
|
discoveredServices,
|
||||||
|
setActiveTab,
|
||||||
} = usePortal();
|
} = usePortal();
|
||||||
|
|
||||||
const runningCount = services.filter(s => healthStatus[s.name] === 'running').length;
|
const runningServices = services.filter(s => healthStatus[s.name] === 'running');
|
||||||
const stoppedCount = services.filter(s => healthStatus[s.name] === 'stopped').length;
|
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 totalCount = services.length;
|
||||||
|
|
||||||
const recentDeployments = deployments.slice(0, 5);
|
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">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||||
{/* Services */}
|
{/* Services */}
|
||||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
|
<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">
|
<SectionHeader
|
||||||
<Icon name="server" size={16} className="text-slate-400 dark:text-stone-500" />
|
icon="server"
|
||||||
Services
|
title="Services"
|
||||||
{isDiscovered && (
|
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">
|
<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
|
Auto-discovered
|
||||||
</span>
|
</span>
|
||||||
)}
|
) : undefined}
|
||||||
</h2>
|
/>
|
||||||
|
|
||||||
<div className="flex items-baseline gap-3 mb-3">
|
<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>
|
<span className="text-sm text-slate-500 dark:text-stone-500">of {totalCount} running</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 mb-3">
|
{/* Stopped services as named chips */}
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
{stoppedServices.length > 0 && (
|
||||||
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
<div className="mb-3">
|
||||||
<span className="text-slate-600 dark:text-stone-400">{runningCount} running</span>
|
<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>
|
||||||
</div>
|
</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>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-1">
|
{/* Unknown services as named chips */}
|
||||||
{services.map(s => {
|
{unknownServices.length > 0 && (
|
||||||
const status = healthStatus[s.name];
|
<div className="mb-3">
|
||||||
const color = status === 'running' ? 'bg-emerald-500' : status === 'stopped' ? 'bg-red-500' : 'bg-slate-400';
|
<p className="text-[11px] font-medium text-slate-400/70 dark:text-stone-500/70 uppercase tracking-wider mb-1.5">Unknown</p>
|
||||||
return (
|
<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">
|
||||||
|
{runningServices.map(s => (
|
||||||
<div
|
<div
|
||||||
key={s.name}
|
key={s.name}
|
||||||
title={`${s.name}: ${status || 'unknown'}`}
|
title={s.name}
|
||||||
className={`w-2.5 h-2.5 rounded-sm ${color}`}
|
className="w-2.5 h-2.5 rounded-sm bg-emerald-500"
|
||||||
/>
|
/>
|
||||||
);
|
))}
|
||||||
})}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Deployments */}
|
{/* 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">
|
<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">
|
<SectionHeader
|
||||||
<Icon name="rocket" size={16} className="text-slate-400 dark:text-stone-500" />
|
icon="rocket"
|
||||||
Recent Deployments
|
title="Recent Deployments"
|
||||||
</h2>
|
tabTarget="deployments"
|
||||||
|
onNavigate={setActiveTab}
|
||||||
|
/>
|
||||||
|
|
||||||
{deploymentsLoading && deployments.length === 0 ? (
|
{deploymentsLoading && deployments.length === 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user