animation
This commit is contained in:
parent
4d3f57870a
commit
f8daef6ee4
|
|
@ -1413,6 +1413,7 @@ export default function DashboardPage() {
|
||||||
secondary={`${formatCount(agents.length)} total`}
|
secondary={`${formatCount(agents.length)} total`}
|
||||||
icon={<Bot className="h-4 w-4" />}
|
icon={<Bot className="h-4 w-4" />}
|
||||||
tone="accent"
|
tone="accent"
|
||||||
|
isLoading={!metrics && metricsQuery.isLoading}
|
||||||
/>
|
/>
|
||||||
<DashboardMetricCard
|
<DashboardMetricCard
|
||||||
title="Tasks In Progress"
|
title="Tasks In Progress"
|
||||||
|
|
@ -1420,6 +1421,7 @@ export default function DashboardPage() {
|
||||||
secondary={`${formatCount(tasksTotal)} total`}
|
secondary={`${formatCount(tasksTotal)} total`}
|
||||||
icon={<LayoutGrid className="h-4 w-4" />}
|
icon={<LayoutGrid className="h-4 w-4" />}
|
||||||
tone="success"
|
tone="success"
|
||||||
|
isLoading={!metrics && metricsQuery.isLoading}
|
||||||
/>
|
/>
|
||||||
<DashboardMetricCard
|
<DashboardMetricCard
|
||||||
title="Error Rate"
|
title="Error Rate"
|
||||||
|
|
@ -1427,6 +1429,7 @@ export default function DashboardPage() {
|
||||||
secondary={`${formatCount(Number(latestThroughputPoint?.value ?? 0))} completed (latest)`}
|
secondary={`${formatCount(Number(latestThroughputPoint?.value ?? 0))} completed (latest)`}
|
||||||
icon={<Activity className="h-4 w-4" />}
|
icon={<Activity className="h-4 w-4" />}
|
||||||
tone="warning"
|
tone="warning"
|
||||||
|
isLoading={!metrics && metricsQuery.isLoading}
|
||||||
/>
|
/>
|
||||||
<DashboardMetricCard
|
<DashboardMetricCard
|
||||||
title="Completion Speed"
|
title="Completion Speed"
|
||||||
|
|
@ -1435,6 +1438,7 @@ export default function DashboardPage() {
|
||||||
infoText={`Based on ${DASHBOARD_RANGE_LABEL}`}
|
infoText={`Based on ${DASHBOARD_RANGE_LABEL}`}
|
||||||
icon={<Timer className="h-4 w-4" />}
|
icon={<Timer className="h-4 w-4" />}
|
||||||
tone="success"
|
tone="success"
|
||||||
|
isLoading={!metrics && metricsQuery.isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1491,12 +1495,14 @@ export default function DashboardPage() {
|
||||||
title="Workload"
|
title="Workload"
|
||||||
rows={workloadRows}
|
rows={workloadRows}
|
||||||
tone="accent"
|
tone="accent"
|
||||||
|
isLoading={!metrics && metricsQuery.isLoading}
|
||||||
/>
|
/>
|
||||||
<DashboardInfoBlock
|
<DashboardInfoBlock
|
||||||
title="Throughput"
|
title="Throughput"
|
||||||
infoText={`All throughput values are calculated for ${DASHBOARD_RANGE_LABEL}`}
|
infoText={`All throughput values are calculated for ${DASHBOARD_RANGE_LABEL}`}
|
||||||
rows={throughputRows}
|
rows={throughputRows}
|
||||||
tone="success"
|
tone="success"
|
||||||
|
isLoading={!metrics && metricsQuery.isLoading}
|
||||||
/>
|
/>
|
||||||
<DashboardInfoBlock
|
<DashboardInfoBlock
|
||||||
title="Gateway Health"
|
title="Gateway Health"
|
||||||
|
|
@ -1509,6 +1515,7 @@ export default function DashboardPage() {
|
||||||
? "danger"
|
? "danger"
|
||||||
: "warning"
|
: "warning"
|
||||||
}
|
}
|
||||||
|
isLoading={!metrics && metricsQuery.isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1552,6 +1559,7 @@ export default function DashboardPage() {
|
||||||
<RecentActivitySection
|
<RecentActivitySection
|
||||||
events={recentLogs}
|
events={recentLogs}
|
||||||
feedHref={activityFeedHref}
|
feedHref={activityFeedHref}
|
||||||
|
isLoading={activityQuery.isLoading}
|
||||||
onRowClick={handleLogRowClick}
|
onRowClick={handleLogRowClick}
|
||||||
onRowKeyDown={handleLogRowKeyDown}
|
onRowKeyDown={handleLogRowKeyDown}
|
||||||
buildHref={buildActivityEventHref}
|
buildHref={buildActivityEventHref}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ interface DashboardInfoBlockProps {
|
||||||
badge?: { text: string; tone: BadgeTone };
|
badge?: { text: string; tone: BadgeTone };
|
||||||
rows: InfoRow[];
|
rows: InfoRow[];
|
||||||
tone?: SectionToneKey;
|
tone?: SectionToneKey;
|
||||||
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,6 +27,7 @@ export function DashboardInfoBlock({
|
||||||
badge,
|
badge,
|
||||||
rows,
|
rows,
|
||||||
tone = "neutral",
|
tone = "neutral",
|
||||||
|
isLoading = false,
|
||||||
}: DashboardInfoBlockProps) {
|
}: DashboardInfoBlockProps) {
|
||||||
return (
|
return (
|
||||||
<DashboardSection
|
<DashboardSection
|
||||||
|
|
@ -35,32 +37,51 @@ export function DashboardInfoBlock({
|
||||||
tone={tone}
|
tone={tone}
|
||||||
>
|
>
|
||||||
<div className="overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)]">
|
<div className="overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)]">
|
||||||
{rows.map((row) => (
|
{isLoading
|
||||||
<div
|
? [28, 20, 36, 24, 32].map((w, i) => (
|
||||||
key={`${row.label}-${row.value}`}
|
<div
|
||||||
className="flex items-start justify-between gap-3 border-b border-[color:var(--border)] px-3 py-2 last:border-b-0"
|
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 min-w-0 items-center gap-2 text-sm text-muted">
|
>
|
||||||
<span
|
<span className="flex items-center gap-2">
|
||||||
className={`h-1.5 w-1.5 rounded-full ${
|
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--border-strong)] opacity-50" />
|
||||||
row.tone === "success"
|
<div
|
||||||
? "bg-[color:var(--success)]"
|
className="h-3.5 animate-pulse rounded bg-[color:var(--surface-strong)]"
|
||||||
: row.tone === "warning"
|
style={{ width: `${w * 4}px` }}
|
||||||
? "bg-[color:var(--warning)]"
|
/>
|
||||||
: row.tone === "danger"
|
</span>
|
||||||
? "bg-[color:var(--danger)]"
|
<div
|
||||||
: "bg-[color:var(--border-strong)]"
|
className="h-3.5 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-70"
|
||||||
}`}
|
style={{ width: `${(5 - i % 3) * 12}px` }}
|
||||||
/>
|
/>
|
||||||
{row.label}
|
</div>
|
||||||
</span>
|
))
|
||||||
<span
|
: rows.map((row) => (
|
||||||
className={`max-w-[65%] break-words text-right text-sm font-medium leading-5 ${toneText[row.tone ?? "default"]}`}
|
<div
|
||||||
>
|
key={`${row.label}-${row.value}`}
|
||||||
{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>
|
>
|
||||||
</div>
|
<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>
|
</div>
|
||||||
</DashboardSection>
|
</DashboardSection>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ interface DashboardMetricCardProps {
|
||||||
infoText?: string;
|
infoText?: string;
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
tone: MetricToneKey;
|
tone: MetricToneKey;
|
||||||
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -22,6 +23,7 @@ export function DashboardMetricCard({
|
||||||
infoText,
|
infoText,
|
||||||
icon,
|
icon,
|
||||||
tone,
|
tone,
|
||||||
|
isLoading = false,
|
||||||
}: DashboardMetricCardProps) {
|
}: DashboardMetricCardProps) {
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
|
|
@ -47,12 +49,18 @@ export function DashboardMetricCard({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-end gap-2">
|
<div className="mt-2 flex items-end gap-2">
|
||||||
<p className="font-heading text-4xl font-bold text-strong">
|
{isLoading ? (
|
||||||
{value}
|
<div className="h-10 w-20 animate-pulse rounded bg-[color:var(--surface-strong)]" />
|
||||||
</p>
|
) : (
|
||||||
{secondary && (
|
<p className="font-heading text-4xl font-bold text-strong">
|
||||||
<p className="pb-1 text-xs text-muted">{secondary}</p>
|
{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>
|
</div>
|
||||||
<div className={`rounded-lg p-2 ${toneIcon[tone]}`}>{icon}</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}
|
action={jobs.length > 0 ? { label: `${jobs.length}`, href: "#" } : undefined}
|
||||||
>
|
>
|
||||||
{isLoading && !cron ? (
|
{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 ? (
|
) : jobs.length === 0 ? (
|
||||||
<DashboardEmptyState message="No cron jobs found on this gateway." />
|
<DashboardEmptyState message="No cron jobs found on this gateway." />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,23 @@ export function GatewayHealthPanel({
|
||||||
return (
|
return (
|
||||||
<DashboardSection title="Gateway Health">
|
<DashboardSection title="Gateway Health">
|
||||||
{isLoading && !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) ? (
|
) : !health || (!c?.cpu_pct && !c?.memory_pct && !c?.uptime_seconds) ? (
|
||||||
<DashboardEmptyState message="Health data unavailable. The gateway may not expose system metrics." />
|
<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"}
|
tone={items.length > 0 ? "warning" : "success"}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{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 ? (
|
) : hasError ? (
|
||||||
<DashboardEmptyState
|
<DashboardEmptyState
|
||||||
tone="warning"
|
tone="warning"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export type ActivityEvent = ActivityEventRead;
|
||||||
interface RecentActivitySectionProps {
|
interface RecentActivitySectionProps {
|
||||||
events: ActivityEvent[];
|
events: ActivityEvent[];
|
||||||
feedHref: string;
|
feedHref: string;
|
||||||
|
isLoading?: boolean;
|
||||||
onRowClick: (e: MouseEvent<HTMLDivElement>, href: string) => void;
|
onRowClick: (e: MouseEvent<HTMLDivElement>, href: string) => void;
|
||||||
onRowKeyDown: (e: KeyboardEvent<HTMLDivElement>, href: string) => void;
|
onRowKeyDown: (e: KeyboardEvent<HTMLDivElement>, href: string) => void;
|
||||||
buildHref: (event: ActivityEvent) => string;
|
buildHref: (event: ActivityEvent) => string;
|
||||||
|
|
@ -51,6 +52,7 @@ const eventTone = (eventType: string) => {
|
||||||
export function RecentActivitySection({
|
export function RecentActivitySection({
|
||||||
events,
|
events,
|
||||||
feedHref,
|
feedHref,
|
||||||
|
isLoading = false,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
onRowKeyDown,
|
onRowKeyDown,
|
||||||
buildHref,
|
buildHref,
|
||||||
|
|
@ -64,7 +66,33 @@ export function RecentActivitySection({
|
||||||
tone="accent"
|
tone="accent"
|
||||||
>
|
>
|
||||||
<div className="max-h-[310px] space-y-2 overflow-x-hidden overflow-y-auto pr-1">
|
<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) => {
|
events.map((event) => {
|
||||||
const href = buildHref(event);
|
const href = buildHref(event);
|
||||||
const tone = eventTone(event.event_type);
|
const tone = eventTone(event.event_type);
|
||||||
|
|
|
||||||
|
|
@ -413,7 +413,50 @@ export function RuntimeUsageSection({
|
||||||
return (
|
return (
|
||||||
<DashboardSection title="Runtime Usage">
|
<DashboardSection title="Runtime Usage">
|
||||||
{isLoading && !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 ? (
|
) : noData ? (
|
||||||
<DashboardEmptyState message="No usage data yet. Usage appears after the first model call." />
|
<DashboardEmptyState message="No usage data yet. Usage appears after the first model call." />
|
||||||
) : hasRuntimeData || providerRows.length > 0 || perGatewayUsage.length > 0 ? (
|
) : hasRuntimeData || providerRows.length > 0 || perGatewayUsage.length > 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,28 @@ export function SessionsSection({
|
||||||
{!hasConfiguredGateways ? (
|
{!hasConfiguredGateways ? (
|
||||||
<DashboardEmptyState message="No gateways are configured for any board yet." />
|
<DashboardEmptyState message="No gateways are configured for any board yet." />
|
||||||
) : isLoading ? (
|
) : 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 ? (
|
) : sessions.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{gatewayUnavailableCount > 0 && (
|
{gatewayUnavailableCount > 0 && (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue