mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 08:15:17 +00:00
feat(ui): migrate home page to design system Monitor archetype
Wrap in PageShell + PageHeader + MonitorLayout. Replace bespoke hero stat row with KpiRow + 4 KpiCards (profiles, cliproxy, accounts, health with status-derived tone). AuthMonitor and ErrorLogsMonitor become MonitorCard children; the 'logs moved' callout becomes a MonitorCard. Pure layout migration -- behavior preserved (navigation, configuration warning Alert, hooks unchanged). Hero punch retained via tone-coded KPI cards. LOC: 208 -> 173.
This commit is contained in:
1 parent
b868852e63
commit
3ae5f20dc8
1 file changed
+137
-172
+137
-172
@@ -1,68 +1,38 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { HeroSection } from '@/components/layout/hero-section';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
ActivityIcon,
|
||||||
|
AlertTriangle,
|
||||||
|
ArrowRight,
|
||||||
|
KeyIcon,
|
||||||
|
ScrollText,
|
||||||
|
ShieldCheck,
|
||||||
|
UsersIcon,
|
||||||
|
Zap,
|
||||||
|
} from 'lucide-react';
|
||||||
import { AuthMonitor } from '@/components/monitoring/auth-monitor';
|
import { AuthMonitor } from '@/components/monitoring/auth-monitor';
|
||||||
import { ErrorLogsMonitor } from '@/components/error-logs-monitor';
|
import { ErrorLogsMonitor } from '@/components/error-logs-monitor';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Key, Zap, Users, Activity, AlertTriangle, ArrowRight, ScrollText } from 'lucide-react';
|
|
||||||
import { useOverview } from '@/hooks/use-overview';
|
import { useOverview } from '@/hooks/use-overview';
|
||||||
import { useSharedSummary } from '@/hooks/use-shared';
|
import { useSharedSummary } from '@/hooks/use-shared';
|
||||||
import { cn } from '@/lib/utils';
|
import { PageShell, PageHeader } from '@/components/page-shell';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import {
|
||||||
import { useTranslation } from 'react-i18next';
|
MonitorLayout,
|
||||||
|
KpiRow,
|
||||||
|
KpiCard,
|
||||||
|
MonitorGrid,
|
||||||
|
MonitorCard,
|
||||||
|
} from '@/components/monitor-layout';
|
||||||
|
|
||||||
const HEALTH_VARIANTS = {
|
const HEALTH_TONE = {
|
||||||
ok: 'success',
|
ok: 'positive',
|
||||||
warning: 'warning',
|
warning: 'warning',
|
||||||
error: 'error',
|
error: 'negative',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type StatVariant = 'default' | 'success' | 'warning' | 'error' | 'accent';
|
|
||||||
|
|
||||||
const variantStyles: Record<StatVariant, { iconBg: string; iconColor: string }> = {
|
|
||||||
default: { iconBg: 'bg-muted', iconColor: 'text-muted-foreground' },
|
|
||||||
success: { iconBg: 'bg-green-600/15', iconColor: 'text-green-700 dark:text-green-500' },
|
|
||||||
warning: { iconBg: 'bg-amber-500/15', iconColor: 'text-amber-700 dark:text-amber-400' },
|
|
||||||
error: { iconBg: 'bg-red-600/15', iconColor: 'text-red-700 dark:text-red-500' },
|
|
||||||
accent: { iconBg: 'bg-accent/15', iconColor: 'text-accent' },
|
|
||||||
};
|
|
||||||
|
|
||||||
function InlineStat({
|
|
||||||
title,
|
|
||||||
value,
|
|
||||||
icon: Icon,
|
|
||||||
variant = 'default',
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
value: number | string;
|
|
||||||
icon: LucideIcon;
|
|
||||||
variant?: StatVariant;
|
|
||||||
onClick?: () => void;
|
|
||||||
}) {
|
|
||||||
const styles = variantStyles[variant];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-3 px-4 py-2.5 rounded-lg border bg-card/50',
|
|
||||||
'transition-all hover:bg-card hover:shadow-sm hover:-translate-y-0.5',
|
|
||||||
'active:scale-[0.98]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={cn('flex items-center justify-center w-9 h-9 rounded-md', styles.iconBg)}>
|
|
||||||
<Icon className={cn('w-4 h-4', styles.iconColor)} />
|
|
||||||
</div>
|
|
||||||
<div className="text-left">
|
|
||||||
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">{title}</p>
|
|
||||||
<p className={cn('text-lg font-bold font-mono leading-tight', styles.iconColor)}>{value}</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -70,139 +40,134 @@ export function HomePage() {
|
|||||||
const { data: shared, isLoading: isSharedLoading } = useSharedSummary();
|
const { data: shared, isLoading: isSharedLoading } = useSharedSummary();
|
||||||
|
|
||||||
if (isOverviewLoading || isSharedLoading) {
|
if (isOverviewLoading || isSharedLoading) {
|
||||||
return (
|
return <HomeLoadingSkeleton />;
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
{/* Hero Row Skeleton */}
|
|
||||||
<div className="rounded-xl border p-6 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Skeleton className="h-12 w-12 rounded-lg" />
|
|
||||||
<div>
|
|
||||||
<Skeleton className="h-7 w-[180px] mb-2" />
|
|
||||||
<Skeleton className="h-4 w-[220px]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{[1, 2, 3, 4].map((i) => (
|
|
||||||
<Skeleton key={i} className="h-14 w-28 rounded-lg" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Auth Monitor Skeleton */}
|
|
||||||
<div className="border rounded-xl overflow-hidden">
|
|
||||||
<div className="px-4 py-2.5 border-b flex items-center justify-between">
|
|
||||||
<Skeleton className="h-4 w-32" />
|
|
||||||
<Skeleton className="h-4 w-20" />
|
|
||||||
</div>
|
|
||||||
<div className="px-4 py-3 border-b">
|
|
||||||
<Skeleton className="h-2 w-full rounded-full" />
|
|
||||||
</div>
|
|
||||||
{[1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="px-4 py-2.5 flex items-center gap-3 border-b last:border-b-0">
|
|
||||||
<Skeleton className="w-2.5 h-2.5 rounded-full" />
|
|
||||||
<Skeleton className="h-4 flex-1" />
|
|
||||||
<Skeleton className="h-1.5 w-24 rounded-full" />
|
|
||||||
<Skeleton className="h-4 w-16" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const healthVariant = overview?.health
|
const healthTone = overview?.health
|
||||||
? HEALTH_VARIANTS[overview.health.status as keyof typeof HEALTH_VARIANTS]
|
? HEALTH_TONE[overview.health.status as keyof typeof HEALTH_TONE]
|
||||||
: undefined;
|
: 'default';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<PageShell>
|
||||||
{/* Hero Row: Logo/Title + Inline Stats */}
|
<PageHeader
|
||||||
<div className="relative overflow-hidden rounded-xl border bg-gradient-to-br from-background via-background to-muted/30">
|
title={t('heroSection.title')}
|
||||||
{/* Subtle background pattern */}
|
description={t('heroSection.subtitle')}
|
||||||
<div className="absolute inset-0 opacity-[0.03] pointer-events-none">
|
status={
|
||||||
<div
|
overview?.version && (
|
||||||
className="absolute inset-0"
|
<Badge variant="outline" className="font-mono text-xs">
|
||||||
style={{
|
v{overview.version}
|
||||||
backgroundImage: `radial-gradient(circle at 1px 1px, currentColor 1px, transparent 0)`,
|
</Badge>
|
||||||
backgroundSize: '24px 24px',
|
)
|
||||||
}}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
<MonitorLayout
|
||||||
|
kpis={
|
||||||
{/* Single Row Layout */}
|
<KpiRow>
|
||||||
<div className="relative p-6 flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
<KpiCard
|
||||||
{/* Left: Logo + Title */}
|
label={t('home.profiles')}
|
||||||
<HeroSection version={overview?.version} />
|
|
||||||
|
|
||||||
{/* Right: Inline Stats */}
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
|
||||||
<InlineStat
|
|
||||||
title={t('home.profiles')}
|
|
||||||
value={overview?.profiles ?? 0}
|
value={overview?.profiles ?? 0}
|
||||||
icon={Key}
|
icon={<KeyIcon className="size-4" />}
|
||||||
variant="accent"
|
|
||||||
onClick={() => navigate('/providers')}
|
|
||||||
/>
|
/>
|
||||||
<InlineStat
|
<KpiCard
|
||||||
title={t('home.cliproxy')}
|
label={t('home.cliproxy')}
|
||||||
value={overview?.cliproxy ?? 0}
|
value={overview?.cliproxy ?? 0}
|
||||||
icon={Zap}
|
icon={<Zap className="size-4" />}
|
||||||
variant="accent"
|
|
||||||
onClick={() => navigate('/cliproxy')}
|
|
||||||
/>
|
/>
|
||||||
<InlineStat
|
<KpiCard
|
||||||
title={t('home.accounts')}
|
label={t('home.accounts')}
|
||||||
value={overview?.accounts ?? 0}
|
value={overview?.accounts ?? 0}
|
||||||
icon={Users}
|
icon={<UsersIcon className="size-4" />}
|
||||||
variant="default"
|
|
||||||
onClick={() => navigate('/accounts')}
|
|
||||||
/>
|
/>
|
||||||
<InlineStat
|
<KpiCard
|
||||||
title={t('home.health')}
|
label={t('home.health')}
|
||||||
value={overview?.health ? `${overview.health.passed}/${overview.health.total}` : '-'}
|
value={overview?.health ? `${overview.health.passed}/${overview.health.total}` : '—'}
|
||||||
icon={Activity}
|
tone={healthTone}
|
||||||
variant={healthVariant}
|
icon={<ActivityIcon className="size-4" />}
|
||||||
onClick={() => navigate('/health')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</KpiRow>
|
||||||
</div>
|
}
|
||||||
</div>
|
>
|
||||||
|
{shared?.symlinkStatus && !shared.symlinkStatus.valid && (
|
||||||
|
<Alert variant="warning">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertTitle>{t('home.configurationRequired')}</AlertTitle>
|
||||||
|
<AlertDescription>{shared.symlinkStatus.message}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Configuration Warning */}
|
<MonitorGrid>
|
||||||
{shared?.symlinkStatus && !shared.symlinkStatus.valid && (
|
{/* Live account monitor — primary viz */}
|
||||||
<Alert variant="warning">
|
<MonitorCard
|
||||||
<AlertTriangle className="h-4 w-4" />
|
span={12}
|
||||||
<AlertTitle>{t('home.configurationRequired')}</AlertTitle>
|
title={
|
||||||
<AlertDescription>{shared.symlinkStatus.message}</AlertDescription>
|
<span className="flex items-center gap-2">
|
||||||
</Alert>
|
<ShieldCheck className="size-4" />
|
||||||
)}
|
Live account monitor
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AuthMonitor />
|
||||||
|
</MonitorCard>
|
||||||
|
|
||||||
{/* Auth Monitor */}
|
{/* Logs callout */}
|
||||||
<AuthMonitor />
|
<MonitorCard span={12}>
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="rounded-xl border bg-card/70 p-5">
|
<div className="flex items-start gap-3">
|
||||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div className="rounded-xl bg-muted p-2.5">
|
||||||
<div className="flex items-start gap-3">
|
<ScrollText className="h-5 w-5 text-muted-foreground" />
|
||||||
<div className="rounded-xl bg-muted p-2.5">
|
</div>
|
||||||
<ScrollText className="h-5 w-5 text-muted-foreground" />
|
<div className="space-y-1">
|
||||||
|
<h2 className="text-base font-semibold">{t('homePageV2.logsMoved')}</h2>
|
||||||
|
<p className="max-w-2xl text-sm text-muted-foreground">
|
||||||
|
Use the unified logs page for source-level filtering, structured entry
|
||||||
|
inspection, and retention policy edits without crowding the home dashboard.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" className="gap-2" onClick={() => navigate('/logs')}>
|
||||||
|
Open logs
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
</MonitorCard>
|
||||||
<h2 className="text-lg font-semibold">{t('homePageV2.logsMoved')}</h2>
|
|
||||||
<p className="max-w-2xl text-sm text-muted-foreground">
|
|
||||||
Use the unified logs page for source-level filtering, structured entry inspection,
|
|
||||||
and retention policy edits without crowding the home dashboard.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" className="gap-2" onClick={() => navigate('/logs')}>
|
|
||||||
{/* TODO i18n: missing key for "Open logs" */}
|
|
||||||
Open logs
|
|
||||||
<ArrowRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ErrorLogsMonitor />
|
{/* Recent errors */}
|
||||||
</div>
|
<MonitorCard
|
||||||
|
span={12}
|
||||||
|
title={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="size-4" />
|
||||||
|
Recent errors
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ErrorLogsMonitor />
|
||||||
|
</MonitorCard>
|
||||||
|
</MonitorGrid>
|
||||||
|
</MonitorLayout>
|
||||||
|
</PageShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HomeLoadingSkeleton() {
|
||||||
|
return (
|
||||||
|
<PageShell>
|
||||||
|
<PageHeader title={<Skeleton className="h-6 w-40" />} />
|
||||||
|
<MonitorLayout
|
||||||
|
kpis={
|
||||||
|
<KpiRow>
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<Skeleton key={i} className="h-20 rounded-xl" />
|
||||||
|
))}
|
||||||
|
</KpiRow>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MonitorGrid>
|
||||||
|
<MonitorCard span={12}>
|
||||||
|
<Skeleton className="h-48 w-full rounded" />
|
||||||
|
</MonitorCard>
|
||||||
|
</MonitorGrid>
|
||||||
|
</MonitorLayout>
|
||||||
|
</PageShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user