fix: useActionState remplaced by controlled client state to render the DOM changes when muting inside the drawer from the action button

This commit is contained in:
alejandrobailo
2026-01-23 18:28:07 +01:00
parent 8792a60d1e
commit e991a9f645
3 changed files with 51 additions and 13 deletions

View File

@@ -4,9 +4,10 @@ import { Input, Textarea } from "@heroui/input";
import {
Dispatch,
SetStateAction,
useActionState,
useEffect,
useRef,
useState,
useTransition,
} from "react";
import { createMuteRule } from "@/actions/mute-rules";
@@ -29,6 +30,8 @@ export function MuteFindingsModal({
onComplete,
}: MuteFindingsModalProps) {
const { toast } = useToast();
const [state, setState] = useState<MuteRuleActionState | null>(null);
const [isPending, startTransition] = useTransition();
// Use refs to avoid stale closures in useEffect
const onCompleteRef = useRef(onComplete);
@@ -37,18 +40,12 @@ export function MuteFindingsModal({
const onOpenChangeRef = useRef(onOpenChange);
onOpenChangeRef.current = onOpenChange;
const [state, formAction, isPending] = useActionState<
MuteRuleActionState,
FormData
>(createMuteRule, null);
useEffect(() => {
if (state?.success) {
toast({
title: "Success",
description: state.success,
});
onCompleteRef.current?.();
onOpenChangeRef.current(false);
} else if (state?.errors?.general) {
@@ -71,7 +68,20 @@ export function MuteFindingsModal({
title="Mute Findings"
size="lg"
>
<form action={formAction} className="flex flex-col gap-4">
<form
className="flex flex-col gap-4"
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
startTransition(() => {
void (async () => {
const result = await createMuteRule(null, formData);
setState(result);
})();
});
}}
>
<input
type="hidden"
name="finding_ids"

View File

@@ -19,9 +19,13 @@ import { FindingsSelectionContext } from "./findings-selection-context";
interface DataTableRowActionsProps {
row: Row<FindingProps>;
onMuteComplete?: (findingIds: string[]) => void;
}
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
export function DataTableRowActions({
row,
onMuteComplete,
}: DataTableRowActionsProps) {
const router = useRouter();
const finding = row.original;
const [isJiraModalOpen, setIsJiraModalOpen] = useState(false);
@@ -84,6 +88,11 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
// 1. If the muted finding was selected, its index now points to a different finding
// 2. rowSelection uses indices (0, 1, 2...) not IDs, so after refresh the wrong findings would appear selected
clearSelection();
if (onMuteComplete) {
onMuteComplete(getMuteIds());
return;
}
router.refresh();
};
@@ -130,7 +139,9 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
label={getMuteLabel()}
description={getMuteDescription()}
disabled={isMuted}
onSelect={() => setIsMuteModalOpen(true)}
onSelect={() => {
setIsMuteModalOpen(true);
}}
/>
<ActionDropdownItem
icon={<JiraIcon size={20} />}

View File

@@ -126,6 +126,7 @@ const getResourceFindingsColumns = (
rowSelection: RowSelectionState,
selectableRowCount: number,
onNavigate: (id: string) => void,
onMuteComplete?: (findingIds: string[]) => void,
): ColumnDef<ResourceFinding>[] => {
const selectedCount = Object.values(rowSelection).filter(Boolean).length;
const isAllSelected =
@@ -227,7 +228,10 @@ const getResourceFindingsColumns = (
id: "actions",
header: () => <div className="w-10" />,
cell: ({ row }) => (
<DataTableRowActions row={row as unknown as Row<FindingProps>} />
<DataTableRowActions
row={row as unknown as Row<FindingProps>}
onMuteComplete={onMuteComplete}
/>
),
enableSorting: false,
},
@@ -255,6 +259,7 @@ export const ResourceDetail = ({
const [resourceTags, setResourceTags] = useState<Record<string, string>>({});
const [findingsLoading, setFindingsLoading] = useState(true);
const [hasInitiallyLoaded, setHasInitiallyLoaded] = useState(false);
const [findingsReloadNonce, setFindingsReloadNonce] = useState(0);
const [selectedFindingId, setSelectedFindingId] = useState<string | null>(
null,
);
@@ -377,7 +382,14 @@ export const ResourceDetail = ({
if (attributes.uid && isDrawerOpen) {
loadFindings();
}
}, [attributes.uid, currentPage, pageSize, searchQuery, isDrawerOpen]);
}, [
attributes.uid,
currentPage,
pageSize,
searchQuery,
isDrawerOpen,
findingsReloadNonce,
]);
const navigateToFinding = async (findingId: string) => {
// Cancel any in-flight request
@@ -438,8 +450,12 @@ export const ResourceDetail = ({
setFindingDetailLoading(false);
};
const handleMuteComplete = () => {
const handleMuteComplete = (_findingIds?: string[]) => {
const ids =
_findingIds && _findingIds.length > 0 ? _findingIds : selectedFindingIds;
setRowSelection({});
if (ids.length > 0) setFindingsReloadNonce((v) => v + 1);
router.refresh();
};
@@ -470,6 +486,7 @@ export const ResourceDetail = ({
rowSelection,
selectableRowCount,
navigateToFinding,
handleMuteComplete,
);
// Build Git URL for IaC resources