mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
fix(ui): enforce 100-char limit on mute rule name input (#11158)
This commit is contained in:
committed by
GitHub
parent
68ffb2b219
commit
9bd4e4b65c
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user