chore: remove src/pages/[owner]/[repo]/settings/webhooks.astro
swadhinbiswas committed 8 months ago
2f1c4d0
1 parent d3cf800
commit 2f1c4d0
1 file changed
+0 -449
Showing 1 changed file+0-447
▼src/pages/[owner]/[repo]/settings/webhooks.astro
+0-447
| @@ -1,449 +0,0 @@ | ||
1 | -import BaseLayout from "@/layouts/BaseLayout.astro"; | |
2 | -import { Button } from "@/components/ui/button"; | |
3 | -import { Input } from "@/components/ui/input"; | |
4 | -import { Label } from "@/components/ui/label"; | |
5 | -import { | |
6 | - Card, | |
7 | - CardContent, | |
8 | - CardDescription, | |
9 | - CardHeader, | |
10 | - CardTitle, | |
11 | -} from "@/components/ui/card"; | |
12 | -import { getDatabase, schema } from "@/db"; | |
13 | -import { eq, and } from "drizzle-orm"; | |
14 | -import { canAdminRepo } from "@/lib/permissions"; | |
15 | -import RepoHeader from "@/components/repo/RepoHeader.astro"; | |
16 | -import { | |
17 | - Webhook, | |
18 | - Plus, | |
19 | - Trash2, | |
20 | - Check, | |
21 | - X, | |
22 | - RefreshCw, | |
23 | - Users, | |
24 | -} from "lucide-react"; | |
25 | - | |
26 | -const { owner: ownerName, repo: repoName } = Astro.params; | |
27 | -const db = getDatabase(); | |
28 | - | |
29 | -// Fetch Repo & Owner | |
30 | -const user = await db.query.users.findFirst({ | |
31 | - where: eq(schema.users.username, ownerName!), | |
32 | -}); | |
33 | - | |
34 | -if (!user) return Astro.redirect("/404"); | |
35 | - | |
36 | -const repoData = await db.query.repositories.findFirst({ | |
37 | - where: and( | |
38 | - eq(schema.repositories.ownerId, user.id), | |
39 | - eq(schema.repositories.name, repoName!), | |
40 | - ), | |
41 | - with: { | |
42 | - owner: true, | |
43 | - }, | |
44 | -}); | |
45 | - | |
46 | -if (!repoData) return Astro.redirect("/404"); | |
47 | - | |
48 | -// Check permissions | |
49 | -const currentUser = Astro.locals.user; | |
50 | -const hasAccess = await canAdminRepo(currentUser?.id, repoData); | |
51 | - | |
52 | -if (!hasAccess) { | |
53 | - return Astro.redirect(`/${ownerName}/${repoName}`); | |
54 | -} | |
55 | - | |
56 | -// Fetch webhooks | |
57 | -const webhooks = await db.query.webhooks.findMany({ | |
58 | - where: eq(schema.webhooks.repositoryId, repoData.id), | |
59 | -}); | |
60 | - | |
61 | -const repo = { | |
62 | - owner: repoData.owner.username, | |
63 | - name: repoData.name, | |
64 | - description: repoData.description, | |
65 | - visibility: repoData.visibility, | |
66 | - defaultBranch: repoData.defaultBranch, | |
67 | - stars: repoData.starCount, | |
68 | - forks: repoData.forkCount, | |
69 | - watchers: repoData.watchCount, | |
70 | - language: repoData.language || "Unknown", | |
71 | - openIssueCount: repoData.openIssueCount, | |
72 | - openPrCount: repoData.openPrCount, | |
73 | - hasIssues: repoData.hasIssues, | |
74 | - hasWiki: repoData.hasWiki, | |
75 | - hasActions: repoData.hasActions, | |
76 | -}; | |
77 | - | |
78 | -const availableEvents = [ | |
79 | - { value: "push", label: "Push", description: "Branch or tag pushed" }, | |
80 | - { | |
81 | - value: "pull_request", | |
82 | - label: "Pull Request", | |
83 | - description: "PR opened, closed, merged", | |
84 | - }, | |
85 | - { | |
86 | - value: "issues", | |
87 | - label: "Issues", | |
88 | - description: "Issue opened, closed, etc.", | |
89 | - }, | |
90 | - { | |
91 | - value: "issue_comment", | |
92 | - label: "Issue Comment", | |
93 | - description: "Comment on issue or PR", | |
94 | - }, | |
95 | - { value: "release", label: "Release", description: "Release published" }, | |
96 | - { value: "star", label: "Star", description: "Repository starred" }, | |
97 | - { value: "fork", label: "Fork", description: "Repository forked" }, | |
98 | -]; | |
99 | - | |
100 | -<BaseLayout title={`Webhooks · ${repo.owner}/${repo.name}`}> | |
101 | - <div class="container py-6"> | |
102 | - <RepoHeader repo={repo} activeTab="settings" /> | |
103 | - | |
104 | - <div class="grid gap-6 md:grid-cols-[240px_1fr]"> | |
105 | - <!-- Settings Sidebar --> | |
106 | - <nav class="space-y-1"> | |
107 | - <a | |
108 | - href={`/${repo.owner}/${repo.name}/settings`} | |
109 | - class="flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground" | |
110 | - > | |
111 | - General | |
112 | - </a> | |
113 | - <a | |
114 | - href={`/${repo.owner}/${repo.name}/settings/collaborators`} | |
115 | - class="flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground" | |
116 | - > | |
117 | - <Users className="h-4 w-4" /> | |
118 | - Collaborators | |
119 | - </a> | |
120 | - <a | |
121 | - href={`/${repo.owner}/${repo.name}/settings/webhooks`} | |
122 | - class="flex items-center gap-2 rounded-md bg-muted px-3 py-2 text-sm font-medium text-foreground" | |
123 | - > | |
124 | - <Webhook className="h-4 w-4" /> | |
125 | - Webhooks | |
126 | - </a> | |
127 | - <a | |
128 | - href={`/${repo.owner}/${repo.name}/settings/external-ci`} | |
129 | - class="flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground" | |
130 | - > | |
131 | - External CI | |
132 | - </a> | |
133 | - <a | |
134 | - href={`/${repo.owner}/${repo.name}/settings/reviews`} | |
135 | - class="flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground" | |
136 | - > | |
137 | - Reviews | |
138 | - </a> | |
139 | - <a | |
140 | - href={`/${repo.owner}/${repo.name}/settings/actions/runners`} | |
141 | - class="flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground" | |
142 | - > | |
143 | - Actions | |
144 | - </a> | |
145 | - </nav> | |
146 | - | |
147 | - <!-- Webhooks Content --> | |
148 | - <div class="space-y-6"> | |
149 | - <!-- Add Webhook --> | |
150 | - <Card> | |
151 | - <CardHeader> | |
152 | - <CardTitle className="flex items-center gap-2"> | |
153 | - <Plus className="h-5 w-5" /> | |
154 | - Add Webhook | |
155 | - </CardTitle> | |
156 | - <CardDescription> | |
157 | - Webhooks allow external services to be notified when events | |
158 | - happen. | |
159 | - </CardDescription> | |
160 | - </CardHeader> | |
161 | - <CardContent> | |
162 | - <form id="add-webhook-form" class="space-y-4"> | |
163 | - <div class="space-y-2"> | |
164 | - <Label htmlFor="url">Payload URL</Label> | |
165 | - <Input | |
166 | - id="url" | |
167 | - name="url" | |
168 | - type="url" | |
169 | - placeholder="https://example.com/webhook" | |
170 | - required | |
171 | - /> | |
172 | - </div> | |
173 | - <div class="space-y-2"> | |
174 | - <Label htmlFor="secret">Secret (optional)</Label> | |
175 | - <Input | |
176 | - id="secret" | |
177 | - name="secret" | |
178 | - type="password" | |
179 | - placeholder="Used to sign payloads" | |
180 | - /> | |
181 | - </div> | |
182 | - <div class="space-y-2"> | |
183 | - <Label>Events</Label> | |
184 | - <div class="grid gap-2 sm:grid-cols-2"> | |
185 | - { | |
186 | - availableEvents.map((event) => ( | |
187 | - <label class="flex items-start gap-2 p-2 rounded border hover:bg-muted cursor-pointer"> | |
188 | - <input | |
189 | - type="checkbox" | |
190 | - name="events" | |
191 | - value={event.value} | |
192 | - class="mt-0.5 accent-primary" | |
193 | - checked={event.value === "push"} | |
194 | - /> | |
195 | - <div> | |
196 | - <div class="font-medium text-sm">{event.label}</div> | |
197 | - <div class="text-xs text-muted-foreground"> | |
198 | - {event.description} | |
199 | - </div> | |
200 | - </div> | |
201 | - </label> | |
202 | - )) | |
203 | - } | |
204 | - </div> | |
205 | - </div> | |
206 | - <Button type="submit"> | |
207 | - <Plus className="h-4 w-4 mr-2" /> | |
208 | - Add Webhook | |
209 | - </Button> | |
210 | - </form> | |
211 | - <p id="add-error" class="text-destructive text-sm mt-2 hidden"></p> | |
212 | - <p id="add-success" class="text-green-600 text-sm mt-2 hidden"></p> | |
213 | - </CardContent> | |
214 | - </Card> | |
215 | - | |
216 | - <!-- Current Webhooks --> | |
217 | - <Card> | |
218 | - <CardHeader> | |
219 | - <CardTitle className="flex items-center gap-2"> | |
220 | - <Webhook className="h-5 w-5" /> | |
221 | - Active Webhooks | |
222 | - </CardTitle> | |
223 | - </CardHeader> | |
224 | - <CardContent> | |
225 | - { | |
226 | - webhooks.length === 0 ? ( | |
227 | - <div class="text-center py-8 text-muted-foreground"> | |
228 | - <Webhook className="h-12 w-12 mx-auto mb-4 opacity-50" /> | |
229 | - <p>No webhooks configured</p> | |
230 | - <p class="text-sm">Add a webhook to get started!</p> | |
231 | - </div> | |
232 | - ) : ( | |
233 | - <div class="space-y-4"> | |
234 | - {webhooks.map((webhook) => { | |
235 | - const events = Array.isArray(webhook.events) | |
236 | - ? webhook.events | |
237 | - : typeof webhook.events === "string" | |
238 | - ? JSON.parse(webhook.events || "[]") | |
239 | - : []; | |
240 | - | |
241 | - const status = parseInt(webhook.lastDeliveryStatus || "0"); | |
242 | - | |
243 | - return ( | |
244 | - <div | |
245 | - class="p-4 rounded-lg border" | |
246 | - data-webhook-id={webhook.id} | |
247 | - > | |
248 | - <div class="flex items-center justify-between"> | |
249 | - <div class="flex-1 min-w-0"> | |
250 | - <div class="flex items-center gap-2"> | |
251 | - {webhook.active ? ( | |
252 | - <span | |
253 | - class="h-2 w-2 rounded-full bg-green-500" | |
254 | - title="Active" | |
255 | - /> | |
256 | - ) : ( | |
257 | - <span | |
258 | - class="h-2 w-2 rounded-full bg-muted-foreground" | |
259 | - title="Inactive" | |
260 | - /> | |
261 | - )} | |
262 | - <span class="font-medium truncate"> | |
263 | - {webhook.url} | |
264 | - </span> | |
265 | - </div> | |
266 | - <div class="flex flex-wrap gap-1 mt-2"> | |
267 | - {events.map((event: string) => ( | |
268 | - <span class="text-xs bg-muted px-2 py-0.5 rounded"> | |
269 | - {event} | |
270 | - </span> | |
271 | - ))} | |
272 | - </div> | |
273 | - {webhook.lastDeliveryAt && ( | |
274 | - <div class="flex items-center gap-2 mt-2 text-xs text-muted-foreground"> | |
275 | - <span> | |
276 | - Last delivery:{" "} | |
277 | - {new Date( | |
278 | - webhook.lastDeliveryAt, | |
279 | - ).toLocaleString()} | |
280 | - </span> | |
281 | - {webhook.lastDeliveryStatus && | |
282 | - (status >= 200 && status < 300 ? ( | |
283 | - <span class="text-green-600"> | |
284 | - ✓ {status} | |
285 | - </span> | |
286 | - ) : ( | |
287 | - <span class="text-destructive"> | |
288 | - ✗ {status} | |
289 | - </span> | |
290 | - ))} | |
291 | - </div> | |
292 | - )} | |
293 | - </div> | |
294 | - <div class="flex items-center gap-2"> | |
295 | - <button | |
296 | - class="toggle-webhook-btn p-2 hover:bg-muted rounded" | |
297 | - data-webhook-id={webhook.id} | |
298 | - data-active={webhook.active} | |
299 | - title={webhook.active ? "Disable" : "Enable"} | |
300 | - > | |
301 | - {webhook.active ? ( | |
302 | - <X className="h-4 w-4" /> | |
303 | - ) : ( | |
304 | - <Check className="h-4 w-4" /> | |
305 | - )} | |
306 | - </button> | |
307 | - <button | |
308 | - class="test-webhook-btn p-2 hover:bg-muted rounded" | |
309 | - data-webhook-id={webhook.id} | |
310 | - title="Test webhook" | |
311 | - > | |
312 | - <RefreshCw className="h-4 w-4" /> | |
313 | - </button> | |
314 | - <button | |
315 | - class="delete-webhook-btn p-2 hover:bg-destructive/10 rounded text-destructive" | |
316 | - data-webhook-id={webhook.id} | |
317 | - title="Delete webhook" | |
318 | - > | |
319 | - <Trash2 className="h-4 w-4" /> | |
320 | - </button> | |
321 | - </div> | |
322 | - </div> | |
323 | - </div> | |
324 | - ); | |
325 | - })} | |
326 | - </div> | |
327 | - ) | |
328 | - } | |
329 | - </CardContent> | |
330 | - </Card> | |
331 | - </div> | |
332 | - </div> | |
333 | - </div> | |
334 | -</BaseLayout> | |
335 | - | |
336 | -<script define:vars={{ owner: repo.owner, repo: repo.name }}> | |
337 | - // Add webhook | |
338 | - const form = document.getElementById("add-webhook-form"); | |
339 | - const errorEl = document.getElementById("add-error"); | |
340 | - const successEl = document.getElementById("add-success"); | |
341 | - | |
342 | - form?.addEventListener("submit", async (e) => { | |
343 | - e.preventDefault(); | |
344 | - errorEl.classList.add("hidden"); | |
345 | - successEl.classList.add("hidden"); | |
346 | - | |
347 | - const formData = new FormData(form); | |
348 | - const url = formData.get("url"); | |
349 | - const secret = formData.get("secret"); | |
350 | - const events = formData.getAll("events"); | |
351 | - | |
352 | - if (events.length === 0) { | |
353 | - errorEl.textContent = "Select at least one event"; | |
354 | - errorEl.classList.remove("hidden"); | |
355 | - return; | |
356 | - } | |
357 | - | |
358 | - try { | |
359 | - const response = await fetch(`/api/repos/${owner}/${repo}/webhooks`, { | |
360 | - method: "POST", | |
361 | - headers: { "Content-Type": "application/json" }, | |
362 | - body: JSON.stringify({ url, secret, events }), | |
363 | - }); | |
364 | - | |
365 | - const data = await response.json(); | |
366 | - | |
367 | - if (!response.ok) { | |
368 | - throw new Error(data.message || data.error || "Failed to add webhook"); | |
369 | - } | |
370 | - | |
371 | - successEl.textContent = "Webhook added!"; | |
372 | - successEl.classList.remove("hidden"); | |
373 | - form.reset(); | |
374 | - setTimeout(() => window.location.reload(), 1000); | |
375 | - } catch (error) { | |
376 | - errorEl.textContent = error.message; | |
377 | - errorEl.classList.remove("hidden"); | |
378 | - } | |
379 | - }); | |
380 | - | |
381 | - // Toggle webhook | |
382 | - document.querySelectorAll(".toggle-webhook-btn").forEach((btn) => { | |
383 | - btn.addEventListener("click", async () => { | |
384 | - const id = btn.dataset.webhookId; | |
385 | - const currentActive = btn.dataset.active === "true"; | |
386 | - | |
387 | - try { | |
388 | - const response = await fetch( | |
389 | - `/api/repos/${owner}/${repo}/webhooks/${id}`, | |
390 | - { | |
391 | - method: "PATCH", | |
392 | - headers: { "Content-Type": "application/json" }, | |
393 | - body: JSON.stringify({ active: !currentActive }), | |
394 | - }, | |
395 | - ); | |
396 | - | |
397 | - if (!response.ok) throw new Error("Failed to toggle webhook"); | |
398 | - window.location.reload(); | |
399 | - } catch (error) { | |
400 | - alert(error.message); | |
401 | - } | |
402 | - }); | |
403 | - }); | |
404 | - | |
405 | - // Delete webhook | |
406 | - document.querySelectorAll(".delete-webhook-btn").forEach((btn) => { | |
407 | - btn.addEventListener("click", async () => { | |
408 | - const id = btn.dataset.webhookId; | |
409 | - if (!confirm("Delete this webhook?")) return; | |
410 | - | |
411 | - try { | |
412 | - const response = await fetch( | |
413 | - `/api/repos/${owner}/${repo}/webhooks/${id}`, | |
414 | - { | |
415 | - method: "DELETE", | |
416 | - }, | |
417 | - ); | |
418 | - | |
419 | - if (!response.ok) throw new Error("Failed to delete webhook"); | |
420 | - btn.closest("[data-webhook-id]").remove(); | |
421 | - } catch (error) { | |
422 | - alert(error.message); | |
423 | - } | |
424 | - }); | |
425 | - }); | |
426 | - | |
427 | - // Test webhook | |
428 | - document.querySelectorAll(".test-webhook-btn").forEach((btn) => { | |
429 | - btn.addEventListener("click", async () => { | |
430 | - const id = btn.dataset.webhookId; | |
431 | - | |
432 | - try { | |
433 | - const response = await fetch( | |
434 | - `/api/repos/${owner}/${repo}/webhooks/${id}/test`, | |
435 | - { | |
436 | - method: "POST", | |
437 | - }, | |
438 | - ); | |
439 | - | |
440 | - const data = await response.json(); | |
441 | - alert(data.success ? "Test ping sent!" : `Error: ${data.message}`); | |
442 | - } catch (error) { | |
443 | - alert(error.message); | |
444 | - } | |
445 | - }); | |
446 | - }); | |
447 | -</script> | |
448 | 0 | |