animation
This commit is contained in:
parent
4d3f57870a
commit
f8daef6ee4
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,7 +37,26 @@ export function DashboardInfoBlock({
|
|||
tone={tone}
|
||||
>
|
||||
<div className="overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)]">
|
||||
{rows.map((row) => (
|
||||
{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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
{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>
|
||||
{secondary && (
|
||||
<p className="pb-1 text-xs text-muted">{secondary}</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>
|
||||
|
|
|
|||
|
|
@ -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." />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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." />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
Loading…
Reference in New Issue