diff --git a/apps/web/src/components/pages/ACL.tsx b/apps/web/src/components/pages/ACL.tsx index a3c964c..ffd37b5 100644 --- a/apps/web/src/components/pages/ACL.tsx +++ b/apps/web/src/components/pages/ACL.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; import { Suspense } from "react"; -import { useTranslation } from "react-i18next"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -14,6 +13,7 @@ import { Plus, Download, Upload, Trash2, Edit, Loader2, UserCog } from "lucide-r import { ACLRule } from "@/types"; import { useToast } from "@/hooks/use-toast"; import { SkeletonTable } from "@/components/ui/skeletons"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { useSuspenseAclRules, useCreateAclRule, @@ -25,11 +25,11 @@ import { // Component for ACL rules table with suspense function AclRulesTable() { - const { t } = useTranslation(); const { toast } = useToast(); const { data: rules } = useSuspenseAclRules(); const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingRule, setEditingRule] = useState(null); + const [ruleToDelete, setRuleToDelete] = useState<{ id: string; name: string } | null>(null); const createAclRule = useCreateAclRule(); const updateAclRule = useUpdateAclRule(); @@ -117,10 +117,13 @@ function AclRulesTable() { setIsDialogOpen(true); }; - const handleDelete = async (id: string) => { + const handleDelete = async () => { + if (!ruleToDelete) return; + try { - await deleteAclRule.mutateAsync(id); + await deleteAclRule.mutateAsync(ruleToDelete.id); toast({ title: "Rule deleted successfully" }); + setRuleToDelete(null); } catch (error: any) { toast({ title: "Error deleting rule", @@ -365,7 +368,11 @@ function AclRulesTable() { - @@ -376,6 +383,24 @@ function AclRulesTable() { + + !open && setRuleToDelete(null)} + title="Delete ACL Rule" + description={ + <> + Are you sure you want to delete the rule {ruleToDelete?.name}? +
+ This action cannot be undone and may affect access control to your domains. + + } + confirmText="Delete Rule" + cancelText="Cancel" + onConfirm={handleDelete} + isLoading={deleteAclRule.isPending} + variant="destructive" + /> ); } diff --git a/apps/web/src/components/pages/Alerts.tsx b/apps/web/src/components/pages/Alerts.tsx index 50ba30c..8388def 100644 --- a/apps/web/src/components/pages/Alerts.tsx +++ b/apps/web/src/components/pages/Alerts.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; import { Suspense } from "react"; -import { useTranslation } from "react-i18next"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -11,10 +10,10 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Plus, Send, Edit, Trash2, Mail, MessageSquare, Loader2, Bell } from "lucide-react"; -import { NotificationChannel, AlertRule } from "@/types"; +import { Plus, Send, Trash2, Mail, MessageSquare, Loader2, Bell } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { SkeletonTable } from "@/components/ui/skeletons"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { useSuspenseNotificationChannels, useSuspenseAlertRules, @@ -29,10 +28,10 @@ import { // Component for notification channels with suspense function NotificationChannelsTab() { - const { t } = useTranslation(); const { toast } = useToast(); const { data: channels } = useSuspenseNotificationChannels(); const [isChannelDialogOpen, setIsChannelDialogOpen] = useState(false); + const [channelToDelete, setChannelToDelete] = useState<{ id: string; name: string } | null>(null); const createNotificationChannel = useCreateNotificationChannel(); const updateNotificationChannel = useUpdateNotificationChannel(); @@ -101,10 +100,13 @@ function NotificationChannelsTab() { } }; - const handleDeleteChannel = async (id: string) => { + const handleDeleteChannel = async () => { + if (!channelToDelete) return; + try { - await deleteNotificationChannel.mutateAsync(id); + await deleteNotificationChannel.mutateAsync(channelToDelete.id); toast({ title: "Channel deleted successfully" }); + setChannelToDelete(null); } catch (error: any) { toast({ title: "Error deleting channel", @@ -263,7 +265,11 @@ function NotificationChannelsTab() { - @@ -272,6 +278,24 @@ function NotificationChannelsTab() { + + !open && setChannelToDelete(null)} + title="Delete Notification Channel" + description={ + <> + Are you sure you want to delete the channel {channelToDelete?.name}? +
+ This action cannot be undone and all associated alert rules will be affected. + + } + confirmText="Delete Channel" + cancelText="Cancel" + onConfirm={handleDeleteChannel} + isLoading={deleteNotificationChannel.isPending} + variant="destructive" + /> ); @@ -279,11 +303,11 @@ function NotificationChannelsTab() { // Component for alert rules with suspense function AlertRulesTab() { - const { t } = useTranslation(); const { toast } = useToast(); const { data: channels } = useSuspenseNotificationChannels(); const { data: alertRules } = useSuspenseAlertRules(); const [isRuleDialogOpen, setIsRuleDialogOpen] = useState(false); + const [ruleToDelete, setRuleToDelete] = useState<{ id: string; name: string } | null>(null); const createAlertRule = useCreateAlertRule(); const updateAlertRule = useUpdateAlertRule(); @@ -390,10 +414,13 @@ function AlertRulesTab() { }); }; - const handleDeleteRule = async (id: string) => { + const handleDeleteRule = async () => { + if (!ruleToDelete) return; + try { - await deleteAlertRule.mutateAsync(id); + await deleteAlertRule.mutateAsync(ruleToDelete.id); toast({ title: "Rule deleted successfully" }); + setRuleToDelete(null); } catch (error: any) { toast({ title: "Error deleting rule", @@ -624,7 +651,11 @@ function AlertRulesTab() { /> - @@ -633,6 +664,24 @@ function AlertRulesTab() { + + !open && setRuleToDelete(null)} + title="Delete Alert Rule" + description={ + <> + Are you sure you want to delete the rule {ruleToDelete?.name}? +
+ This action cannot be undone and you will stop receiving alerts for this condition. + + } + confirmText="Delete Rule" + cancelText="Cancel" + onConfirm={handleDeleteRule} + isLoading={deleteAlertRule.isPending} + variant="destructive" + /> ); diff --git a/apps/web/src/components/pages/SSLTable.tsx b/apps/web/src/components/pages/SSLTable.tsx index 2e796b4..c99a950 100644 --- a/apps/web/src/components/pages/SSLTable.tsx +++ b/apps/web/src/components/pages/SSLTable.tsx @@ -14,13 +14,26 @@ import { } from '@/components/ui/table'; import { AlertTriangle, CheckCircle2, RefreshCw, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; -import { SSLCertificate } from '@/types'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; export function SSLTable() { const [renewingId, setRenewingId] = useState(null); const { data: certificates } = useSuspenseSSLCertificates(); const renewMutation = useRenewSSLCertificate(); const deleteMutation = useDeleteSSLCertificate(); + const [confirmDialog, setConfirmDialog] = useState<{ + open: boolean; + title: string; + description: string; + onConfirm: () => void; + certificateId: string; + }>({ + open: false, + title: '', + description: '', + onConfirm: () => {}, + certificateId: '', + }); const handleRenew = async (id: string) => { try { @@ -34,15 +47,22 @@ export function SSLTable() { } }; - const handleDelete = async (id: string) => { - if (!confirm('Are you sure you want to delete this certificate?')) return; - - try { - await deleteMutation.mutateAsync(id); - toast.success('Certificate deleted successfully'); - } catch (error: any) { - toast.error(error.response?.data?.message || 'Failed to delete certificate'); - } + const handleDelete = (id: string, domainName: string) => { + setConfirmDialog({ + open: true, + title: 'Delete SSL Certificate', + description: `Are you sure you want to delete the SSL certificate for ${domainName}? This action cannot be undone.`, + certificateId: id, + onConfirm: async () => { + try { + await deleteMutation.mutateAsync(id); + toast.success('Certificate deleted successfully'); + setConfirmDialog(prev => ({ ...prev, open: false })); + } catch (error: any) { + toast.error(error.response?.data?.message || 'Failed to delete certificate'); + } + }, + }); }; const getStatusIcon = (status: string) => { @@ -145,7 +165,7 @@ export function SSLTable() { @@ -158,6 +178,16 @@ export function SSLTable() { )} + + setConfirmDialog(prev => ({ ...prev, open }))} + title={confirmDialog.title} + description={confirmDialog.description} + onConfirm={confirmDialog.onConfirm} + confirmText="Delete" + isLoading={deleteMutation.isPending} + /> ); } \ No newline at end of file diff --git a/apps/web/src/components/pages/Users.tsx b/apps/web/src/components/pages/Users.tsx index 86f9189..b007d7e 100644 --- a/apps/web/src/components/pages/Users.tsx +++ b/apps/web/src/components/pages/Users.tsx @@ -14,6 +14,7 @@ import { UserPlus, Key, Trash2, Edit, Shield, Loader2, Users as UsersIcon, Copy, import { useToast } from "@/hooks/use-toast"; import { useStore } from "@/store/useStore"; import { SkeletonStatsCard, SkeletonTable } from "@/components/ui/skeletons"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { useSuspenseUsers, useSuspenseUserStats, @@ -81,6 +82,11 @@ function UsersTable() { const [editingUser, setEditingUser] = useState(null); const [resetPasswordDialog, setResetPasswordDialog] = useState<{ isOpen: boolean; userId: string; username: string; newPassword?: string; }>({ isOpen: false, userId: '', username: '' }); const [passwordCopied, setPasswordCopied] = useState(false); + const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; userId: string; username: string }>({ + isOpen: false, + userId: '', + username: '' + }); const createUser = useCreateUser(); const updateUser = useUpdateUser(); @@ -190,10 +196,16 @@ function UsersTable() { }; const handleDelete = async (id: string) => { - if (!confirm("Are you sure you want to delete this user?")) return; + const user = users.data.find(u => u.id === id); + if (!user) return; + + setDeleteConfirm({ isOpen: true, userId: id, username: user.username }); + }; + const confirmDelete = async () => { try { - await deleteUser.mutateAsync(id); + await deleteUser.mutateAsync(deleteConfirm.userId); + setDeleteConfirm({ isOpen: false, userId: '', username: '' }); toast({ title: "User deleted successfully" }); } catch (error: any) { toast({ @@ -713,6 +725,26 @@ function UsersTable() { + + {/* Delete User Confirmation Dialog */} + !open && setDeleteConfirm({ isOpen: false, userId: '', username: '' })} + title="Delete User" + description={ +
+

Are you sure you want to delete user {deleteConfirm.username}?

+

+ This action cannot be undone. All data associated with this user will be permanently removed. +

+
+ } + confirmText="Delete User" + cancelText="Cancel" + onConfirm={confirmDelete} + isLoading={deleteUser.isPending} + variant="destructive" + /> ); } diff --git a/apps/web/src/components/ui/confirm-dialog.tsx b/apps/web/src/components/ui/confirm-dialog.tsx index 0fbb828..27792ce 100644 --- a/apps/web/src/components/ui/confirm-dialog.tsx +++ b/apps/web/src/components/ui/confirm-dialog.tsx @@ -8,16 +8,18 @@ import { DialogTitle, } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" +import { Loader2 } from "lucide-react" interface ConfirmDialogProps { open: boolean onOpenChange: (open: boolean) => void title: string - description: string + description: React.ReactNode confirmText?: string cancelText?: string onConfirm: () => void isLoading?: boolean + variant?: "default" | "destructive" } export function ConfirmDialog({ @@ -29,6 +31,7 @@ export function ConfirmDialog({ cancelText = "Cancel", onConfirm, isLoading = false, + variant = "default", }: ConfirmDialogProps) { return ( @@ -45,7 +48,12 @@ export function ConfirmDialog({ > {cancelText} -