Skip to content

Commit f781ac8

Browse files
committed
feat: add delete repo button to dashboard UI
Trash icon appears on hover in the repo list. Clicking shows a confirmation dialog, then calls DELETE /api/repos/{id} and refreshes.
1 parent cc785fc commit f781ac8

File tree

4 files changed

+156
-41
lines changed

4 files changed

+156
-41
lines changed

packages/web/src/app/page.tsx

Lines changed: 44 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ConfidenceBadge } from "@/components/wiki/confidence-badge";
2121
import { EmptyState } from "@/components/shared/empty-state";
2222
import { formatRelativeTime, formatNumber } from "@/lib/utils/format";
2323
import { scoreToStatus } from "@/lib/utils/confidence";
24+
import { DeleteRepoButton } from "@/components/repos/delete-repo-button";
2425

2526
export const metadata: Metadata = { title: "Dashboard" };
2627

@@ -124,48 +125,51 @@ export default async function DashboardPage() {
124125
) : (
125126
<ul className="divide-y divide-[var(--color-border-default)]">
126127
{repoList.map((repo) => (
127-
<li key={repo.id}>
128-
<Link
129-
href={`/repos/${repo.id}`}
130-
className="flex items-start gap-3 px-6 py-3.5 transition-colors hover:bg-[var(--color-bg-elevated)] group"
131-
>
132-
<div className="mt-0.5 h-2 w-2 rounded-full bg-[var(--color-accent-primary)] shrink-0" />
133-
<div className="flex-1 min-w-0">
134-
<div className="flex items-center gap-2">
135-
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-[var(--color-accent-primary)] transition-colors">
136-
{repo.name}
137-
</p>
138-
</div>
139-
<div className="flex items-center gap-2 mt-0.5">
140-
<p className="text-xs text-[var(--color-text-tertiary)] font-mono truncate" title={repo.local_path}>
141-
{repo.local_path}
142-
</p>
143-
</div>
144-
<div className="flex items-center gap-2 mt-1 flex-wrap">
145-
{repo.head_commit && (
146-
<span className="text-xs font-mono text-[var(--color-text-tertiary)]">
147-
{repo.head_commit.slice(0, 7)}
128+
<li key={repo.id} className="group">
129+
<div className="flex items-start gap-3 px-6 py-3.5 transition-colors hover:bg-[var(--color-bg-elevated)]">
130+
<Link
131+
href={`/repos/${repo.id}`}
132+
className="flex items-start gap-3 flex-1 min-w-0"
133+
>
134+
<div className="mt-0.5 h-2 w-2 rounded-full bg-[var(--color-accent-primary)] shrink-0" />
135+
<div className="flex-1 min-w-0">
136+
<div className="flex items-center gap-2">
137+
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-[var(--color-accent-primary)] transition-colors">
138+
{repo.name}
139+
</p>
140+
</div>
141+
<div className="flex items-center gap-2 mt-0.5">
142+
<p className="text-xs text-[var(--color-text-tertiary)] font-mono truncate" title={repo.local_path}>
143+
{repo.local_path}
144+
</p>
145+
</div>
146+
<div className="flex items-center gap-2 mt-1 flex-wrap">
147+
{repo.head_commit && (
148+
<span className="text-xs font-mono text-[var(--color-text-tertiary)]">
149+
{repo.head_commit.slice(0, 7)}
150+
</span>
151+
)}
152+
<span className="text-xs text-[var(--color-text-tertiary)]">
153+
Updated {formatRelativeTime(repo.updated_at)}
148154
</span>
149-
)}
150-
<span className="text-xs text-[var(--color-text-tertiary)]">
151-
Updated {formatRelativeTime(repo.updated_at)}
152-
</span>
153-
{gitMap.has(repo.id) && (() => {
154-
const g = gitMap.get(repo.id)!;
155-
return (
156-
<>
157-
{g.hotspot_count > 0 && (
158-
<Badge variant="outdated">{g.hotspot_count} hotspot{g.hotspot_count !== 1 ? "s" : ""}</Badge>
159-
)}
160-
{g.stable_count > 0 && (
161-
<Badge variant="fresh">{g.stable_count} stable</Badge>
162-
)}
163-
</>
164-
);
165-
})()}
155+
{gitMap.has(repo.id) && (() => {
156+
const g = gitMap.get(repo.id)!;
157+
return (
158+
<>
159+
{g.hotspot_count > 0 && (
160+
<Badge variant="outdated">{g.hotspot_count} hotspot{g.hotspot_count !== 1 ? "s" : ""}</Badge>
161+
)}
162+
{g.stable_count > 0 && (
163+
<Badge variant="fresh">{g.stable_count} stable</Badge>
164+
)}
165+
</>
166+
);
167+
})()}
168+
</div>
166169
</div>
167-
</div>
168-
</Link>
170+
</Link>
171+
<DeleteRepoButton repoId={repo.id} repoName={repo.name} />
172+
</div>
169173
</li>
170174
))}
171175
</ul>

packages/web/src/app/repos/[id]/settings/page.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { notFound } from "next/navigation";
33
import { Settings } from "lucide-react";
44
import { getRepo } from "@/lib/api/repos";
55
import { RepoSettingsForm } from "@/components/repos/repo-settings-form";
6+
import { DeleteRepoButton } from "@/components/repos/delete-repo-button";
67
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
78
import { Separator } from "@/components/ui/separator";
89
import { OperationsPanel } from "@/components/repos/operations-panel";
@@ -83,6 +84,20 @@ export default async function RepoSettingsPage({ params }: Props) {
8384
))}
8485
</div>
8586
</div>
87+
88+
<Separator />
89+
90+
<Card className="border-red-900/30">
91+
<CardHeader>
92+
<CardTitle className="text-sm font-medium text-red-400">Danger Zone</CardTitle>
93+
<CardDescription>
94+
Permanently delete this repository and all its generated pages, symbols, and history.
95+
</CardDescription>
96+
</CardHeader>
97+
<CardContent>
98+
<DeleteRepoButton repoId={id} repoName={repo.name} variant="button" />
99+
</CardContent>
100+
</Card>
86101
</div>
87102
);
88103
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { useRouter } from "next/navigation";
5+
import { toast } from "sonner";
6+
import { Trash2, AlertTriangle } from "lucide-react";
7+
import { deleteRepo } from "@/lib/api/repos";
8+
import { Button } from "@/components/ui/button";
9+
import {
10+
Dialog,
11+
DialogContent,
12+
DialogHeader,
13+
DialogTitle,
14+
DialogFooter,
15+
} from "@/components/ui/dialog";
16+
17+
interface DeleteRepoButtonProps {
18+
repoId: string;
19+
repoName: string;
20+
variant?: "icon" | "button";
21+
}
22+
23+
export function DeleteRepoButton({ repoId, repoName, variant = "icon" }: DeleteRepoButtonProps) {
24+
const [open, setOpen] = useState(false);
25+
const [deleting, setDeleting] = useState(false);
26+
const router = useRouter();
27+
28+
async function handleDelete() {
29+
setDeleting(true);
30+
try {
31+
const result = await deleteRepo(repoId);
32+
toast.success(`Deleted ${repoName}${result.deleted_pages} pages removed`);
33+
setOpen(false);
34+
router.refresh();
35+
} catch (err) {
36+
toast.error(`Failed to delete: ${err instanceof Error ? err.message : "Unknown error"}`);
37+
} finally {
38+
setDeleting(false);
39+
}
40+
}
41+
42+
return (
43+
<>
44+
{variant === "button" ? (
45+
<Button
46+
variant="destructive"
47+
size="sm"
48+
onClick={() => setOpen(true)}
49+
>
50+
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
51+
Delete Repository
52+
</Button>
53+
) : (
54+
<button
55+
onClick={(e) => {
56+
e.preventDefault();
57+
e.stopPropagation();
58+
setOpen(true);
59+
}}
60+
className="opacity-0 group-hover:opacity-100 p-1 text-[var(--color-text-tertiary)] hover:text-red-400 transition-all"
61+
title="Delete repository"
62+
>
63+
<Trash2 className="h-3.5 w-3.5" />
64+
</button>
65+
)}
66+
67+
<Dialog open={open} onOpenChange={setOpen}>
68+
<DialogContent className="sm:max-w-sm">
69+
<DialogHeader>
70+
<DialogTitle className="flex items-center gap-2">
71+
<AlertTriangle className="h-4 w-4 text-[var(--color-stale)]" />
72+
Delete Repository
73+
</DialogTitle>
74+
</DialogHeader>
75+
<p className="text-sm text-[var(--color-text-secondary)]">
76+
This will permanently delete{" "}
77+
<span className="font-medium text-[var(--color-text-primary)]">{repoName}</span>{" "}
78+
and all its generated pages, symbols, and history.
79+
</p>
80+
<DialogFooter>
81+
<Button variant="ghost" onClick={() => setOpen(false)}>
82+
Cancel
83+
</Button>
84+
<Button variant="destructive" onClick={handleDelete} disabled={deleting}>
85+
{deleting ? "Deleting..." : "Delete Repository"}
86+
</Button>
87+
</DialogFooter>
88+
</DialogContent>
89+
</Dialog>
90+
</>
91+
);
92+
}

packages/web/src/lib/api/repos.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { apiGet, apiPost, apiPatch } from "./client";
1+
import { apiGet, apiPost, apiPatch, apiDelete } from "./client";
22
import type { RepoCreate, RepoUpdate, RepoResponse, JobResponse, RepoStatsResponse } from "./types";
33

44
export async function listRepos(): Promise<RepoResponse[]> {
@@ -25,6 +25,10 @@ export async function fullResyncRepo(repoId: string): Promise<JobResponse> {
2525
return apiPost<JobResponse>(`/api/repos/${repoId}/full-resync`);
2626
}
2727

28+
export async function deleteRepo(repoId: string): Promise<{ ok: boolean; deleted_pages: number }> {
29+
return apiDelete<{ ok: boolean; deleted_pages: number }>(`/api/repos/${repoId}`);
30+
}
31+
2832
export async function getRepoStats(repoId: string): Promise<RepoStatsResponse> {
2933
return apiGet<RepoStatsResponse>(`/api/repos/${repoId}/stats`);
3034
}

0 commit comments

Comments
 (0)