Blocks

Webhooks Manager

A four-state webhook endpoint manager — event multi-select, reveal-and-rotate signing secrets with the overlap window stated, test sends that explain when they are blocked, and per-endpoint deliveries with retry.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import {
  Ban,
  Check,
  ChevronRight,
  CircleAlert,
  CircleCheck,
  Clock,
  Copy,
  CornerUpRight,
  Eye,
  EyeOff,

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/webhooks-manager.json

Prompt

Build a React + TypeScript + Tailwind "WebhooksManager" block (lucide-react,
zod, and the shadcn Button / Input / Badge / Dialog / AlertDialog primitives).
It manages the endpoints themselves — not a read-only delivery log.

Contract
- One zod schema is the single source of truth:
  { status: "loading" | "empty" | "error" | "ready";
    now: ISO instant (injected — never call new Date() in render);
    endpoints: { id, url, events: string[], status: "active" | "paused" |
      "failing" | "disabled", secretMasked, lastDelivery?: { at, code|null },
      previousSecretExpiresAt?, consecutiveFailures?, disabledAt? }[];
    availableEvents: { id, description? }[];
    deliveries: { id, endpointId, event, code|null, ms, at, attempt }[];
    secretOverlapHours: number;          // your policy, stated as data
    autoDisableAfterFailures?: number }  // omit and the panel claims no limit
- Props = z.infer of that schema plus onSave / onRevealSecret / onRotateSecret /
  onSendTest / onRetryDelivery / onSetStatus / onRetry, all optional: an
  affordance whose callback is missing is not rendered at all.
- Export the URL checker from the contract so the schema and the editor agree:
  it rejects non-https, embedded credentials, "#fragment", and hosts that can
  never be reached from the internet (localhost, .local/.internal/.home.arpa,
  ::1, fc00::/7, fe80::/10, and the IPv4 reserved ranges 0/8, 10/8,
  100.64/10, 127/8, 169.254/16, 172.16/12, 192.168/16) — each with the reason
  in words. IPv4-mapped IPv6 goes through the IPv4 rules rather than past them:
  new URL turns [::ffff:127.0.0.1] into [::ffff:7f00:1], so unwrap the ::ffff:
  prefix in both the dotted and the hex-pair spelling before judging the host.
  On success it returns the normalised URL.

Behavior
- Four first-class branches: loading (skeleton rows + sr-only status), empty
  (explains the model and keeps Add endpoint working — its prose names an
  action only when the matching callback exists, so a read-only card is not
  told to add anything), error (nothing was changed, optional Try again, adding
  withheld because the visible list is not the truth), ready (the table).
- The table carries URL, subscribed events, status and last delivery. Below the
  container's @lg width the last three columns are dropped and their facts move
  into the URL cell, so a row is self-sufficient at ~420px. "Manage" expands one
  row at a time (aria-expanded / aria-controls).
- Expanded row: Edit, Pause / Resume / Re-enable, the signing secret, the test
  sender, and that endpoint's recent deliveries.
- Signing secret: masked from the data; Reveal fetches the full value through a
  callback and keeps it in state only while the row is open — collapsing drops
  it. Copy falls back to selecting the text when the Clipboard API refuses.
  A copy failure is tied to the value it describes, so hiding or rotating the
  secret takes the message with it instead of leaving "the secret is selected"
  over a masked box.
  Rotate goes through a confirmation that states the consequence first: the
  current secret keeps verifying for secretOverlapHours more hours and then
  stops. While an overlap is open, show the server's exact expiry instant plus
  how long is left.
- Every blocked action explains its blocker instead of greying out silently: no
  events selected, endpoint paused, endpoint auto-disabled. An auto-disabled
  endpoint states when it was stopped, after how many consecutive failures, and
  the limit — only when the data carries one.
- Deliveries: outcome classes derived from the code, with null (timeout /
  refused / TLS) kept separate from any status code. The mix line uses
  largest-remainder apportionment so the shares total exactly 100. Retry is
  offered on failed attempts only, and its confirmation says the endpoint will
  receive a second copy — plus whether repeating is worth it at all (a 4xx that
  is not 408/429 will be refused identically).
- Editor dialog: URL field plus an event multi-select grouped by the id prefix
  with per-group select-all and a live "N of M selected" count. Events the
  endpoint is subscribed to but the catalogue no longer offers get their own
  group, so an edit cannot silently unsubscribe them. Refusals are inline and
  named; focus returns to the trigger on close.

Rendering & styling
- Semantic tokens only: bg-card, bg-muted/40, text-muted-foreground, border,
  primary for success, destructive for failure. Colour is never the only
  channel — every state also carries an icon and a word.
- @container, not viewport breakpoints: this panel is a full-width settings page
  in one product and a 420px drawer in another.
- cn() merges className; the root spreads the remaining props and forwards ref.
- Spinners and the skeleton pulse carry motion-reduce:animate-none; the chevron
  rotation carries motion-reduce:transition-none. Async work is guarded by a ref
  so a double click cannot send two test events, and a mounted flag stops a late
  resolution from setting state after unmount. That guard is global — one action
  at a time — so while one is in flight the row's other action buttons are
  disabled instead of swallowing the click, and a rejection is reported under
  the endpoint whose action raised it, never inherited by the next row opened.

Customization levers
- Column set: the three @lg-only columns are independent — drop "Events" or add
  a description column without touching the expanded panel.
- Density: CELL/HEAD_CELL padding constants control the whole table; switch the
  single-open accordion to a Set of open ids for multi-open.
- Status vocabulary: STATUS_META maps status → icon + label + token classes;
  rename "Auto-disabled" or add a "pending verification" state there only.
- Outcome classes: OUTCOME_META and classifyCode decide the delivery taxonomy —
  split 2xx into 200/202, or fold redirects into rejected, in one place.
- Secret handling: pass no onRevealSecret for a panel that only ever shows the
  mask; drop onRotateSecret for products where rotation is CLI-only.
- Depth: the expanded panel's four sections are siblings — remove the test
  sender or the deliveries list without changing the table.

Concepts

  • Reveal on demand, not on load — the masked fingerprint lives in the data; the full signing secret is fetched by a callback and held only while its row is open, so it never sits in a payload, a cache or a collapsed DOM node waiting to be inspected.
  • Rotation overlap — a signing secret cannot be swapped atomically across a fleet, so both secrets verify for a stated window. The window is data (secretOverlapHours), the deadline is the server's instant, and the confirmation says what breaks after it rather than promising anything the component cannot know.
  • Blocked action explains itself — a test send that cannot happen names the reason (no events selected, paused, auto-disabled) and points at the fix. A greyed-out button with no explanation is the same dead end as no button at all. The one moment a button does grey out is while another action in the same row is in flight — the spinner on that one is the explanation, and disabling the rest beats dropping their clicks in silence.
  • Retry worth — an outcome class decides whether repeating the identical request can end differently: 5xx, timeout and 408/429 can, a plain 4xx cannot. The confirmation says which one this is before it sends a second copy.
  • Largest remainder — the outcome mix under a delivery list is apportioned with the Hare–Niemeyer method, so three equal thirds read 34/33/33 and not 33/33/33; a percentage row that fails to total 100 quietly destroys trust in every other number on the card.
  • Auto-disable threshold — the failing and disabled states are the provider's verdict, not the owner's, so they get their own words, their own counts, and a re-enable path that warns the same failure will stop it again.

On This Page