Skip to content
File

Blob: src/client/components/webhooks/webhook-row.tsx

typescript130 lines
1import type { WebhookSummary } from "@/contracts";
2import { useState, useEffect, useRef } from "react";
3import { History, MoreVertical, Power, RotateCw, Trash2 } from "lucide-react";
4import { Badge, Button } from "@/client/components/ui";
5import { getWebhookProviderIcon } from "@/client/components/webhooks/webhook-presentation";
6import { webhookProviderCatalog } from "@/lib/webhooks";
7 
8interface WebhookRowProps {
9 webhook: WebhookSummary;
10 onToggle: () => void;
11 onViewDeliveries: () => void;
12 onRotateSecret: () => void;
13 onDelete: () => void;
14 toggling: boolean;
15}
16 
17export const WebhookRow = ({
18 webhook,
19 onToggle,
20 onViewDeliveries,
21 onRotateSecret,
22 onDelete,
23 toggling,
24}: WebhookRowProps) => {
25 const [menuOpen, setMenuOpen] = useState(false);
26 const menuRef = useRef<HTMLDivElement>(null);
27 
28 useEffect(() => {
29 if (!menuOpen) return;
30 
31 const handleClick = (e: MouseEvent) => {
32 if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
33 setMenuOpen(false);
34 }
35 };
36 
37 document.addEventListener("click", handleClick);
38 return () => document.removeEventListener("click", handleClick);
39 }, [menuOpen]);
40 
41 useEffect(() => {
42 if (!menuOpen) return;
43 
44 const handleEscape = (event: KeyboardEvent) => {
45 if (event.key === "Escape") {
46 setMenuOpen(false);
47 }
48 };
49 
50 document.addEventListener("keydown", handleEscape);
51 return () => document.removeEventListener("keydown", handleEscape);
52 }, [menuOpen]);
53 
54 return (
55 <div className="flex flex-wrap items-center gap-2 rounded-xl border border-zinc-800/60 bg-zinc-900/30 px-3 py-2">
56 {/* Left */}
57 <div className="flex min-w-0 flex-1 items-center gap-2">
58 {getWebhookProviderIcon(webhook.provider)}
59 <span className="text-sm font-medium text-zinc-200">
60 {webhookProviderCatalog[webhook.provider].displayName}
61 </span>
62 <Badge variant={webhook.enabled ? "success" : "default"}>{webhook.enabled ? "Enabled" : "Disabled"}</Badge>
63 </div>
64
65 {/* Right */}
66 <div className="flex items-center gap-1">
67 <Button
68 variant="ghost"
69 size="sm"
70 disabled={toggling}
71 onClick={onToggle}
72 aria-label={webhook.enabled ? "Disable webhook" : "Enable webhook"}
73 >
74 <Power className="h-4 w-4" />
75 </Button>
76
77 <Button variant="ghost" size="sm" onClick={onViewDeliveries} aria-label="View deliveries">
78 <History className="h-4 w-4" />
79 {webhook.recentDeliveries.length > 0 && <span>{webhook.recentDeliveries.length}</span>}
80 </Button>
81
82 <div ref={menuRef} className="relative">
83 <Button
84 variant="ghost"
85 size="sm"
86 onClick={() => setMenuOpen((v) => !v)}
87 aria-expanded={menuOpen}
88 aria-haspopup="true"
89 aria-label="Webhook actions"
90 >
91 <MoreVertical className="h-4 w-4" />
92 </Button>
93
94 {menuOpen && (
95 <div
96 role="menu"
97 className="absolute right-0 top-full z-10 mt-1 min-w-max rounded-xl border border-zinc-700/60 bg-zinc-800 p-1 shadow-xl"
98 >
99 <button
100 type="button"
101 role="menuitem"
102 className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-700/50"
103 onClick={() => {
104 setMenuOpen(false);
105 onRotateSecret();
106 }}
107 >
108 <RotateCw className="h-4 w-4" />
109 Rotate secret
110 </button>
111 <button
112 type="button"
113 role="menuitem"
114 className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-red-400 hover:text-red-300 hover:bg-zinc-700/50"
115 onClick={() => {
116 setMenuOpen(false);
117 onDelete();
118 }}
119 >
120 <Trash2 className="h-4 w-4" />
121 Delete webhook
122 </button>
123 </div>
124 )}
125 </div>
126 </div>
127 </div>
128 );
129};