import { toast } from "@unom/ui/toast"; import { AlertTriangle, Lock, RefreshCw, ShieldCheck, ShieldOff, Trash2, } from "lucide-react"; import { type FC, type FormEvent, useState } from "react"; import { ApiError } from "@/api/fetcher"; import { type SourceBody, type StoreSource, useDeleteSource, useRefreshCatalog, useSetSource, useStoreSources, } from "@/api/store"; import { QueryState } from "@/components/query-state"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { m } from "@/paraglide/messages"; /** A source the operator has filled in but not yet agreed to trust. */ type SourceDraft = SourceBody & { name: string }; /** Unix seconds → a locale date-time, or "never" for a source that has never fetched. */ const fmtFetched = (secs: number): string => secs > 0 ? new Date(secs * 1000).toLocaleString() : m.store_source_never(); /** * Container: the catalog sources. Owns the source listing, the refresh-all action, and add/remove. * Adding is a two-step: the form collects the source, and a one-time trust dialog states plainly * what trusting a third-party catalog means before anything is written to the host. */ export const SourcesTab: FC = () => { const sources = useStoreSources(); const refresh = useRefreshCatalog(); const save = useSetSource(); const remove = useDeleteSource(); // The draft waiting on the trust dialog, and a key that re-mounts (and so clears) the form. const [draft, setDraft] = useState(null); const [formKey, setFormKey] = useState(0); const onRefresh = () => refresh.mutate(undefined, { onError: () => toast.error(m.store_refresh_failed()), }); const onConfirmAdd = async () => { if (!draft) return; try { await save.mutateAsync(draft); setDraft(null); setFormKey((k) => k + 1); } catch { toast.error(m.store_add_source_failed()); } }; const onRemove = async (source: StoreSource) => { if (!confirm(m.store_source_remove_confirm({ name: source.name }))) return; try { await remove.mutateAsync(source.name); } catch (e) { // 403 is the host refusing to drop its built-in catalog — say exactly that. toast.error( e instanceof ApiError && e.status === 403 ? m.store_source_builtin_locked() : m.store_source_remove_failed(), ); } }; return (
setDraft(null)} onConfirm={onConfirmAdd} />
); }; /** The source table: health per source, with the built-in one locked. */ export const SourceList: FC<{ sources: { data?: StoreSource[]; isLoading: boolean; error: unknown; refetch?: () => void; }; /** Name of the source whose delete is in flight, or null. */ busyName: string | null; isRefreshing: boolean; onRefresh: () => void; onRemove: (source: StoreSource) => void; }> = ({ sources, busyName, isRefreshing, onRefresh, onRemove }) => { const rows = sources.data ?? []; return ( {m.store_sources_title()}

{m.store_sources_help()}

{rows.map((s) => (
{s.name} {s.builtin && ( {m.store_source_builtin()} )} {s.signed ? ( {m.store_source_signed()} ) : ( {m.store_source_unsigned()} )} {s.stale && ( {m.store_source_stale()} )}

{s.url}

{m.store_source_entries({ count: s.entry_count })} ·{" "} {m.store_source_fetched({ when: fmtFetched(s.fetched_at) })}

{s.error && (

{s.error}

)}
{/* The built-in catalog gets no delete button at all — not a disabled one. */} {!s.builtin && ( )}
))}
); }; /** The add-source form. Reports a draft; the parent takes it through the trust dialog. */ export const AddSourceForm: FC<{ onSubmit: (draft: SourceDraft) => void; isSaving: boolean; }> = ({ onSubmit, isSaving }) => { const [name, setName] = useState(""); const [url, setUrl] = useState(""); const [publicKey, setPublicKey] = useState(""); const handleSubmit = (e: FormEvent) => { e.preventDefault(); const key = publicKey.trim(); if (!name.trim() || !url.trim()) return; onSubmit({ name: name.trim(), url: url.trim(), public_key: key ? key : undefined, }); }; return ( {m.store_add_source_title()}
setName(e.target.value)} />
setUrl(e.target.value)} />
setPublicKey(e.target.value)} />

{m.store_field_source_key_help()}

); }; /** The one-time trust warning shown before a third-party catalog is written to the host. */ export const TrustSourceDialog: FC<{ draft: SourceDraft | null; isSaving: boolean; onCancel: () => void; onConfirm: () => void; }> = ({ draft, isSaving, onCancel, onConfirm }) => ( !open && onCancel()}> {draft && ( {m.store_source_trust_title()} {m.store_source_trust_body({ name: draft.name })}

{draft.url}

{!draft.public_key && (

{m.store_source_trust_unsigned()}

)}
)}
);