Skip to content

Commit 7fb79bf

Browse files
committed
Link health grid cells to events
1 parent 1e8262d commit 7fb79bf

3 files changed

Lines changed: 76 additions & 12 deletions

File tree

web/src/components/HealthGrid.tsx

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Fragment } from "react";
2+
import { Link } from "react-router-dom";
23
import clsx from "clsx";
3-
import type { HealthCell } from "../api/types";
4+
import type { Filter, HealthCell } from "../api/types";
45
import { formatTimestamp } from "../lib/utils";
56

67
interface Props {
78
// Outer = days (chronological), inner = 96 cells per day (15-minute spans).
89
grid: HealthCell[][];
10+
filter?: Filter;
911
}
1012

1113
function cellTone(cell: HealthCell, maxTotal: number): string {
@@ -19,7 +21,7 @@ function cellTone(cell: HealthCell, maxTotal: number): string {
1921
return "bg-success/40";
2022
}
2123

22-
export default function HealthGrid({ grid }: Props) {
24+
export default function HealthGrid({ grid, filter }: Props) {
2325
if (!grid || grid.length === 0) {
2426
return (
2527
<div className="bg-panel border border-border rounded-lg p-6 text-muted text-sm text-center">
@@ -63,14 +65,27 @@ export default function HealthGrid({ grid }: Props) {
6365
</div>
6466
{days.map((day, di) => {
6567
const cell = day.hours[hour];
68+
const title = `${day.title} ${hourLabel(hour)}${cell.total} requests, ${cell.failed} failed${
69+
cell.bucket ? ` (${formatTimestamp(cell.bucket)})` : ""
70+
}`;
6671
return (
6772
<div key={`${di}-${hour}`} className="flex h-3 items-center justify-center">
68-
<div
69-
className={clsx("h-3 w-3 rounded-[2px]", cellTone(cell, maxTotal))}
70-
title={`${day.title} ${hourLabel(hour)}${cell.total} requests, ${cell.failed} failed${
71-
cell.bucket ? ` (${formatTimestamp(cell.bucket)})` : ""
72-
}`}
73-
/>
73+
{cell.total > 0 && cell.bucket ? (
74+
<Link
75+
to={{ pathname: "/events", search: eventSearch(cell, filter) }}
76+
className={clsx(
77+
"block h-3 w-3 rounded-[2px] transition-shadow hover:ring-1 hover:ring-accent focus:outline-none focus:ring-1 focus:ring-accent",
78+
cellTone(cell, maxTotal),
79+
)}
80+
title={title}
81+
aria-label={`Open events for ${title}`}
82+
/>
83+
) : (
84+
<div
85+
className={clsx("h-3 w-3 rounded-[2px]", cellTone(cell, maxTotal))}
86+
title={title}
87+
/>
88+
)}
7489
</div>
7590
);
7691
})}
@@ -82,6 +97,32 @@ export default function HealthGrid({ grid }: Props) {
8297
);
8398
}
8499

100+
function eventSearch(cell: HealthCell, filter?: Filter): string {
101+
const start = new Date(cell.bucket);
102+
if (Number.isNaN(start.getTime())) return "";
103+
const end = new Date(start.getTime() + 60 * 60 * 1000);
104+
const sp = new URLSearchParams();
105+
sp.set("range", "custom");
106+
sp.set("start", formatDateTimeParam(start));
107+
sp.set("end", formatDateTimeParam(end));
108+
for (const model of filter?.models ?? []) sp.append("model", model);
109+
for (const source of filter?.sources ?? []) sp.append("source", source);
110+
for (const key of filter?.apiKey ?? []) sp.append("api_key", key);
111+
if (filter?.authIndex) sp.set("auth_index", filter.authIndex);
112+
if (filter?.result) sp.set("result", filter.result);
113+
return `?${sp.toString()}`;
114+
}
115+
116+
function formatDateTimeParam(d: Date): string {
117+
const year = d.getFullYear();
118+
const month = String(d.getMonth() + 1).padStart(2, "0");
119+
const day = String(d.getDate()).padStart(2, "0");
120+
const hour = String(d.getHours()).padStart(2, "0");
121+
const minute = String(d.getMinutes()).padStart(2, "0");
122+
const second = String(d.getSeconds()).padStart(2, "0");
123+
return `${year}-${month}-${day}T${hour}:${minute}:${second}`;
124+
}
125+
85126
function hourlyCells(day: HealthCell[]): HealthCell[] {
86127
return Array.from({ length: 24 }, (_, hour) => {
87128
const cells = day.slice(hour * 4, hour * 4 + 4);

web/src/pages/Events.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1-
import { useEffect, useState } from "react";
1+
import { useEffect, useMemo, useState } from "react";
2+
import { useSearchParams } from "react-router-dom";
23
import FilterBar from "../components/FilterBar";
34
import Table, { Column } from "../components/Table";
45
import EventLogModal from "../components/EventLogModal";
56
import { api } from "../api/client";
67
import { todayFilter, useFilter } from "../hooks/useFilter";
78
import { useRefreshTick } from "../lib/refresh";
89
import { formatCost, formatLatency, formatNumber, formatTimestamp } from "../lib/utils";
9-
import type { UsageEventRecord, UsageEventsPage } from "../api/types";
10+
import type { Filter, RangeKey, ResultFilter, UsageEventRecord, UsageEventsPage } from "../api/types";
1011

1112
const PAGE_SIZES = [20, 50, 100, 500, 1000];
13+
const RANGE_KEYS: RangeKey[] = ["all", "today", "4h", "8h", "12h", "24h", "7d", "30d", "custom"];
14+
const RESULT_KEYS: ResultFilter[] = ["", "success", "failed"];
1215

1316
export default function EventsPage() {
14-
const { filter, setFilter } = useFilter(todayFilter);
17+
const [searchParams] = useSearchParams();
18+
const initialFilter = useMemo(() => filterFromSearch(searchParams), [searchParams]);
19+
const { filter, setFilter } = useFilter(initialFilter);
1520
const [page, setPage] = useState(1);
1621
const [pageSize, setPageSize] = useState(100);
1722
const [data, setData] = useState<UsageEventsPage | null>(null);
@@ -158,6 +163,24 @@ export default function EventsPage() {
158163
);
159164
}
160165

166+
function filterFromSearch(sp: URLSearchParams): Filter {
167+
const rangeParam = sp.get("range");
168+
const range = RANGE_KEYS.includes(rangeParam as RangeKey) ? (rangeParam as RangeKey) : todayFilter.range;
169+
const resultParam = sp.get("result") ?? "";
170+
const result = RESULT_KEYS.includes(resultParam as ResultFilter) ? (resultParam as ResultFilter) : "";
171+
return {
172+
...todayFilter,
173+
range,
174+
start: range === "custom" ? sp.get("start") || undefined : undefined,
175+
end: range === "custom" ? sp.get("end") || undefined : undefined,
176+
models: sp.getAll("model"),
177+
sources: sp.getAll("source"),
178+
apiKey: sp.getAll("api_key"),
179+
authIndex: sp.get("auth_index") || "",
180+
result,
181+
};
182+
}
183+
161184
interface PageProps {
162185
total: number;
163186
page: number;

web/src/pages/Overview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export default function Overview() {
105105

106106
<div>
107107
<h2 className="text-sm uppercase tracking-wider text-muted mb-2">Health</h2>
108-
{data && <HealthGrid grid={data.health_grid || []} />}
108+
{data && <HealthGrid grid={data.health_grid || []} filter={filter} />}
109109
</div>
110110
</div>
111111
</div>

0 commit comments

Comments
 (0)