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,
useCreateEndpoint,
useDeleteEndpoint,
useDlqCountByEndpoint,
useEndpoints,
usePauseEndpoint,
useResumeEndpoint,
useRevealEndpointSecret,
useUpdateEndpoint,
} from "@repost/portal-react";
import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { EndpointForm, type EndpointFormValues } from "@/components/endpoint-form";
import { EndpointsList, type EndpointListItem } from "@/components/endpoints-list";
export type EndpointManagementProps = {
/** Return a portal access token minted by your backend. */
getToken?: () => Promise<string>;
/**
* A pre-built client, instead of `getToken` — used by previews and tests to
* run the block against fixtures.
*/
client?: PortalClient;
};
/**
* Endpoint management, wired: list with status + DLQ badges, create/edit
* form, pause/resume/delete, and secret reveal. Hooks at the top, props
* down — every presentational piece is swappable.
*/
export function EndpointManagement({ getToken, client }: EndpointManagementProps) {
return (
<RepostPortalProvider {...(client ? { client } : { getToken: getToken! })}>
<EndpointManagementContent />
</RepostPortalProvider>
);
}
type PanelState = { mode: "closed" } | { mode: "create" } | { mode: "edit"; endpoint: EndpointListItem };
function EndpointManagementContent() {
const [panel, setPanel] = useState<PanelState>({ mode: "closed" });
const [revealedSecret, setRevealedSecret] = useState<string | null>(null);
const endpoints = useEndpoints();
const dlqCounts = useDlqCountByEndpoint();
const create = useCreateEndpoint();
const update = useUpdateEndpoint();
const remove = useDeleteEndpoint();
const pause = usePauseEndpoint();
const resume = useResumeEndpoint();
const reveal = useRevealEndpointSecret();
const rows = useMemo<EndpointListItem[]>(() => {
const countByEndpoint = new Map((dlqCounts.data ?? []).map((entry) => [entry.endpointId, entry.count]));
return (endpoints.data ?? []).map((endpoint) => ({
id: endpoint.id,
url: endpoint.url,
description: endpoint.description ?? null,
subscriptions: endpoint.subscriptions,
status: endpoint.status,
dlqCount: countByEndpoint.get(endpoint.id) ?? 0,
}));
}, [endpoints.data, dlqCounts.data]);
const busyEndpointId = pause.isPending || resume.isPending || remove.isPending || reveal.isPending ? "*" : null;
const submitForm = (values: EndpointFormValues) => {
if (panel.mode === "edit") {
update.mutate({ endpointId: panel.endpoint.id, ...values });
} else {
create.mutate(values);
}
setPanel({ mode: "closed" });
};
return (
<div className="flex flex-col gap-4">
<header className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Endpoints</h2>
<Button size="sm" onClick={() => setPanel(panel.mode === "create" ? { mode: "closed" } : { mode: "create" })}>
{panel.mode === "create" ? "Close" : "Add endpoint"}
</Button>
</header>
{panel.mode !== "closed" && (
<div className="rounded-md border border-border bg-card p-4">
<EndpointForm
key={panel.mode === "edit" ? panel.endpoint.id : "create"}
initialValues={panel.mode === "edit" ? { ...panel.endpoint, description: panel.endpoint.description ?? undefined } : undefined}
onSubmit={submitForm}
isSubmitting={create.isPending || update.isPending}
error={create.error?.message ?? update.error?.message ?? null}
submitLabel={panel.mode === "edit" ? "Save changes" : "Create endpoint"}
/>
</div>
)}
{create.data?.secret && (
<SecretNotice label="Endpoint created. Signing secret (shown once):" secret={create.data.secret} onDismiss={() => create.reset()} />
)}
{revealedSecret && <SecretNotice label="Active signing secret:" secret={revealedSecret} onDismiss={() => setRevealedSecret(null)} />}
<EndpointsList
endpoints={rows}
isLoading={endpoints.isLoading}
busyEndpointId={busyEndpointId}
onEdit={(endpoint) => setPanel({ mode: "edit", endpoint })}
onPause={(endpoint) => pause.mutate({ endpointId: endpoint.id })}
onResume={(endpoint) => resume.mutate({ endpointId: endpoint.id })}
onDelete={(endpoint) => remove.mutate({ endpointId: endpoint.id })}
onRevealSecret={(endpoint) =>
reveal.mutateAsync({ endpointId: endpoint.id }).then((secrets) => {
const active = secrets.versions.find((version) => version.active) ?? secrets.versions[0];
setRevealedSecret(active?.whsec ?? null);
})
}
/>
</div>
);
}
function SecretNotice({ label, secret, onDismiss }: { label: string; secret: string; onDismiss: () => void }) {
return (
<div className="flex items-center gap-2 rounded-md border border-status-warning/40 bg-status-warning-muted p-3 text-sm">
<span className="shrink-0 text-muted-foreground">{label}</span>
<code className="min-w-0 flex-1 truncate font-mono">{secret}</code>
<Button variant="ghost" size="sm" onClick={onDismiss}>
Dismiss
</Button>
</div>
);
}
Endpoint list, create/edit form, pause, resume, and secrets.
endpoint-management

