milestone 9: react spa admin ui, frontend ci and embedded dist
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { ApiError } from "@/lib/api";
|
||||
import { healthQuery, statsQuery, timeseriesQuery, upstreamHealthQuery } from "@/lib/queries";
|
||||
import type { Period } from "@/lib/types";
|
||||
import DiskCard from "./DiskCard";
|
||||
import HealthBanners from "./HealthBanners";
|
||||
import StatCards from "./StatCards";
|
||||
import TimeseriesChart from "./TimeseriesChart";
|
||||
import UpstreamHealthTable from "./UpstreamHealthTable";
|
||||
|
||||
const PERIODS: Period[] = ["1h", "24h", "7d", "30d"];
|
||||
|
||||
function PeriodPicker({ period, onChange }: { period: Period; onChange: (period: Period) => void }) {
|
||||
return (
|
||||
<div role="group" aria-label="Period" className="flex gap-1">
|
||||
{PERIODS.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
aria-pressed={option === period}
|
||||
onClick={() => onChange(option)}
|
||||
className={`rounded px-2.5 py-1 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 ${
|
||||
option === period
|
||||
? "bg-zinc-200 font-medium text-zinc-900 dark:bg-zinc-700 dark:text-zinc-50"
|
||||
: "text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800"
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InlineError({ error, onRetry }: { error: unknown; onRetry: () => void }) {
|
||||
const message = error instanceof ApiError ? error.message : "request failed";
|
||||
return (
|
||||
<div className="rounded border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-900 dark:border-red-900 dark:bg-red-950 dark:text-red-200">
|
||||
Failed to load: {message}{" "}
|
||||
<button type="button" onClick={onRetry} className="font-medium underline">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Skeleton({ height }: { height: number }) {
|
||||
return <div aria-hidden="true" className="animate-pulse rounded bg-zinc-200 dark:bg-zinc-800" style={{ height }} />;
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [period, setPeriod] = useState<Period>("24h");
|
||||
const stats = useQuery({ ...statsQuery(period), placeholderData: keepPreviousData });
|
||||
const timeseries = useQuery({ ...timeseriesQuery(period), placeholderData: keepPreviousData });
|
||||
const health = useQuery(healthQuery());
|
||||
const upstreamHealth = useQuery(upstreamHealthQuery());
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h1 className="text-2xl font-semibold">Dashboard</h1>
|
||||
<PeriodPicker period={period} onChange={setPeriod} />
|
||||
</div>
|
||||
|
||||
{health.data !== undefined && <HealthBanners health={health.data} />}
|
||||
|
||||
{stats.isError ? (
|
||||
<InlineError error={stats.error} onRetry={() => void stats.refetch()} />
|
||||
) : stats.data === undefined ? (
|
||||
<Skeleton height={76} />
|
||||
) : (
|
||||
<StatCards stats={stats.data} />
|
||||
)}
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[2fr_1fr]">
|
||||
<section className="rounded border border-zinc-200 bg-white px-4 py-3 dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<h2 className="mb-3 text-sm font-semibold">Queries over time</h2>
|
||||
{timeseries.isError ? (
|
||||
<InlineError error={timeseries.error} onRetry={() => void timeseries.refetch()} />
|
||||
) : timeseries.data === undefined ? (
|
||||
<Skeleton height={240} />
|
||||
) : (
|
||||
<TimeseriesChart data={timeseries.data} />
|
||||
)}
|
||||
</section>
|
||||
{health.data === undefined ? <Skeleton height={160} /> : <DiskCard disk={health.data.disk} />}
|
||||
</div>
|
||||
|
||||
{upstreamHealth.isError ? (
|
||||
<InlineError error={upstreamHealth.error} onRetry={() => void upstreamHealth.refetch()} />
|
||||
) : upstreamHealth.data === undefined ? (
|
||||
<Skeleton height={120} />
|
||||
) : (
|
||||
<UpstreamHealthTable health={upstreamHealth.data} />
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user