fix(ui): enforce 100-char limit on mute rule name input (#11158)

This commit is contained in:
Hugo Pereira Brito
2026-05-14 09:13:36 +01:00
committed by GitHub
parent 68ffb2b219
commit 9bd4e4b65c
4 changed files with 84 additions and 8 deletions
+4
View File
@@ -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)
@@ -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(
<MuteFindingsModal
isOpen
onOpenChange={vi.fn()}
findingIds={["finding-1"]}
/>,
);
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();
});
});
+36 -8
View File
@@ -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<MuteRuleActionState | null>(null);
const [name, setName] = useState("");
const [nameLengthError, setNameLengthError] = useState<string>();
const [reason, setReason] = useState("");
const [reasonLengthError, setReasonLengthError] = useState<string>();
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<HTMLInputElement>) => {
const nextName = enforceMuteRuleNameLimit(event.target.value);
setName(nextName.value);
setNameLengthError(nextName.error);
};
const handleReasonChange = (
event: React.ChangeEvent<HTMLTextAreaElement>,
) => {
@@ -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"
}
/>
<p
id="mute-rule-name-description"
className="text-text-neutral-tertiary text-xs"
>
A descriptive name for this mute rule
</p>
<div className="flex items-center justify-between gap-3">
<p
id="mute-rule-name-description"
className="text-text-neutral-tertiary text-xs"
>
A descriptive name for this mute rule
</p>
<p className="text-text-neutral-tertiary shrink-0 text-xs">
{getMuteRuleNameCounterText(name)}
</p>
</div>
{nameError ? (
<p
id="mute-rule-name-error"
@@ -250,7 +278,7 @@ export function MuteFindingsModal({
value={reason}
onChange={handleReasonChange}
rows={4}
maxLength={500}
maxLength={MAX_MUTE_RULE_REASON_LENGTH}
aria-invalid={reasonError ? "true" : "false"}
aria-describedby={
reasonError
+20
View File
@@ -1,11 +1,31 @@
export const MAX_MUTE_RULE_NAME_LENGTH = 100;
export const MAX_MUTE_RULE_REASON_LENGTH = 500;
export const MUTE_RULE_NAME_TOO_LONG_MESSAGE = `Name must be ${MAX_MUTE_RULE_NAME_LENGTH} characters or fewer`;
export const MUTE_RULE_REASON_TOO_LONG_MESSAGE = `Reason must be ${MAX_MUTE_RULE_REASON_LENGTH} characters or fewer`;
export function getMuteRuleNameCounterText(name: string): string {
return `${name.length}/${MAX_MUTE_RULE_NAME_LENGTH} characters`;
}
export function getMuteRuleReasonCounterText(reason: string): string {
return `${reason.length}/${MAX_MUTE_RULE_REASON_LENGTH} characters`;
}
export function enforceMuteRuleNameLimit(name: string): {
value: string;
error?: string;
} {
if (name.length <= MAX_MUTE_RULE_NAME_LENGTH) {
return { value: name };
}
return {
value: name.slice(0, MAX_MUTE_RULE_NAME_LENGTH),
error: MUTE_RULE_NAME_TOO_LONG_MESSAGE,
};
}
export function enforceMuteRuleReasonLimit(reason: string): {
value: string;
error?: string;