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
components/endpoint-management.tsx
"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
endpoint-management