The portal timeline is now available
Webhook Dashboards, Ready to Paste
The Repost portal's surfaces as blocks: delivery history, endpoint management, the dead-letter queue and the event catalog. Wired to your data, owned by you. Open Source.
Files
"use client";
import {
RepostPortalProvider,
type PortalClient,
useDeliveryAttempts,
useLogsFeed,
useLogsHistogram,
usePortalContext,
useReplayDelivery,
} from "@repost/portal-react";
import { useCallback, useMemo, useState } from "react";
import { DeliveryDetail, RowDetailSheet } from "@/components/delivery-detail";
import { DeliveryHistoryTable, deliveryHistoryRowId } from "@/components/delivery-history-table";
import {
TimelineHistogram,
useTimelineScale,
type TimelineDateRange,
type TimelineRange,
type TimelineSeries,
type TimelineSlot,
} from "@/components/timeline-histogram";
import { WebhookMetricsCards } from "@/components/webhook-metrics-cards";
import { StatusIndicator } from "@/components/ui/status-indicator";
import { DEFAULT_LIVE_DATE_WINDOW, type LiveDateWindow } from "@/hooks/live-date-window";
import { cn } from "@/lib/utils";
import type { WebhookLogRow } from "@/lib/webhook-types";
export type WebhookDashboardProps = {
/**
* Return a portal access token minted by your backend. Called on load, on
* expiry, and after auth failures — never expose your Repost API key to
* the browser.
*/
getToken?: () => Promise<string>;
/**
* A pre-built client, instead of `getToken` — used by previews and tests to
* run the block against fixtures.
*/
client?: PortalClient;
/** Narrow the dashboard to one endpoint. */
endpointId?: string;
/** How far back the timeline can browse. Default 90 days. */
historyWindowDays?: number;
/** The dashboard fills its parent — size it there (e.g. "h-[720px]"). */
className?: string;
};
/** The portal's status classes, stacked in the timeline. */
const DELIVERY_SERIES: TimelineSeries[] = [
{ key: "2xx", label: "2xx", color: "var(--status-success)", statuses: ["2xx"] },
{ key: "3xx", label: "3xx", color: "var(--status-info)", statuses: ["3xx"] },
{ key: "4xx", label: "4xx", color: "var(--status-warning)", statuses: ["4xx"] },
{ key: "5xx", label: "5xx", color: "var(--status-error)", statuses: ["5xx"] },
];
/**
* The full webhook dashboard, wired the way the Repost portal wires it:
* headline metrics, the timeline histogram with its drag-to-select scrubber,
* the live virtualized delivery history, and per-delivery attempt detail.
* All data flows through @repost/portal-react hooks at the top of this file
* and into purely presentational components as props — swap any part freely.
*/
export function WebhookDashboard({ getToken, endpointId, historyWindowDays = 90, client, className }: WebhookDashboardProps) {
return (
<RepostPortalProvider {...(client ? { client } : { getToken: getToken! })}>
<WebhookDashboardContent endpointId={endpointId} historyWindowDays={historyWindowDays} className={className} />
</RepostPortalProvider>
);
}
/** `timestamp:[a TO b]` — the query clause the portal API filters logs with. */
const timestampClause = (range: TimelineRange): string => `timestamp:[${range.from.toISOString()} TO ${range.to.toISOString()}]`;
const inferIntervalSecs = (slots: TimelineSlot[], dateFrom: Date, dateTo: Date): number => {
if (slots.length > 1) {
return Math.max(1, Math.round((slots[1]!.timestamp.getTime() - slots[0]!.timestamp.getTime()) / 1000));
}
return Math.max(1, Math.round((dateTo.getTime() - dateFrom.getTime()) / 1000));
};
function WebhookDashboardContent({
endpointId,
historyWindowDays,
className,
}: {
endpointId?: string;
historyWindowDays: number;
className?: string;
}) {
// ── Timeline state: the pinned range (null = live window) and the zoom ────
const [selection, setSelection] = useState<TimelineRange | null>(null);
const [liveDateWindow, setLiveDateWindow] = useState<LiveDateWindow>(DEFAULT_LIVE_DATE_WINDOW);
const [live, setLive] = useState(true);
const {
scale,
defaultRange,
options,
activeOptionKey,
selectSpan,
frameOptions,
activeFrameKey,
selectFrame,
jump,
canJumpBack,
canJumpForward,
} = useTimelineScale({ selection, liveDateWindow, planWindowDays: historyWindowDays });
const visibleSelection = selection ?? defaultRange;
// ── Data: histogram over the visible frame, feed over the pinned range ────
const context = usePortalContext();
const histogram = useLogsHistogram({ endpointId, dateFrom: scale.from, dateTo: scale.to });
const feedQuery = selection ? timestampClause(selection) : undefined;
const feed = useLogsFeed({ endpointId, query: feedQuery, live });
const [selectedRow, setSelectedRow] = useState<WebhookLogRow | null>(null);
const attempts = useDeliveryAttempts(selectedRow?.deliveryId ?? null);
const replay = useReplayDelivery();
const slots = useMemo<TimelineSlot[]>(
() =>
(histogram.data?.slots ?? []).map((slot) => ({
timestamp: new Date(slot.timestamp),
total: slot.total,
counts: slot.counts,
})),
[histogram.data],
);
const intervalSecs = inferIntervalSecs(slots, scale.from, scale.to);
// ── Handlers ──────────────────────────────────────────────────────────────
const handleApplyRange = useCallback((range: TimelineRange) => setSelection(range), []);
const handleSelectSpan = useCallback(
(option: (typeof options)[number]) => {
selectSpan(option);
setLiveDateWindow(option.key);
setSelection(null);
},
[selectSpan],
);
const handleDateRangeChange = useCallback((range: TimelineDateRange | undefined) => {
setSelection(range?.from && range.to ? { from: range.from, to: range.to } : null);
}, []);
const handleRowClick = (row: WebhookLogRow) => {
replay.reset();
setSelectedRow((current) => (current && deliveryHistoryRowId(current) === deliveryHistoryRowId(row) ? null : row));
};
const dateRange = useMemo(
() => ({
value: { from: visibleSelection.from, to: visibleSelection.to },
onChange: handleDateRangeChange,
isUserRange: selection !== null,
endLabel: selection === null ? "now" : undefined,
}),
[handleDateRangeChange, selection, visibleSelection.from, visibleSelection.to],
);
return (
<div className={cn("flex h-full min-h-0 flex-col gap-4", className)}>
<header className="flex items-center gap-2">
<h2 className="text-lg font-semibold">{context.data ? `${context.data.appName} webhooks` : "Webhooks"}</h2>
<span className="ml-auto inline-flex items-center gap-1.5 text-xs text-muted-foreground">
<StatusIndicator color={feed.status === "live" ? "emerald" : "amber"} pulse={feed.status === "live"} />
{feed.status === "live" ? "Live" : feed.status === "off" ? "Paused" : feed.status}
</span>
</header>
<WebhookMetricsCards slots={histogram.data?.slots ?? []} isLoading={histogram.isLoading} />
<TimelineHistogram
slots={histogram.isLoading && slots.length === 0 ? undefined : slots}
intervalSecs={intervalSecs}
dateFrom={scale.from}
dateTo={scale.to}
series={DELIVERY_SERIES}
isLoading={histogram.isLoading}
selection={visibleSelection}
onApplyRange={handleApplyRange}
scaleOptions={options}
activeOptionKey={activeOptionKey}
onSelectSpan={handleSelectSpan}
frameOptions={frameOptions}
activeFrameKey={activeFrameKey}
onSelectFrame={selectFrame}
onJump={jump}
canJumpBack={canJumpBack}
canJumpForward={canJumpForward}
emptyMessage="No deliveries in range"
dateRange={dateRange}
/>
<DeliveryHistoryTable
className="min-h-0 flex-1"
rows={feed.rows}
isLoading={feed.isLoading}
onRowClick={handleRowClick}
selectedRowId={selectedRow ? deliveryHistoryRowId(selectedRow) : null}
onLoadOlder={feed.hasMore ? feed.loadOlder : undefined}
isLoadingOlder={feed.isLoadingMore}
streaming={live && feed.status !== "off"}
streamStatus={feed.status}
streamDroppedCount={feed.droppedCount}
onToggleStream={() => setLive((value) => !value)}
onClearStreamDropped={feed.refresh}
/>
{/* The portal's golden drawer: clicking a row opens the delivery
aggregate — state header, attempt trail, request/response
inspectors — in the non-modal right-hand sheet. */}
<RowDetailSheet
open={selectedRow !== null}
onClose={() => {
setSelectedRow(null);
replay.reset();
}}
title="Delivery details"
description="Delivery attempt detail view"
>
{selectedRow && (
<DeliveryDetail
deliveryId={selectedRow.deliveryId}
initialAttempt={{ generation: selectedRow.generation, attempt: selectedRow.attempt }}
delivery={attempts.data?.delivery ?? null}
attempts={attempts.data?.attempts ?? []}
isLoading={attempts.isLoading}
isError={attempts.isError}
onReplay={(delivery) => replay.mutate({ deliveryId: delivery.id })}
replayPending={replay.isPending}
replayQueued={replay.data !== undefined}
replayError={replay.error?.message ?? null}
/>
)}
</RowDetailSheet>
</div>
);
}
Headline metrics, the portal's timeline histogram with drag-to-select scrubber, live virtualized delivery history, and attempt detail.
webhook-dashboard

