animation

This commit is contained in:
null 2026-05-27 19:22:55 -05:00
parent 4d3f57870a
commit f8daef6ee4
9 changed files with 218 additions and 37 deletions

View File

@ -1413,6 +1413,7 @@ export default function DashboardPage() {
secondary={`${formatCount(agents.length)} total`}
icon={<Bot className="h-4 w-4" />}
tone="accent"
isLoading={!metrics && metricsQuery.isLoading}
/>
<DashboardMetricCard
title="Tasks In Progress"
@ -1420,6 +1421,7 @@ export default function DashboardPage() {
secondary={`${formatCount(tasksTotal)} total`}
icon={<LayoutGrid className="h-4 w-4" />}
tone="success"
isLoading={!metrics && metricsQuery.isLoading}
/>
<DashboardMetricCard
title="Error Rate"
@ -1427,6 +1429,7 @@ export default function DashboardPage() {
secondary={`${formatCount(Number(latestThroughputPoint?.value ?? 0))} completed (latest)`}
icon={<Activity className="h-4 w-4" />}
tone="warning"
isLoading={!metrics && metricsQuery.isLoading}
/>
<DashboardMetricCard
title="Completion Speed"
@ -1435,6 +1438,7 @@ export default function DashboardPage() {
infoText={`Based on ${DASHBOARD_RANGE_LABEL}`}
icon={<Timer className="h-4 w-4" />}
tone="success"
isLoading={!metrics && metricsQuery.isLoading}
/>
</div>
@ -1491,12 +1495,14 @@ export default function DashboardPage() {
title="Workload"
rows={workloadRows}
tone="accent"
isLoading={!metrics && metricsQuery.isLoading}
/>
<DashboardInfoBlock
title="Throughput"
infoText={`All throughput values are calculated for ${DASHBOARD_RANGE_LABEL}`}
rows={throughputRows}
tone="success"
isLoading={!metrics && metricsQuery.isLoading}
/>
<DashboardInfoBlock
title="Gateway Health"
@ -1509,6 +1515,7 @@ export default function DashboardPage() {
? "danger"
: "warning"
}
isLoading={!metrics && metricsQuery.isLoading}
/>
</div>
@ -1552,6 +1559,7 @@ export default function DashboardPage() {
<RecentActivitySection
events={recentLogs}
feedHref={activityFeedHref}
isLoading={activityQuery.isLoading}
onRowClick={handleLogRowClick}
onRowKeyDown={handleLogRowKeyDown}
buildHref={buildActivityEventHref}

View File

@ -14,6 +14,7 @@ interface DashboardInfoBlockProps {
badge?: { text: string; tone: BadgeTone };
rows: InfoRow[];
tone?: SectionToneKey;
isLoading?: boolean;
}
/**
@ -26,6 +27,7 @@ export function DashboardInfoBlock({
badge,
rows,
tone = "neutral",
isLoading = false,
}: DashboardInfoBlockProps) {
return (
<DashboardSection
@ -35,32 +37,51 @@ export function DashboardInfoBlock({
tone={tone}
>
<div className="overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)]">
{rows.map((row) => (
<div
key={`${row.label}-${row.value}`}
className="flex items-start justify-between gap-3 border-b border-[color:var(--border)] px-3 py-2 last:border-b-0"
>
<span className="flex min-w-0 items-center gap-2 text-sm text-muted">
<span
className={`h-1.5 w-1.5 rounded-full ${
row.tone === "success"
? "bg-[color:var(--success)]"
: row.tone === "warning"
? "bg-[color:var(--warning)]"
: row.tone === "danger"
? "bg-[color:var(--danger)]"
: "bg-[color:var(--border-strong)]"
}`}
/>
{row.label}
</span>
<span
className={`max-w-[65%] break-words text-right text-sm font-medium leading-5 ${toneText[row.tone ?? "default"]}`}
>
{row.value}
</span>
</div>
))}
{isLoading
? [28, 20, 36, 24, 32].map((w, i) => (
<div
key={i}
className="flex items-center justify-between gap-3 border-b border-[color:var(--border)] px-3 py-2 last:border-b-0"
>
<span className="flex items-center gap-2">
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--border-strong)] opacity-50" />
<div
className="h-3.5 animate-pulse rounded bg-[color:var(--surface-strong)]"
style={{ width: `${w * 4}px` }}
/>
</span>
<div
className="h-3.5 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-70"
style={{ width: `${(5 - i % 3) * 12}px` }}
/>
</div>
))
: rows.map((row) => (
<div
key={`${row.label}-${row.value}`}
className="flex items-start justify-between gap-3 border-b border-[color:var(--border)] px-3 py-2 last:border-b-0"
>
<span className="flex min-w-0 items-center gap-2 text-sm text-muted">
<span
className={`h-1.5 w-1.5 rounded-full ${
row.tone === "success"
? "bg-[color:var(--success)]"
: row.tone === "warning"
? "bg-[color:var(--warning)]"
: row.tone === "danger"
? "bg-[color:var(--danger)]"
: "bg-[color:var(--border-strong)]"
}`}
/>
{row.label}
</span>
<span
className={`max-w-[65%] break-words text-right text-sm font-medium leading-5 ${toneText[row.tone ?? "default"]}`}
>
{row.value}
</span>
</div>
))}
</div>
</DashboardSection>
);

View File

@ -9,6 +9,7 @@ interface DashboardMetricCardProps {
infoText?: string;
icon: ReactNode;
tone: MetricToneKey;
isLoading?: boolean;
}
/**
@ -22,6 +23,7 @@ export function DashboardMetricCard({
infoText,
icon,
tone,
isLoading = false,
}: DashboardMetricCardProps) {
return (
<section
@ -47,12 +49,18 @@ export function DashboardMetricCard({
)}
</div>
<div className="mt-2 flex items-end gap-2">
<p className="font-heading text-4xl font-bold text-strong">
{value}
</p>
{secondary && (
<p className="pb-1 text-xs text-muted">{secondary}</p>
{isLoading ? (
<div className="h-10 w-20 animate-pulse rounded bg-[color:var(--surface-strong)]" />
) : (
<p className="font-heading text-4xl font-bold text-strong">
{value}
</p>
)}
{isLoading ? (
<div className="mb-1 h-3 w-16 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
) : secondary ? (
<p className="pb-1 text-xs text-muted">{secondary}</p>
) : null}
</div>
</div>
<div className={`rounded-lg p-2 ${toneIcon[tone]}`}>{icon}</div>

View File

@ -56,7 +56,27 @@ export function GatewayCronPanel({
action={jobs.length > 0 ? { label: `${jobs.length}`, href: "#" } : undefined}
>
{isLoading && !cron ? (
<DashboardEmptyState message="Loading cron data…" />
<div className="space-y-1.5">
{[0, 1].map((i) => (
<div
key={i}
className="flex min-w-0 items-start gap-2 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2"
>
<div className="mt-0.5 h-3.5 w-3.5 shrink-0 animate-pulse rounded-full bg-[color:var(--surface-strong)]" />
<div className="min-w-0 flex-1 space-y-1.5">
<div className="flex items-baseline gap-2">
<div className="h-3.5 w-32 animate-pulse rounded bg-[color:var(--surface-strong)]" />
<div className="h-3 w-20 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
</div>
<div className="flex gap-3">
<div className="h-3 w-16 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-50" />
<div className="h-3 w-16 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-50" />
</div>
</div>
<div className="h-3 w-8 shrink-0 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
</div>
))}
</div>
) : jobs.length === 0 ? (
<DashboardEmptyState message="No cron jobs found on this gateway." />
) : (

View File

@ -63,7 +63,23 @@ export function GatewayHealthPanel({
return (
<DashboardSection title="Gateway Health">
{isLoading && !health ? (
<DashboardEmptyState message="Loading health data…" />
<div className="space-y-3">
{["CPU", "Memory", "Disk"].map((label) => (
<div key={label}>
<div className="mb-1.5 h-3 w-12 animate-pulse rounded bg-[color:var(--surface-strong)]" />
<div className="flex items-center gap-2">
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-[color:var(--surface-strong)]">
<div className="h-full w-1/3 animate-pulse rounded-full bg-[color:var(--surface-strong)] opacity-60" />
</div>
<div className="h-3 w-10 animate-pulse rounded bg-[color:var(--surface-strong)]" />
</div>
</div>
))}
<div className="flex gap-4 border-t border-[color:var(--border)] pt-3">
<div className="h-3 w-20 animate-pulse rounded bg-[color:var(--surface-strong)]" />
<div className="h-3 w-24 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
</div>
</div>
) : !health || (!c?.cpu_pct && !c?.memory_pct && !c?.uptime_seconds) ? (
<DashboardEmptyState message="Health data unavailable. The gateway may not expose system metrics." />
) : (

View File

@ -35,7 +35,23 @@ export function PendingApprovalsSection({
tone={items.length > 0 ? "warning" : "success"}
>
{isLoading ? (
<DashboardEmptyState message="Loading pending approvals..." />
<div className="overflow-hidden rounded-lg border border-[color:rgba(251,191,36,0.24)] bg-[color:var(--surface-muted)]">
{[0, 1, 2].map((i) => (
<div
key={i}
className="flex items-center justify-between gap-3 border-b border-[color:var(--border)] px-3 py-2 last:border-b-0"
>
<span className="min-w-0 flex-1">
<span className="mb-1 flex items-center gap-2">
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--warning)] opacity-40" />
<span className="h-3.5 w-40 animate-pulse rounded bg-[color:var(--surface-strong)]" />
</span>
<span className="h-3 w-28 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
</span>
<span className="h-3 w-10 shrink-0 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
</div>
))}
</div>
) : hasError ? (
<DashboardEmptyState
tone="warning"

View File

@ -11,6 +11,7 @@ export type ActivityEvent = ActivityEventRead;
interface RecentActivitySectionProps {
events: ActivityEvent[];
feedHref: string;
isLoading?: boolean;
onRowClick: (e: MouseEvent<HTMLDivElement>, href: string) => void;
onRowKeyDown: (e: KeyboardEvent<HTMLDivElement>, href: string) => void;
buildHref: (event: ActivityEvent) => string;
@ -51,6 +52,7 @@ const eventTone = (eventType: string) => {
export function RecentActivitySection({
events,
feedHref,
isLoading = false,
onRowClick,
onRowKeyDown,
buildHref,
@ -64,7 +66,33 @@ export function RecentActivitySection({
tone="accent"
>
<div className="max-h-[310px] space-y-2 overflow-x-hidden overflow-y-auto pr-1">
{events.length > 0 ? (
{isLoading && events.length === 0 ? (
<>
{[60, 80, 48, 72].map((titleW, i) => (
<div
key={i}
className="overflow-hidden rounded-lg border border-l-4 border-[color:var(--border)] border-l-[color:var(--accent)] bg-[color:var(--surface-muted)] px-3 py-2"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1 space-y-1.5">
<div
className="h-3.5 animate-pulse rounded bg-[color:var(--surface-strong)]"
style={{ width: `${titleW}%` }}
/>
<div className="flex items-center gap-2">
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--accent)] opacity-40" />
<div className="h-3 w-24 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
</div>
</div>
<div className="shrink-0 space-y-1 text-right">
<div className="ml-auto h-3 w-10 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
<div className="ml-auto h-3 w-16 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-40" />
</div>
</div>
</div>
))}
</>
) : events.length > 0 ? (
events.map((event) => {
const href = buildHref(event);
const tone = eventTone(event.event_type);

View File

@ -413,7 +413,50 @@ export function RuntimeUsageSection({
return (
<DashboardSection title="Runtime Usage">
{isLoading && !usage ? (
<DashboardEmptyState message="Loading usage data…" />
<div className="space-y-3">
{/* Provider windows skeleton */}
<div className="space-y-2 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)] p-3">
<div className="h-3 w-40 animate-pulse rounded bg-[color:var(--surface-strong)]" />
<div className="space-y-2">
{[0, 1].map((i) => (
<div
key={i}
className="rounded-md border border-[color:var(--border)] bg-[color:var(--surface)] p-2"
>
<div className="mb-1 flex items-center justify-between gap-2">
<div className="h-3.5 w-32 animate-pulse rounded bg-[color:var(--surface-strong)]" />
<div className="h-3 w-14 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-[color:var(--surface-muted)]">
<div
className="h-full animate-pulse rounded-full bg-[color:var(--surface-strong)]"
style={{ width: `${45 + i * 20}%` }}
/>
</div>
<div className="mt-1 h-3 w-48 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-50" />
</div>
))}
</div>
</div>
{/* Stat cards skeleton */}
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{[
{ w: "w-16" },
{ w: "w-14" },
{ w: "w-20" },
{ w: "w-12" },
].map(({ w }, i) => (
<div
key={i}
className="flex flex-col gap-1 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)] p-3"
>
<div className="h-3 w-20 animate-pulse rounded bg-[color:var(--surface-strong)]" />
<div className={`h-6 ${w} animate-pulse rounded bg-[color:var(--surface-strong)]`} />
<div className="h-3 w-16 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-50" />
</div>
))}
</div>
</div>
) : noData ? (
<DashboardEmptyState message="No usage data yet. Usage appears after the first model call." />
) : hasRuntimeData || providerRows.length > 0 || perGatewayUsage.length > 0 ? (

View File

@ -47,7 +47,28 @@ export function SessionsSection({
{!hasConfiguredGateways ? (
<DashboardEmptyState message="No gateways are configured for any board yet." />
) : isLoading ? (
<DashboardEmptyState message="Loading sessions..." />
<div className="space-y-2">
{[0, 1].map((i) => (
<div
key={i}
className="overflow-hidden rounded-lg border border-l-4 border-[color:rgba(52,211,153,0.28)] border-l-[color:var(--success)] bg-[color:var(--surface-muted)] px-3 py-2"
>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-[color:var(--success)] opacity-40" />
<div className="h-3.5 w-36 animate-pulse rounded bg-[color:var(--surface-strong)]" />
</div>
<div className="h-3 w-28 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
</div>
<div className="min-w-0 max-w-[45%] space-y-1 text-right">
<div className="ml-auto h-3.5 w-16 animate-pulse rounded bg-[color:var(--surface-strong)]" />
<div className="ml-auto h-3 w-24 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
</div>
</div>
</div>
))}
</div>
) : sessions.length > 0 ? (
<>
{gatewayUnavailableCount > 0 && (