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
▼src/pages/[owner]/[repo]/settings/webhooks.astro
+0-447
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