diff --git a/server/routers/ruleTemplate/deleteRuleTemplate.ts b/server/routers/ruleTemplate/deleteRuleTemplate.ts index 6d37095b..f064da43 100644 --- a/server/routers/ruleTemplate/deleteRuleTemplate.ts +++ b/server/routers/ruleTemplate/deleteRuleTemplate.ts @@ -31,7 +31,26 @@ export async function deleteRuleTemplate(req: any, res: any) { }); } - // Delete template rules first (due to foreign key constraint) + // Get all template rules for this template + const templateRulesToDelete = await db + .select({ ruleId: templateRules.ruleId }) + .from(templateRules) + .where(eq(templateRules.templateId, templateId)); + + // Delete resource rules that reference these template rules first + if (templateRulesToDelete.length > 0) { + const { resourceRules } = await import("@server/db"); + const templateRuleIds = templateRulesToDelete.map(rule => rule.ruleId); + + // Delete all resource rules that reference any of the template rules + for (const ruleId of templateRuleIds) { + await db + .delete(resourceRules) + .where(eq(resourceRules.templateRuleId, ruleId)); + } + } + + // Delete template rules await db .delete(templateRules) .where(eq(templateRules.templateId, templateId)); diff --git a/server/routers/ruleTemplate/deleteTemplateRule.ts b/server/routers/ruleTemplate/deleteTemplateRule.ts index 3eda8eac..9de0dfc4 100644 --- a/server/routers/ruleTemplate/deleteTemplateRule.ts +++ b/server/routers/ruleTemplate/deleteTemplateRule.ts @@ -67,15 +67,20 @@ export async function deleteTemplateRule( ); } - // Delete the rule - await db - .delete(templateRules) - .where(and(eq(templateRules.templateId, templateId), eq(templateRules.ruleId, parseInt(ruleId)))); - - // Also delete all resource rules that were created from this template rule + // Count affected resources for the response message + let affectedResourcesCount = 0; try { const { resourceRules } = await import("@server/db"); + // Get affected resource rules before deletion for counting + const affectedResourceRules = await db + .select() + .from(resourceRules) + .where(eq(resourceRules.templateRuleId, parseInt(ruleId))); + + affectedResourcesCount = affectedResourceRules.length; + + // Delete the resource rules first (due to foreign key constraint) await db .delete(resourceRules) .where(eq(resourceRules.templateRuleId, parseInt(ruleId))); @@ -84,11 +89,20 @@ export async function deleteTemplateRule( // Don't fail the template rule deletion if resource rule deletion fails, just log it } + // Delete the template rule after resource rules are deleted + await db + .delete(templateRules) + .where(and(eq(templateRules.templateId, templateId), eq(templateRules.ruleId, parseInt(ruleId)))); + + const message = affectedResourcesCount > 0 + ? `Template rule deleted successfully. Removed from ${affectedResourcesCount} assigned resource${affectedResourcesCount > 1 ? 's' : ''}.` + : "Template rule deleted successfully."; + return response(res, { data: null, success: true, error: false, - message: "Template rule deleted successfully", + message, status: HttpCode.OK }); } catch (error) { diff --git a/server/routers/ruleTemplate/getRuleTemplate.ts b/server/routers/ruleTemplate/getRuleTemplate.ts index ec1a83ac..c16550b7 100644 --- a/server/routers/ruleTemplate/getRuleTemplate.ts +++ b/server/routers/ruleTemplate/getRuleTemplate.ts @@ -9,6 +9,14 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; +export type GetRuleTemplateResponse = { + templateId: string; + orgId: string; + name: string; + description: string | null; + createdAt: number; +}; + const getRuleTemplateParamsSchema = z .object({ orgId: z.string().min(1), diff --git a/server/routers/ruleTemplate/updateTemplateRule.ts b/server/routers/ruleTemplate/updateTemplateRule.ts index 6a964760..2183a743 100644 --- a/server/routers/ruleTemplate/updateTemplateRule.ts +++ b/server/routers/ruleTemplate/updateTemplateRule.ts @@ -144,8 +144,8 @@ export async function updateTemplateRule( // Remove undefined values Object.keys(propagationData).forEach(key => { - if (propagationData[key] === undefined) { - delete propagationData[key]; + if ((propagationData as any)[key] === undefined) { + delete (propagationData as any)[key]; } }); @@ -161,11 +161,28 @@ export async function updateTemplateRule( // Don't fail the template rule update if propagation fails, just log it } + // Count affected resources for the response message + let affectedResourcesCount = 0; + try { + const { resourceTemplates } = await import("@server/db"); + const affectedResources = await db + .select() + .from(resourceTemplates) + .where(eq(resourceTemplates.templateId, templateId)); + affectedResourcesCount = affectedResources.length; + } catch (error) { + logger.error("Error counting affected resources:", error); + } + + const message = affectedResourcesCount > 0 + ? `Template rule updated successfully. Changes propagated to ${affectedResourcesCount} assigned resource${affectedResourcesCount > 1 ? 's' : ''}.` + : "Template rule updated successfully."; + return response(res, { data: updatedRule, success: true, error: false, - message: "Template rule updated successfully", + message, status: HttpCode.OK }); } catch (error) { diff --git a/src/app/[orgId]/settings/rule-templates/[templateId]/general/page.tsx b/src/app/[orgId]/settings/rule-templates/[templateId]/general/page.tsx index c11eae84..304df96a 100644 --- a/src/app/[orgId]/settings/rule-templates/[templateId]/general/page.tsx +++ b/src/app/[orgId]/settings/rule-templates/[templateId]/general/page.tsx @@ -12,9 +12,9 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { SettingsContainer, SettingsSection, - SettingsSectionHeader, - SettingsSectionTitle + SettingsSectionHeader } from "@app/components/Settings"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { Button } from "@app/components/ui/button"; import { Input } from "@app/components/ui/input"; import { Textarea } from "@app/components/ui/textarea"; @@ -79,8 +79,9 @@ export default function GeneralPage() { data ); toast({ - title: t("ruleTemplateUpdated"), - description: t("ruleTemplateUpdatedDescription") + title: "Template Updated", + description: "Template details have been updated successfully. Changes to template rules will automatically propagate to all assigned resources.", + variant: "default" }); } catch (error) { toast({ diff --git a/src/app/[orgId]/settings/rule-templates/[templateId]/rules/page.tsx b/src/app/[orgId]/settings/rule-templates/[templateId]/rules/page.tsx index ba4bee6f..3beb9f18 100644 --- a/src/app/[orgId]/settings/rule-templates/[templateId]/rules/page.tsx +++ b/src/app/[orgId]/settings/rule-templates/[templateId]/rules/page.tsx @@ -4,9 +4,9 @@ import { useParams } from "next/navigation"; import { SettingsContainer, SettingsSection, - SettingsSectionHeader, - SettingsSectionTitle + SettingsSectionHeader } from "@app/components/Settings"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { TemplateRulesManager } from "@app/components/ruleTemplate/TemplateRulesManager"; export default function RulesPage() { diff --git a/src/components/ConfirmationDialog.tsx b/src/components/ConfirmationDialog.tsx new file mode 100644 index 00000000..476c5697 --- /dev/null +++ b/src/components/ConfirmationDialog.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from "@app/components/ui/dialog"; +import { Button } from "@app/components/ui/button"; +import { AlertTriangle } from "lucide-react"; + +interface ConfirmationDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description: string; + confirmText?: string; + cancelText?: string; + variant?: "destructive" | "default"; + onConfirm: () => Promise | void; + loading?: boolean; +} + +export function ConfirmationDialog({ + open, + onOpenChange, + title, + description, + confirmText = "Confirm", + cancelText = "Cancel", + variant = "destructive", + onConfirm, + loading = false +}: ConfirmationDialogProps) { + const handleConfirm = async () => { + try { + await onConfirm(); + onOpenChange(false); + } catch (error) { + // Error handling is done by the calling component + console.error("Confirmation action failed:", error); + } + }; + + return ( + + + + + + {title} + + + {description} + + + + + + + + + ); +} diff --git a/src/components/ruleTemplate/ResourceRulesManager.tsx b/src/components/ruleTemplate/ResourceRulesManager.tsx index f43d2c22..899553cd 100644 --- a/src/components/ruleTemplate/ResourceRulesManager.tsx +++ b/src/components/ruleTemplate/ResourceRulesManager.tsx @@ -8,6 +8,7 @@ import { useToast } from "@app/hooks/useToast"; import { Trash2 } from "lucide-react"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { ConfirmationDialog } from "@app/components/ConfirmationDialog"; interface RuleTemplate { templateId: string; @@ -38,6 +39,9 @@ export function ResourceRulesManager({ const [resourceTemplates, setResourceTemplates] = useState([]); const [loading, setLoading] = useState(true); const [selectedTemplate, setSelectedTemplate] = useState(""); + const [unassignDialogOpen, setUnassignDialogOpen] = useState(false); + const [templateToUnassign, setTemplateToUnassign] = useState(null); + const [unassigning, setUnassigning] = useState(false); const { toast } = useToast(); const { env } = useEnvContext(); const api = createApiClient({ env }); @@ -79,8 +83,9 @@ export function ResourceRulesManager({ if (response.status === 200 || response.status === 201) { toast({ - title: "Success", - description: "Template assigned successfully" + title: "Template Assigned", + description: "Template has been assigned to this resource. All template rules have been applied and will be automatically updated when the template changes.", + variant: "default" }); setSelectedTemplate(""); @@ -105,17 +110,22 @@ export function ResourceRulesManager({ }; const handleUnassignTemplate = async (templateId: string) => { - if (!confirm("Are you sure you want to unassign this template?")) { - return; - } + setTemplateToUnassign(templateId); + setUnassignDialogOpen(true); + }; + const confirmUnassignTemplate = async () => { + if (!templateToUnassign) return; + + setUnassigning(true); try { - const response = await api.delete(`/resource/${resourceId}/templates/${templateId}`); + const response = await api.delete(`/resource/${resourceId}/templates/${templateToUnassign}`); if (response.status === 200 || response.status === 201) { toast({ - title: "Success", - description: "Template unassigned successfully" + title: "Template Unassigned", + description: "Template has been unassigned from this resource. All template-managed rules have been removed from this resource.", + variant: "default" }); await fetchData(); @@ -124,17 +134,20 @@ export function ResourceRulesManager({ } } else { toast({ - title: "Error", - description: response.data.message || "Failed to unassign template", + title: "Unassign Failed", + description: response.data.message || "Failed to unassign template. Please try again.", variant: "destructive" }); } } catch (error) { toast({ - title: "Error", - description: formatAxiosError(error, "Failed to unassign template"), + title: "Unassign Failed", + description: formatAxiosError(error, "Failed to unassign template. Please try again."), variant: "destructive" }); + } finally { + setUnassigning(false); + setTemplateToUnassign(null); } }; @@ -199,6 +212,18 @@ export function ResourceRulesManager({ )} + + ); } diff --git a/src/components/ruleTemplate/TemplateRulesManager.tsx b/src/components/ruleTemplate/TemplateRulesManager.tsx index 578b5d78..16dffd1f 100644 --- a/src/components/ruleTemplate/TemplateRulesManager.tsx +++ b/src/components/ruleTemplate/TemplateRulesManager.tsx @@ -46,6 +46,7 @@ import { } from "@app/components/ui/table"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "@server/lib/validators"; import { ArrowUpDown, Trash2, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react"; +import { ConfirmationDialog } from "@app/components/ConfirmationDialog"; const addRuleSchema = z.object({ action: z.enum(["ACCEPT", "DROP"]), @@ -79,6 +80,9 @@ export function TemplateRulesManager({ templateId, orgId }: TemplateRulesManager pageIndex: 0, pageSize: 25 }); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [ruleToDelete, setRuleToDelete] = useState(null); + const [deletingRule, setDeletingRule] = useState(false); const RuleAction = { ACCEPT: t('alwaysAllow'), @@ -151,18 +155,19 @@ export function TemplateRulesManager({ templateId, orgId }: TemplateRulesManager return; } - await api.post(`/org/${orgId}/rule-templates/${templateId}/rules`, data); + const response = await api.post(`/org/${orgId}/rule-templates/${templateId}/rules`, data); toast({ - title: "Success", - description: "Rule added successfully" + title: "Template Rule Added", + description: "A new rule has been added to the template. It will be available for assignment to resources.", + variant: "default" }); form.reset(); fetchRules(); } catch (error) { toast({ variant: "destructive", - title: "Error", - description: formatAxiosError(error, "Failed to add rule") + title: "Add Rule Failed", + description: formatAxiosError(error, "Failed to add rule. Please check your input and try again.") }); } finally { setAddingRule(false); @@ -170,28 +175,54 @@ export function TemplateRulesManager({ templateId, orgId }: TemplateRulesManager }; const removeRule = async (ruleId: number) => { + setRuleToDelete(ruleId); + setDeleteDialogOpen(true); + }; + + const confirmDeleteRule = async () => { + if (!ruleToDelete) return; + + setDeletingRule(true); try { - await api.delete(`/org/${orgId}/rule-templates/${templateId}/rules/${ruleId}`); + await api.delete(`/org/${orgId}/rule-templates/${templateId}/rules/${ruleToDelete}`); toast({ - title: "Success", - description: "Rule removed successfully" + title: "Template Rule Removed", + description: "The rule has been removed from the template and from all assigned resources.", + variant: "default" }); fetchRules(); } catch (error) { toast({ variant: "destructive", - title: "Error", - description: formatAxiosError(error, "Failed to remove rule") + title: "Removal Failed", + description: formatAxiosError(error, "Failed to remove template rule") }); + } finally { + setDeletingRule(false); + setRuleToDelete(null); } }; const updateRule = async (ruleId: number, data: Partial) => { try { - await api.put(`/org/${orgId}/rule-templates/${templateId}/rules/${ruleId}`, data); + const response = await api.put(`/org/${orgId}/rule-templates/${templateId}/rules/${ruleId}`, data); + + // Show success notification with propagation info if available + const message = response.data?.message || "The template rule has been updated and changes have been propagated to all assigned resources."; + toast({ + title: "Template Rule Updated", + description: message, + variant: "default" + }); + fetchRules(); } catch (error) { console.error("Failed to update rule:", error); + toast({ + title: "Update Failed", + description: formatAxiosError(error, "Failed to update template rule. Please try again."), + variant: "destructive" + }); } }; @@ -348,7 +379,7 @@ export function TemplateRulesManager({ templateId, orgId }: TemplateRulesManager name="action" render={({ field }) => ( - Action + {t('rulesAction')} @@ -373,7 +406,7 @@ export function TemplateRulesManager({ templateId, orgId }: TemplateRulesManager name="match" render={({ field }) => ( - Match + {t('rulesMatchType')} @@ -399,7 +432,7 @@ export function TemplateRulesManager({ templateId, orgId }: TemplateRulesManager name="value" render={({ field }) => ( - Value + {t('value')} @@ -413,7 +446,7 @@ export function TemplateRulesManager({ templateId, orgId }: TemplateRulesManager name="priority" render={({ field }) => ( - Priority (optional) + {t('rulesPriority')} (optional) )} + + {/* Confirmation Dialog */} + ); }