diff --git a/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningFindingRow.tsx b/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningFindingRow.tsx index 7f47bf6c1..f2aea5a5f 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningFindingRow.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningFindingRow.tsx @@ -9,6 +9,7 @@ import { ProjectPermissionCan } from "@app/components/permissions"; import { Badge, Button, + Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -34,11 +35,18 @@ import { } from "@app/hooks/api/secretScanningV2"; type Props = { + isSelected: boolean; + onToggleSelect: (e: boolean) => void; finding: TSecretScanningFinding; onUpdate: (finding: TSecretScanningFinding) => void; }; -export const SecretScanningFindingRow = ({ finding, onUpdate }: Props) => { +export const SecretScanningFindingRow = ({ + isSelected, + onToggleSelect, + finding, + onUpdate +}: Props) => { const { resourceName, id, @@ -84,6 +92,16 @@ export const SecretScanningFindingRow = ({ finding, onUpdate }: Props) => { )} key={`resource-${id}`} > + + onToggleSelect(!isSelected)} + onClick={(e) => { + e.stopPropagation(); + }} + /> +
{ status: initStatus ? [initStatus] : [] }); + const [selectedRows, setSelectedRows] = useState([]); + const { search, setSearch, @@ -276,6 +285,20 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => { + {filteredFindings.slice(offset, perPage * page).map((finding) => ( + v + ? setSelectedRows((sr) => [...sr, finding.id]) + : setSelectedRows((sr) => sr.filter((r) => r !== finding.id)) + } key={finding.id} finding={finding} - onUpdate={() => handlePopUpOpen("updateFinding", finding)} + onUpdate={() => handlePopUpOpen("updateFinding", [finding])} /> ))} @@ -365,8 +394,37 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => { handlePopUpToggle("updateFinding", isOpen)} - finding={popUp.updateFinding.data} + findings={popUp.updateFinding.data} /> + {selectedRows.length > 0 && ( +
+ + {selectedRows.length} finding{selectedRows.length === 1 ? "" : "s"} selected + + +
+ + {(isAllowed) => ( + + )} + +
+
+ )} ); }; diff --git a/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningUpdateFindingModal.tsx b/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningUpdateFindingModal.tsx index 3651ae3c2..c4e04df91 100644 --- a/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningUpdateFindingModal.tsx +++ b/frontend/src/pages/secret-scanning/SecretScanningFindingsPage/components/SecretScanningUpdateFindingModal.tsx @@ -21,52 +21,60 @@ import { } from "@app/hooks/api/secretScanningV2"; type Props = { - finding?: TSecretScanningFinding; + findings?: TSecretScanningFinding[]; isOpen: boolean; onOpenChange: (isOpen: boolean) => void; }; const FormSchema = z.object({ remarks: z.string().max(256, "Cannot exceed 256 characters").optional(), - status: z.nativeEnum(SecretScanningFindingStatus) + status: z.nativeEnum(SecretScanningFindingStatus).optional() }); type FormType = z.infer; type ContentProps = { - finding: TSecretScanningFinding; + findings: TSecretScanningFinding[]; onComplete: () => void; }; -const Content = ({ finding, onComplete }: ContentProps) => { +const Content = ({ findings, onComplete }: ContentProps) => { const updateFinding = useUpdateSecretScanningFinding(); + const single = findings.length === 1; + const { handleSubmit, control } = useForm({ resolver: zodResolver(FormSchema), defaultValues: { - status: finding.status, - remarks: finding.remarks ?? "" + status: single ? findings[0].status : undefined, + remarks: single ? findings[0].remarks : undefined } }); const onSubmit = async (data: FormType) => { try { - await updateFinding.mutateAsync({ - ...data, - findingId: finding.id, - projectId: finding.projectId - }); + for await (const finding of findings) { + // If a status wasn't set, don't update + if (data.status) { + await updateFinding.mutateAsync({ + ...data, + status: data.status, + findingId: finding.id, + projectId: finding.projectId + }); + } + } createNotification({ type: "success", - text: "Finding status successfully updated" + text: `Finding status${single ? "" : "es"} successfully updated` }); onComplete(); } catch { createNotification({ type: "error", - text: "Failed to update Finding status" + text: `Failed to update finding status${single ? "" : "es"}` }); } }; @@ -81,12 +89,15 @@ const Content = ({ finding, onComplete }: ContentProps) => {
+ + setSelectedRows((sr) => + sr.length === filteredFindings.length ? [] : filteredFindings.map((f) => f.id) + ) + } + onClick={(e) => { + e.stopPropagation(); + }} + /> + Platform
@@ -335,9 +358,15 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => {