diff --git a/frontend/src/components/auth/ApiKeyManager.tsx b/frontend/src/components/auth/ApiKeyManager.tsx index faf08eb4..a3a1f920 100644 --- a/frontend/src/components/auth/ApiKeyManager.tsx +++ b/frontend/src/components/auth/ApiKeyManager.tsx @@ -12,6 +12,7 @@ import { } from "@/components/ui/dialog"; import { api } from "@/lib/api"; import { Key, Plus, Trash2, Copy, Check } from "lucide-react"; +import { ConfirmationDialog } from "@/components/ui/confirm-dialog"; interface ApiKey { id: string; @@ -25,6 +26,7 @@ export default function ApiKeyManager() { const [newKey, setNewKey] = useState(null); const [loading, setLoading] = useState(false); const [copied, setCopied] = useState(false); + const [revokeConfirmKeyId, setRevokeConfirmKeyId] = useState(null); const fetchKeys = async () => { try { @@ -58,9 +60,14 @@ export default function ApiKeyManager() { } }; - const revokeKey = async (id: string) => { - if (!confirm("Are you sure you want to revoke this key? Any integrations using it will immediately break.")) return; - + const revokeKey = (id: string) => { + setRevokeConfirmKeyId(id); + }; + + const executeRevokeKey = async () => { + if (!revokeConfirmKeyId) return; + const id = revokeConfirmKeyId; + setRevokeConfirmKeyId(null); try { await api.delete(`/api/v1/auth/api-keys/${id}`); setKeys((prev) => prev.filter((k) => k.id !== id)); @@ -78,97 +85,111 @@ export default function ApiKeyManager() { }; return ( - { if (!open) setNewKey(null); }}> - - - API Keys - - } - /> - + <> + { if (!open) setNewKey(null); }}> + + + API Keys + + } + /> + - - API Keys - - Manage API keys to access the RAG engine programmatically from your own applications or scripts. - - + + API Keys + + Manage API keys to access the RAG engine programmatically from your own applications or scripts. + + - {newKey && ( -
-

- Save your new API key -

-

- Please copy this key and store it somewhere safe. For security reasons, you will never be able to view it again. -

-
- - {newKey} - - + {newKey && ( +
+

+ Save your new API key +

+

+ Please copy this key and store it somewhere safe. For security reasons, you will never be able to view it again. +

+
+ + {newKey} + + +
-
- )} + )} -
-
-

Active Keys

- -
+
+
+

Active Keys

+ +
-
- {keys.length === 0 ? ( -
- - You don't have any API keys yet. -
- ) : ( -
- {keys.map((key) => ( -
-
-
- {key.key_prefix}•••••••••••••••••••••• -
-
- Created: {new Date(key.created_at).toLocaleDateString()} - Last used: {key.last_used ? new Date(key.last_used).toLocaleDateString() : "Never"} +
+ {keys.length === 0 ? ( +
+ + You don't have any API keys yet. +
+ ) : ( +
+ {keys.map((key) => ( +
+
+
+ {key.key_prefix}•••••••••••••••••••••• +
+
+ Created: {new Date(key.created_at).toLocaleDateString()} + Last used: {key.last_used ? new Date(key.last_used).toLocaleDateString() : "Never"} +
+
- -
- ))} -
- )} + ))} +
+ )} +
-
- -
+
+
+ + {/* Revoke Key Confirmation Dialog */} + { if (!open) setRevokeConfirmKeyId(null); }} + title="Revoke API Key" + message="Are you sure you want to revoke this key? Any integrations using it will immediately break." + confirmLabel="Revoke" + cancelLabel="Cancel" + variant="danger" + onConfirm={executeRevokeKey} + /> + ); } diff --git a/frontend/src/components/document/DocumentSidebar.tsx b/frontend/src/components/document/DocumentSidebar.tsx index 4ffd3524..1fa42a29 100644 --- a/frontend/src/components/document/DocumentSidebar.tsx +++ b/frontend/src/components/document/DocumentSidebar.tsx @@ -18,6 +18,7 @@ import { Settings } from "lucide-react"; import DocumentSettings from "./DocumentSettings"; import DocumentCard from "./DocumentCard"; import { toast } from "sonner"; +import { ConfirmationDialog } from "@/components/ui/confirm-dialog"; interface Props { documents: DocInfo[]; @@ -63,6 +64,7 @@ export default function DocumentSidebar({ const [uploadProgress, setUploadProgress] = useState(0); const [uploadError, setUploadError] = useState(""); const [deleting, setDeleting] = useState(null); + const [deleteConfirmDocId, setDeleteConfirmDocId] = useState(null); const [settingsDoc, setSettingsDoc] = useState(null); const [editingDocId, setEditingDocId] = useState(null); const [draftName, setDraftName] = useState(""); @@ -137,10 +139,16 @@ export default function DocumentSidebar({ disabled: uploading, }); - const handleDelete = async (docId: string, e: React.MouseEvent) => { + const handleDelete = (docId: string, e: React.MouseEvent) => { e.stopPropagation(); - if (!confirm(t("documents.deleteConfirm"))) return; + setDeleteConfirmDocId(docId); + }; + + const executeDelete = async () => { + if (!deleteConfirmDocId) return; + const docId = deleteConfirmDocId; setDeleting(docId); + setDeleteConfirmDocId(null); try { await api.delete(`/api/v1/documents/${docId}`); await onDocumentsChange(); @@ -466,6 +474,19 @@ export default function DocumentSidebar({ )} + {/* Delete Confirmation Dialog */} + { if (!open) setDeleteConfirmDocId(null); }} + title={t("documents.deleteTitle") || "Delete Document"} + message={t("documents.deleteConfirm")} + confirmLabel={t("documents.deleteConfirmLabel") || "Delete"} + cancelLabel={t("documents.deleteCancelLabel") || "Cancel"} + variant="danger" + loading={deleting !== null} + onConfirm={executeDelete} + /> + {/* Settings Modal */} {/* The DocumentSettings component is rendered here and controlled by the settingsDoc state. When a user clicks the settings button for a document, it sets that document in settingsDoc, which opens the modal. The modal can then call onDocumentsChange to refresh the list after saving settings. */} {settingsDoc && ( diff --git a/frontend/src/components/ui/confirm-dialog.tsx b/frontend/src/components/ui/confirm-dialog.tsx new file mode 100644 index 00000000..a8f0abe8 --- /dev/null +++ b/frontend/src/components/ui/confirm-dialog.tsx @@ -0,0 +1,112 @@ +"use client" + +import * as React from "react" +import { Loader2 } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" + +const variantStyles = { + danger: { + confirmVariant: "destructive" as const, + icon: "text-destructive", + border: "data-closed:animate-out", + }, + warning: { + confirmVariant: "secondary" as const, + icon: "text-amber-500", + border: "data-closed:animate-out", + }, + default: { + confirmVariant: "default" as const, + icon: "text-primary", + border: "data-closed:animate-out", + }, +} as const + +export interface ConfirmationDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + title: string + message: string | React.ReactNode + confirmLabel?: string + cancelLabel?: string + variant?: "default" | "danger" | "warning" + loading?: boolean + onConfirm: () => void | Promise +} + +export function ConfirmationDialog({ + open, + onOpenChange, + title, + message, + confirmLabel = "Confirm", + cancelLabel = "Cancel", + variant = "default", + loading = false, + onConfirm, +}: ConfirmationDialogProps) { + const style = variantStyles[variant] + + const handleConfirm = async () => { + await onConfirm() + } + + return ( + + + + + {title} + + + {typeof message === "string" ? ( + {message} + ) : ( + message + )} + + + + + + + + + + ) +}