diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 0573fa18..5f40ba0b 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -732,6 +732,16 @@ export const namespaceApi = { headers: await ensureCsrfHeaders(), }) }, + + async delete(slug: string, reason: string): Promise { + await fetchJson(`${WEB_API_PREFIX}/namespaces/${normalizeNamespaceSlug(slug)}`, { + method: 'DELETE', + headers: await ensureCsrfHeaders({ + 'Content-Type': 'application/json', + }), + body: JSON.stringify({ reason: reason.trim() }), + }) + }, } export const tokenApi = { diff --git a/web/src/api/types.ts b/web/src/api/types.ts index 0cba6fcc..fd7714b6 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -121,6 +121,7 @@ export interface ManagedNamespace extends Namespace { canUnfreeze: boolean canArchive: boolean canRestore: boolean + canDelete: boolean } export interface NamespaceMember { diff --git a/web/src/features/namespace/delete-namespace-dialog.tsx b/web/src/features/namespace/delete-namespace-dialog.tsx new file mode 100644 index 00000000..af64ffaa --- /dev/null +++ b/web/src/features/namespace/delete-namespace-dialog.tsx @@ -0,0 +1,97 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/shared/ui/dialog' +import { Button } from '@/shared/ui/button' +import { Textarea } from '@/shared/ui/textarea' +import { Label } from '@/shared/ui/label' + +interface DeleteNamespaceDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + namespaceName: string + onConfirm: (reason: string) => void | Promise +} + +export function DeleteNamespaceDialog({ + open, + onOpenChange, + namespaceName, + onConfirm, +}: DeleteNamespaceDialogProps) { + const { t } = useTranslation() + const [reason, setReason] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + const handleConfirm = async () => { + if (!reason.trim()) { + return + } + + setIsSubmitting(true) + try { + await onConfirm(reason) + setReason('') + onOpenChange(false) + } finally { + setIsSubmitting(false) + } + } + + const handleOpenChange = (newOpen: boolean) => { + if (!isSubmitting) { + if (!newOpen) { + setReason('') + } + onOpenChange(newOpen) + } + } + + return ( + + + + {t('namespace.delete.confirm.title')} + + {t('namespace.delete.confirm.description', { name: namespaceName })} + + +
+
+ +