From 8d8b92e9c96cd8785e9f81992345cb09b950ff73 Mon Sep 17 00:00:00 2001 From: Trezy Date: Tue, 23 Jun 2026 13:52:08 -0500 Subject: [PATCH] fix: port service entry design optimisations to other pages Signed-off-by: Trezy Signed-off-by: Trezy --- web/src/app/dashboard/backfill/page.tsx | 20 +-- web/src/app/dashboard/dead-letters/page.tsx | 95 +++++++++++--- web/src/app/dashboard/lexicons/page.tsx | 61 ++++++--- web/src/app/dashboard/records/page.tsx | 119 +++++++++++------- .../dashboard/settings/api-clients/page.tsx | 64 ++++++---- .../app/dashboard/settings/api-keys/page.tsx | 75 +++++------ .../dashboard/settings/env-variables/page.tsx | 65 +++++++--- .../app/dashboard/settings/labelers/page.tsx | 68 +++++----- .../app/dashboard/settings/scripts/page.tsx | 61 ++++++--- web/src/app/dashboard/settings/users/page.tsx | 85 ++++++++----- 10 files changed, 455 insertions(+), 258 deletions(-) diff --git a/web/src/app/dashboard/backfill/page.tsx b/web/src/app/dashboard/backfill/page.tsx index 27e007b..3a7807d 100644 --- a/web/src/app/dashboard/backfill/page.tsx +++ b/web/src/app/dashboard/backfill/page.tsx @@ -2,8 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; +import { toast } from "sonner"; import { useCurrentUser } from "@/hooks/use-current-user"; +import { toastError } from "@/lib/format"; import { cancelBackfillJob, pauseBackfillJob, @@ -377,13 +379,12 @@ function ProfileRow({ did, profile, suffix }: { export default function BackfillPage() { const { hasPermission } = useCurrentUser(); const [jobs, setJobs] = useState([]); - const [error, setError] = useState(null); const [selectedJobId, setSelectedJobId] = useState(null); const load = useCallback(() => { getBackfillJobs() .then(setJobs) - .catch((e) => setError(e.message)); + .catch((e) => toastError("Failed to load backfill jobs", e)); }, []); useEffect(() => { @@ -404,8 +405,6 @@ export default function BackfillPage() { <>
- {error &&

{error}

} -

Backfill Jobs

@@ -425,6 +424,7 @@ export default function BackfillPage() { Cancel { await flushAllBackfillDetails(); + toast.success("All job details cleared"); setSelectedJobId(null); load(); }}>Clear @@ -456,7 +456,7 @@ export default function BackfillPage() { colSpan={5} className="text-muted-foreground text-center" > - No backfill jobs yet. + No backfill jobs yet. Create a job to import historical records from the AT Protocol network. )} @@ -608,6 +608,7 @@ function JobDetail({ setCancelling(true); try { await onCancel(); + toast.success("Backfill job cancelled"); } finally { setCancelling(false); } @@ -617,6 +618,7 @@ function JobDetail({ setPausing(true); try { await onPause(); + toast.success("Backfill job paused"); } finally { setPausing(false); } @@ -626,6 +628,7 @@ function JobDetail({ setResuming(true); try { await onResume(); + toast.success("Backfill job resumed"); } finally { setResuming(false); } @@ -923,6 +926,7 @@ function JobDetail({ Cancel { await flushBackfillDetails(job.id); + toast.success("Job details cleared"); setDiscoveredRepos([]); setDiscoveredCursor(null); setDiscoveredLoaded(false); @@ -1134,7 +1138,6 @@ function ProgressRow({ function CreateDialog({ onSuccess }: { onSuccess: () => void }) { const [collection, setCollection] = useState(null); const [did, setDid] = useState(""); - const [error, setError] = useState(null); const [open, setOpen] = useState(false); const [recordLexicons, setRecordLexicons] = useState([]); @@ -1154,18 +1157,18 @@ function CreateDialog({ onSuccess }: { onSuccess: () => void }) { }, [open]); async function handleCreate() { - setError(null); try { await createBackfillJob({ collection: collection || undefined, did: did || undefined, }); + toast.success("Backfill job created"); setCollection(null); setDid(""); setOpen(false); onSuccess(); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); + toastError("Failed to create backfill job", e); } } @@ -1194,7 +1197,6 @@ function CreateDialog({ onSuccess }: { onSuccess: () => void }) {
- {error &&

{error}

}
([]); - const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [viewDetail, setViewDetail] = useState(null); const [actionLoading, setActionLoading] = useState(false); const [resolvedFilter, setResolvedFilter] = useState("false"); const [rowSelection, setRowSelection] = useState({}); + const [dismissAllOpen, setDismissAllOpen] = useState(false); const [cursorStack, setCursorStack] = useState([]); const [nextCursor, setNextCursor] = useState(null); @@ -191,7 +204,6 @@ export default function DeadLettersPage() { const fetchItems = useCallback( async (cursor?: string) => { setLoading(true); - setError(null); try { const data = await getDeadLetters({ collection: collectionFilter, @@ -203,7 +215,7 @@ export default function DeadLettersPage() { setNextCursor(data.cursor); setRowSelection({}); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); + toastError("Failed to load dead letters", e); setItems([]); setNextCursor(null); } finally { @@ -218,6 +230,21 @@ export default function DeadLettersPage() { fetchItems(); }, [fetchItems]); + useEffect(() => { + if (Object.keys(rowSelection).length === 0) return; + const validIds = new Set(items.map((item) => item.id)); + const pruned: RowSelectionState = {}; + let changed = false; + for (const [id, selected] of Object.entries(rowSelection)) { + if (validIds.has(id)) { + pruned[id] = selected; + } else { + changed = true; + } + } + if (changed) setRowSelection(pruned); + }, [items]); + function handleNext() { if (!nextCursor) return; setCursorStack((prev) => [...prev, nextCursor]); @@ -237,8 +264,8 @@ export default function DeadLettersPage() { try { const detail = await getDeadLetter(row.id); setViewDetail(detail); - } catch { - setError("Failed to load detail"); + } catch (e: unknown) { + toastError("Failed to load dead letter detail", e); } } @@ -249,10 +276,11 @@ export default function DeadLettersPage() { if (action === "retry") await retryDeadLetter(viewDetail.id); else if (action === "reindex") await reindexDeadLetter(viewDetail.id); else await dismissDeadLetter(viewDetail.id); + toast.success(action === "retry" ? "Dead letter retried" : action === "reindex" ? "Dead letter re-indexed" : "Dead letter dismissed"); setViewDetail(null); fetchItems(); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); + toastError(`Failed to ${action} dead letter`, e); } finally { setActionLoading(false); } @@ -269,6 +297,7 @@ export default function DeadLettersPage() { ) { setLoading(true); try { + const count = selectedIds.length; const body = scope === "all" ? { all: true, collection: collectionFilter } @@ -276,10 +305,14 @@ export default function DeadLettersPage() { if (action === "retry") await bulkRetryDeadLetters(body); else if (action === "reindex") await bulkReindexDeadLetters(body); else await bulkDismissDeadLetters(body); + const verb = action === "retry" ? "retried" : action === "reindex" ? "re-indexed" : "dismissed"; + toast.success(scope === "all" + ? `All matching dead letters ${verb}` + : `${count} dead ${count === 1 ? "letter" : "letters"} ${verb}`); setRowSelection({}); fetchItems(); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); + toastError(`Failed to ${action} dead letters`, e); } finally { setLoading(false); } @@ -442,8 +475,6 @@ export default function DeadLettersPage() { <>
- {error &&

{error}

} -
([]); - const [error, setError] = useState(null); const load = useCallback(() => { getScriptVariables() .then(setVars) - .catch((e) => setError(e.message)); + .catch((e) => toastError("Failed to load variables", e)); }, []); useEffect(() => { @@ -52,9 +64,10 @@ export default function EnvVariablesPage() { async function handleDeleteVar(key: string) { try { await deleteScriptVariable(key); + toast.success("Variable deleted"); load(); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); + toastError("Failed to delete variable", e); } } @@ -62,8 +75,6 @@ export default function EnvVariablesPage() { <>
- {error &&

{error}

} -

Script Variables

@@ -94,7 +105,7 @@ export default function EnvVariablesPage() { colSpan={4} className="text-muted-foreground text-center" > - No script variables yet. + No script variables yet. Variables defined here are accessible to Lua scripts via the env global table. )} @@ -116,16 +127,37 @@ export default function EnvVariablesPage() { editKey={v.key} /> {hasPermission("script-variables:delete") && ( - + + + + + + + Delete variable? + + This will permanently remove the variable. Scripts + using this variable will fail on next execution. + + + + Cancel + handleDeleteVar(v.key)} + > + Delete + + + + )}
@@ -160,6 +192,7 @@ function UpsertVariableDialog({ key: isEdit ? editKey : key, value, }); + toast.success(isEdit ? "Variable updated" : "Variable created"); setKey(editKey ?? ""); setValue(""); setOpen(false); diff --git a/web/src/app/dashboard/settings/labelers/page.tsx b/web/src/app/dashboard/settings/labelers/page.tsx index 7175eb8..39da8ab 100644 --- a/web/src/app/dashboard/settings/labelers/page.tsx +++ b/web/src/app/dashboard/settings/labelers/page.tsx @@ -2,8 +2,10 @@ import { useCallback, useEffect, useState } from "react"; import { Trash2, Pause, Play } from "lucide-react"; +import { toast } from "sonner"; import { useCurrentUser } from "@/hooks/use-current-user"; +import { toastError } from "@/lib/format"; import { getLabelers, addLabeler, @@ -12,6 +14,16 @@ import { } from "@/lib/api"; import type { LabelerSummary } from "@/types/labelers"; import { SiteHeader } from "@/components/site-header"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { @@ -39,14 +51,13 @@ export default function LabelersPage() { const { hasPermission } = useCurrentUser(); const [labelers, setLabelers] = useState([]); const [handles, setHandles] = useState>({}); - const [error, setError] = useState(null); const [deleteDid, setDeleteDid] = useState(null); const [deleting, setDeleting] = useState(false); const load = useCallback(() => { getLabelers() .then(setLabelers) - .catch((e) => setError(e.message)); + .catch((e) => toastError("Failed to load labelers", e)); }, []); useEffect(() => { @@ -77,9 +88,10 @@ export default function LabelersPage() { try { const newStatus = labeler.status === "active" ? "paused" : "active"; await updateLabeler(labeler.did, { status: newStatus }); + toast.success(labeler.status === "active" ? "Labeler paused" : "Labeler resumed"); load(); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); + toastError("Failed to update labeler status", e); } } @@ -88,9 +100,10 @@ export default function LabelersPage() { try { await deleteLabeler(did); setDeleteDid(null); + toast.success("Labeler deleted"); load(); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); + toastError("Failed to delete labeler", e); } finally { setDeleting(false); } @@ -100,8 +113,6 @@ export default function LabelersPage() { <>
- {error &&

{error}

} -

Labeler Subscriptions

@@ -133,7 +144,7 @@ export default function LabelersPage() { colSpan={6} className="text-muted-foreground text-center" > - No labeler subscriptions yet. + No labeler subscriptions yet. Add a labeler to subscribe to external content labeling services. )} @@ -208,43 +219,32 @@ export default function LabelersPage() {
- { - if (!open) setDeleteDid(null); - }} - > - - - Delete labeler? - + { if (!open) setDeleteDid(null); }}> + + + Delete labeler? + This will remove the labeler subscription and delete all labels it has emitted. This action cannot be undone. - - + + {deleteDid && ( {deleteDid} )} - - - - - - - - + + + + ); } @@ -262,10 +262,12 @@ function AddLabelerDialog({ setError(null); try { await addLabeler({ did }); + toast.success("Labeler added"); setDid(""); setOpen(false); onSuccess(); } catch (e: unknown) { + toastError("Failed to add labeler", e); setError(e instanceof Error ? e.message : String(e)); } } diff --git a/web/src/app/dashboard/settings/scripts/page.tsx b/web/src/app/dashboard/settings/scripts/page.tsx index 36e993a..2bc127e 100644 --- a/web/src/app/dashboard/settings/scripts/page.tsx +++ b/web/src/app/dashboard/settings/scripts/page.tsx @@ -18,8 +18,10 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ExternalLink, Eye, Trash2 } from "lucide-react"; +import { toast } from "sonner"; import { useCurrentUser } from "@/hooks/use-current-user"; +import { toastError } from "@/lib/format"; import { deleteScript, getScripts } from "@/lib/api"; import type { Script } from "@/types/scripts"; import { @@ -34,6 +36,17 @@ import { DataTable } from "@/components/data-table/data-table"; import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"; import { DataTableToolbar } from "@/components/data-table/data-table-toolbar"; import { SiteHeader } from "@/components/site-header"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -47,12 +60,11 @@ export default function ScriptsPage() { const { hasPermission } = useCurrentUser(); const router = useRouter(); const [scripts, setScripts] = useState([]); - const [error, setError] = useState(null); const load = useCallback(() => { getScripts() .then(setScripts) - .catch((e) => setError(e instanceof Error ? e.message : String(e))); + .catch((e) => toastError("Failed to load scripts", e)); }, []); useEffect(() => { @@ -74,12 +86,12 @@ export default function ScriptsPage() { ); async function handleDelete(id: string) { - if (!confirm(`Delete script '${id}'?`)) return; try { await deleteScript(id); + toast.success("Script deleted"); load(); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); + toastError("Failed to delete script", e); } } @@ -224,19 +236,32 @@ export default function ScriptsPage() { {hasPermission("scripts:manage") && ( - + + + + + + + Delete script? + + This will permanently remove the script. This action cannot be undone. + + + + Cancel + handleDelete(row.original.id)}>Delete + + + )}
), @@ -288,8 +313,6 @@ export default function ScriptsPage() { <>
- {error &&

{error}

} - diff --git a/web/src/app/dashboard/settings/users/page.tsx b/web/src/app/dashboard/settings/users/page.tsx index f3d06b3..23096c9 100644 --- a/web/src/app/dashboard/settings/users/page.tsx +++ b/web/src/app/dashboard/settings/users/page.tsx @@ -5,6 +5,7 @@ import { ChevronRight, Search, Shield, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { useAuth } from "@/lib/auth-context"; +import { toastError } from "@/lib/format"; import { getUsers, addUser, @@ -15,6 +16,17 @@ import { } from "@/lib/api"; import type { PermissionEntry, PermissionTemplate } from "@/lib/api"; import type { UserSummary } from "@/types/users"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; import { SiteHeader } from "@/components/site-header"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -77,7 +89,6 @@ export default function UsersPage() { const { did: currentDid } = useAuth(); const [users, setUsers] = useState([]); const [handles, setHandles] = useState>({}); - const [error, setError] = useState(null); const [selectedUserId, setSelectedUserId] = useState(null); const [pendingPermissions, setPendingPermissions] = useState([]); const [saving, setSaving] = useState(false); @@ -124,7 +135,7 @@ export default function UsersPage() { const load = useCallback(() => { getUsers() .then(setUsers) - .catch((e) => setError(e instanceof Error ? e.message : String(e))); + .catch((e) => toastError("Failed to load users", e)); }, []); useEffect(() => { @@ -134,7 +145,7 @@ export default function UsersPage() { setPermissionEntries(catalog.permissions); setTemplates(catalog.templates); }) - .catch((e) => setError(e instanceof Error ? e.message : String(e))); + .catch((e) => toastError("Failed to load permissions", e)); }, [load]); // Resolve DIDs to handles via PLC directory @@ -190,9 +201,10 @@ export default function UsersPage() { async function handleDelete(id: string) { try { await deleteUser(id); + toast.success("User deleted"); load(); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); + toastError("Failed to delete user", e); } } @@ -253,7 +265,7 @@ export default function UsersPage() { toast.success("Permissions updated"); load(); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); + toastError("Failed to save permissions", e); } finally { setSaving(false); } @@ -262,9 +274,10 @@ export default function UsersPage() { async function handleTransferSuper(targetUserId: string) { try { await transferSuper({ target_user_id: targetUserId }); + toast.success("Ownership transferred"); load(); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); + toastError("Failed to transfer ownership", e); } } @@ -272,8 +285,6 @@ export default function UsersPage() { <>
- {error &&

{error}

} -

Users

{(isCurrentUserSuper || @@ -304,7 +315,7 @@ export default function UsersPage() { colSpan={5} className="text-muted-foreground text-center" > - No users yet. + No users yet. The first authenticated user is automatically granted owner permissions. )} @@ -550,25 +561,37 @@ export default function UsersPage() {
- + + + + + + + Delete user? + + This will permanently remove the user and revoke all their permissions. This action cannot be undone. + + + + Cancel + { handleDelete(selectedUser.id); setSelectedUserId(null); }}>Delete + + + {isCurrentUserSuper && ( (""); - const [error, setError] = useState(null); const [open, setOpen] = useState(false); async function handleAdd() { - setError(null); try { const body: { did: string; template?: string } = { did }; if (template) body.template = template; await addUser(body); + toast.success("User added"); setDid(""); setTemplate(""); setOpen(false); onSuccess(); } catch (e: unknown) { - setError(e instanceof Error ? e.message : String(e)); + toastError("Failed to add user", e); } } @@ -800,7 +822,6 @@ function AddUserDialog({
- {error &&

{error}

}