From 262dfda0aab191d13cb125cda5c94188635c0565 Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:44:48 +0200 Subject: [PATCH] fix(ui): handle alert form errors (#11623) --- .../__tests__/alert-form-modal.test.tsx | 25 +++++++ .../__tests__/alerts-manager.test.tsx | 73 ++++++++++++++++++- .../alerts/_components/alert-form-modal.tsx | 1 + .../alerts/_components/alerts-manager.tsx | 9 ++- 4 files changed, 103 insertions(+), 5 deletions(-) diff --git a/ui/app/(prowler)/alerts/_components/__tests__/alert-form-modal.test.tsx b/ui/app/(prowler)/alerts/_components/__tests__/alert-form-modal.test.tsx index be336be2a7..f23fafb948 100644 --- a/ui/app/(prowler)/alerts/_components/__tests__/alert-form-modal.test.tsx +++ b/ui/app/(prowler)/alerts/_components/__tests__/alert-form-modal.test.tsx @@ -701,6 +701,31 @@ describe("AlertFormModal", () => { expect(errorMessage).toHaveClass("text-text-error-primary"); }); + it("should clear the preview error when save shows the form error", async () => { + // Given + const user = userEvent.setup(); + alertsActionMocks.seedAlertRule.mockResolvedValue({ + error: "No alert-compatible filters", + }); + mockRecipientsList(); + renderCreateModal({ editingAlert: createEditingAlert() }); + + // When + await user.click(screen.getByRole("button", { name: /^test$/i })); + expect(await screen.findByText("Test result")).toBeVisible(); + await user.click(screen.getByRole("button", { name: /^save$/i })); + + // Then + await waitFor(() => + expect(screen.queryByText("Test result")).not.toBeInTheDocument(), + ); + expect( + screen.getAllByText( + "Apply at least one alert-compatible Findings filter.", + ), + ).toHaveLength(1); + }); + it("should hydrate advanced edit mode filters and normalize them on save", async () => { // Given const user = userEvent.setup(); diff --git a/ui/app/(prowler)/alerts/_components/__tests__/alerts-manager.test.tsx b/ui/app/(prowler)/alerts/_components/__tests__/alerts-manager.test.tsx index 1ee2c67302..0230dccfa3 100644 --- a/ui/app/(prowler)/alerts/_components/__tests__/alerts-manager.test.tsx +++ b/ui/app/(prowler)/alerts/_components/__tests__/alerts-manager.test.tsx @@ -1,6 +1,6 @@ import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { isValidElement, type ReactNode } from "react"; +import { isValidElement, type ReactNode, useState } from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { @@ -8,6 +8,10 @@ import { ALERT_TRIGGER_KINDS, type AlertRule, } from "@/app/(prowler)/alerts/_types"; +import type { + AlertFormSubmitResult, + AlertFormValues, +} from "@/app/(prowler)/alerts/_types/alert-form"; import { AlertsManager } from "../alerts-manager"; @@ -96,12 +100,32 @@ vi.mock("../alert-form-modal", () => ({ open, editingAlert, onOpenChange, + onSubmit, }: { open: boolean; editingAlert?: AlertRule | null; onOpenChange: (open: boolean) => void; - }) => - open ? ( + onSubmit: (values: AlertFormValues) => Promise; + }) => { + const [error, setError] = useState(null); + + const submit = async () => { + const result = await onSubmit({ + name: "Updated alert", + description: "", + method: "email", + frequency: ALERT_TRIGGER_KINDS.AFTER_SCAN, + condition: { + op: ALERT_AGGREGATE_OPS.ANY, + filter: { severity: ["critical"] }, + }, + recipientEmails: [], + enabled: true, + }); + setError(result.ok ? null : (result.error ?? null)); + }; + + return open ? (
({ + {editingAlert?.attributes.name} + {error &&

{error}

}
- ) : null, + ) : null; + }, })); vi.mock("../alerts-empty-state", () => ({ @@ -259,6 +288,42 @@ describe("AlertsManager", () => { }); }); + it("shows a manage alerts permission message for edit 403 errors", async () => { + // Given + const user = userEvent.setup(); + const alert = makeAlert(true); + actionMocks.updateAlert.mockResolvedValue({ + error: "You do not have permission to perform this action.", + status: 403, + }); + render( + , + ); + + // When + await user.click(screen.getByRole("button", { name: /submit alert/i })); + + // Then + expect( + await screen.findByText( + "You don't have permission to manage alerts. Ask an administrator to update your role.", + ), + ).toBeVisible(); + expect(toastMock).not.toHaveBeenCalled(); + }); + it("shows a success toast after disabling an alert", async () => { // Given const user = userEvent.setup(); diff --git a/ui/app/(prowler)/alerts/_components/alert-form-modal.tsx b/ui/app/(prowler)/alerts/_components/alert-form-modal.tsx index e3529d4056..12d7a85df9 100644 --- a/ui/app/(prowler)/alerts/_components/alert-form-modal.tsx +++ b/ui/app/(prowler)/alerts/_components/alert-form-modal.tsx @@ -451,6 +451,7 @@ const AlertFormModalContent = ({ ? await seedAlertRule(pendingFilters) : null; if (seedResult?.error) { + setPreview(null); setErrors({ root: ALERT_SEED_ERROR }); return; } diff --git a/ui/app/(prowler)/alerts/_components/alerts-manager.tsx b/ui/app/(prowler)/alerts/_components/alerts-manager.tsx index 0c69225b5e..e438727c6f 100644 --- a/ui/app/(prowler)/alerts/_components/alerts-manager.tsx +++ b/ui/app/(prowler)/alerts/_components/alerts-manager.tsx @@ -49,6 +49,8 @@ interface AlertsManagerProps { const ALERTS_FINDINGS_HREF = "/findings?filter[muted]=false&filter[status__in]=FAIL"; +const ALERTS_PERMISSION_ERROR = + "You don't have permission to manage alerts. Ask an administrator to update your role."; export const AlertsManager = ({ alerts, @@ -108,7 +110,12 @@ export const AlertsManager = ({ } const payload = toAlertPayload(values); const result = await updateAlert(editingAlert.id, payload); - if (result?.error) return { ok: false, error: result.error }; + if (result?.error) { + return { + ok: false, + error: result.status === 403 ? ALERTS_PERMISSION_ERROR : result.error, + }; + } toast({ title: "Alert updated", description: result.data.attributes.name,