Skip to content

Commit 06c6824

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 9c3130d commit 06c6824

File tree

3 files changed

+129
-41
lines changed

3 files changed

+129
-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>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
}
21+
22+
export function DeleteRepoButton({ repoId, repoName }: DeleteRepoButtonProps) {
23+
const [open, setOpen] = useState(false);
24+
const [deleting, setDeleting] = useState(false);
25+
const router = useRouter();
26+
27+
async function handleDelete() {
28+
setDeleting(true);
29+
try {
30+
const result = await deleteRepo(repoId);
31+
toast.success(`Deleted ${repoName}${result.deleted_pages} pages removed`);
32+
setOpen(false);
33+
router.refresh();
34+
} catch (err) {
35+
toast.error(`Failed to delete: ${err instanceof Error ? err.message : "Unknown error"}`);
36+
} finally {
37+
setDeleting(false);
38+
}
39+
}
40+
41+
return (
42+
<>
43+
<button
44+
onClick={(e) => {
45+
e.preventDefault();
46+
e.stopPropagation();
47+
setOpen(true);
48+
}}
49+
className="opacity-0 group-hover:opacity-100 p-1 text-[var(--color-text-tertiary)] hover:text-red-400 transition-all"
50+
title="Delete repository"
51+
>
52+
<Trash2 className="h-3.5 w-3.5" />
53+
</button>
54+
55+
<Dialog open={open} onOpenChange={setOpen}>
56+
<DialogContent className="sm:max-w-sm">
57+
<DialogHeader>
58+
<DialogTitle className="flex items-center gap-2">
59+
<AlertTriangle className="h-4 w-4 text-[var(--color-stale)]" />
60+
Delete Repository
61+
</DialogTitle>
62+
</DialogHeader>
63+
<p className="text-sm text-[var(--color-text-secondary)]">
64+
This will permanently delete{" "}
65+
<span className="font-medium text-[var(--color-text-primary)]">{repoName}</span>{" "}
66+
and all its generated pages, symbols, and history.
67+
</p>
68+
<DialogFooter>
69+
<Button variant="ghost" onClick={() => setOpen(false)}>
70+
Cancel
71+
</Button>
72+
<Button variant="destructive" onClick={handleDelete} disabled={deleting}>
73+
{deleting ? "Deleting..." : "Delete Repository"}
74+
</Button>
75+
</DialogFooter>
76+
</DialogContent>
77+
</Dialog>
78+
</>
79+
);
80+
}

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)