diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 2557a69939..6d8e04cb63 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -15,6 +15,10 @@ All notable changes to the **Prowler UI** are documented in this file. - Attack Paths graph now uses React Flow with improved layout, interactions, export, minimap, and browser test coverage [(#10686)](https://github.com/prowler-cloud/prowler/pull/10686) - SAML ACS URL is only shown if the email domain is configured [(#11144)](https://github.com/prowler-cloud/prowler/pull/11144) +### 🐞 Fixed + +- Mute Findings modal now enforces the 100-character limit on the rule name input with a live counter and inline error, matching the existing reason field behaviour [(#11158)](https://github.com/prowler-cloud/prowler/pull/11158) + --- ## [1.26.2] (Prowler 5.26.2) diff --git a/ui/components/findings/mute-findings-modal.test.tsx b/ui/components/findings/mute-findings-modal.test.tsx index 96afa77a4e..7f323da7cc 100644 --- a/ui/components/findings/mute-findings-modal.test.tsx +++ b/ui/components/findings/mute-findings-modal.test.tsx @@ -106,6 +106,11 @@ describe("MuteFindingsModal", () => { expect( screen.getByText("Explain why these findings are being muted"), ).toBeInTheDocument(); + expect(screen.getByText("0/100 characters")).toBeInTheDocument(); + expect(screen.getByLabelText("Rule Name")).toHaveAttribute( + "maxLength", + "100", + ); expect(screen.getByText("0/500 characters")).toBeInTheDocument(); expect(screen.getByLabelText("Reason")).toHaveAttribute("maxLength", "500"); }); @@ -183,4 +188,23 @@ describe("MuteFindingsModal", () => { screen.getByText("Reason must be 500 characters or fewer"), ).toBeInTheDocument(); }); + + it("clamps oversized rule name input and shows a local validation error", () => { + render( + , + ); + + fireEvent.change(screen.getByLabelText("Rule Name"), { + target: { value: "a".repeat(101) }, + }); + + expect(screen.getByText("100/100 characters")).toBeInTheDocument(); + expect( + screen.getByText("Name must be 100 characters or fewer"), + ).toBeInTheDocument(); + }); }); diff --git a/ui/components/findings/mute-findings-modal.tsx b/ui/components/findings/mute-findings-modal.tsx index dd339613e2..999f0dead9 100644 --- a/ui/components/findings/mute-findings-modal.tsx +++ b/ui/components/findings/mute-findings-modal.tsx @@ -11,8 +11,12 @@ import { FormButtons } from "@/components/ui/form"; import { Label } from "@/components/ui/form/Label"; import { useMuteRuleAction } from "@/hooks/use-mute-rule-action"; import { + enforceMuteRuleNameLimit, enforceMuteRuleReasonLimit, + getMuteRuleNameCounterText, getMuteRuleReasonCounterText, + MAX_MUTE_RULE_NAME_LENGTH, + MAX_MUTE_RULE_REASON_LENGTH, } from "@/lib/mute-rules"; interface MuteFindingsModalProps { @@ -35,6 +39,8 @@ export function MuteFindingsModal({ preparationError = null, }: MuteFindingsModalProps) { const [state, setState] = useState(null); + const [name, setName] = useState(""); + const [nameLengthError, setNameLengthError] = useState(); const [reason, setReason] = useState(""); const [reasonLengthError, setReasonLengthError] = useState(); const { isPending, runAction } = useMuteRuleAction(); @@ -48,9 +54,16 @@ export function MuteFindingsModal({ isPreparing || findingIds.length === 0 || Boolean(preparationError); - const nameError = state?.errors?.name; + const nameError = nameLengthError || state?.errors?.name; const reasonError = reasonLengthError || state?.errors?.reason; + const handleNameChange = (event: React.ChangeEvent) => { + const nextName = enforceMuteRuleNameLimit(event.target.value); + + setName(nextName.value); + setNameLengthError(nextName.error); + }; + const handleReasonChange = ( event: React.ChangeEvent, ) => { @@ -77,8 +90,15 @@ export function MuteFindingsModal({ } const formData = new FormData(e.currentTarget); + formData.set("name", name); formData.set("reason", reason); + const nextName = enforceMuteRuleNameLimit(name); + if (nextName.error) { + setNameLengthError(nextName.error); + return; + } + const nextReason = enforceMuteRuleReasonLimit(reason); if (nextReason.error) { setReasonLengthError(nextReason.error); @@ -211,6 +231,9 @@ export function MuteFindingsModal({ placeholder="e.g., Ignore dev environment S3 buckets" required disabled={isPending} + value={name} + onChange={handleNameChange} + maxLength={MAX_MUTE_RULE_NAME_LENGTH} aria-invalid={nameError ? "true" : "false"} aria-describedby={ nameError @@ -218,12 +241,17 @@ export function MuteFindingsModal({ : "mute-rule-name-description" } /> -

- A descriptive name for this mute rule -

+
+

+ A descriptive name for this mute rule +

+

+ {getMuteRuleNameCounterText(name)} +

+
{nameError ? (