"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import { AgentLink } from "@/components/AgentLink";
import { CheckStatusSummary } from "@/components/CheckStatusSummary";
import { ClientPager } from "@/components/ClientPager";
import { EmptyPanel, SummaryCell, Td, Th } from "@/components/DataPanel";
import { DetailTabs } from "@/components/DetailTabs";
import { PageSizeSelect } from "@/components/PageSizeSelect";
import { Pager } from "@/components/Pager";
import { DateTime } from "@/components/TimeZoneControls";
import type { components } from "@/lib/api-types";
import { groupChecks, statusSeverity, type Agent, type CheckGroup } from "@/lib/check-groups";
import { statusBadgeClass } from "@/lib/format";
import {
DEFAULT_PAGE_SIZE,
clampPage,
pageCountOf,
paginate,
type PageSize,
} from "@/lib/pagination";
import { usePolling } from "@/lib/usePolling";
import type { InventoryData } from "@/lib/view-types";
type StoredEvent = components["schemas"]["StoredCheckResultEvent"];
type EventsPageData = { items: StoredEvent[]; next_cursor?: string | null; prev_cursor?: string | null };
type TabKey = "agents" | "events";
export function CheckDetailBrowser({
checkId,
initialData,
initialEvents,
tab,
eventsLimit,
eventsCursor,
eventsBack,
eventsPage,
}: {
checkId: string;
initialData: InventoryData;
initialEvents: EventsPageData;
tab: TabKey;
eventsLimit: PageSize;
eventsCursor?: string;
eventsBack: boolean;
eventsPage: number;
}) {
const { data } = usePolling(initialData, "/api/poll/inventory");
const agentsById = useMemo(
() => new Map(data.agents.map((agent) => [agent.agent_id, agent])),
[data.agents],
);
const group = useMemo(
() => groupChecks(data.checks, agentsById).find((item) => item.checkId === checkId),
[agentsById, checkId, data.checks],
);
if (!group) {
return (
← checks
{checkId}
Check not found.
);
}
return (
← checks
{checkId}
- status
-
- agents
- {group.checks.length}
- last_observed_at
-
{tab === "agents" ? (
) : (
)}
);
}
function AgentsForCheck({ group }: { group: CheckGroup }) {
const rows = useMemo(
() =>
[...group.checks].sort(
(a, b) =>
statusSeverity[b.check.status] - statusSeverity[a.check.status] ||
hostName(a).localeCompare(hostName(b)),
),
[group.checks],
);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [page, setPage] = useState(1);
const onPageSizeChange = (next: PageSize) => {
setPageSize(next);
setPage(1);
};
const pageCount = pageCountOf(rows.length, pageSize);
const currentPage = clampPage(page, pageCount);
const visible = paginate(rows, currentPage, pageSize);
return (
| HOST |
STATUS |
LAST_OBSERVED_AT |
SUMMARY |
{visible.map(({ check, agent }) => (
|
|
{check.status} |
|
))}
);
}
function EventsForCheck({
checkId,
initialData,
agentsById,
eventsLimit,
eventsCursor,
eventsBack,
eventsPage,
}: {
checkId: string;
initialData: EventsPageData;
agentsById: Map;
eventsLimit: PageSize;
eventsCursor?: string;
eventsBack: boolean;
eventsPage: number;
}) {
const router = useRouter();
const pollUrl = useMemo(() => {
const qs = new URLSearchParams({
check_id: checkId,
limit: String(eventsLimit),
});
if (eventsCursor) qs.set("cursor", eventsCursor);
if (eventsBack) qs.set("back", "1");
return `/api/poll/events?${qs.toString()}`;
}, [checkId, eventsCursor, eventsBack, eventsLimit]);
const { data } = usePolling(initialData, pollUrl);
const onLimitChange = (next: PageSize) => {
router.push(
`/checks/${encodeURIComponent(checkId)}?tab=events&events_limit=${next}`,
);
};
return (
{data.items.length === 0 ? (
) : (
| OBSERVED_AT |
RECEIVED_AT |
HOST |
STATUS |
DURATION_MS |
SUMMARY |
{data.items.map((event) => {
const agent = agentsById.get(event.agent_id);
return (
|
|
|
|
{event.status} |
{event.duration_ms} |
);
})}
)}
);
}
function hostName(row: { agent?: Agent; check: { agent_id: string } }): string {
return row.agent?.hostname || row.check.agent_id;
}