improvement(secret-scanning): Multi-select actions

This commit is contained in:
x032205
2025-06-12 01:25:53 -04:00
parent dbf7ecc9b6
commit aa577b095c
3 changed files with 108 additions and 21 deletions

View File

@@ -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}`}
>
<Td className="pr-0">
<Checkbox
id={`checkbox-${id}`}
isChecked={isSelected}
onCheckedChange={() => onToggleSelect(!isSelected)}
onClick={(e) => {
e.stopPropagation();
}}
/>
</Td>
<Td className="!min-w-[4rem] max-w-0">
<div className="flex w-full items-center">
<img

View File

@@ -12,6 +12,8 @@ import { useSearch } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import {
Button,
Checkbox,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -43,6 +45,11 @@ import {
import { SecretScanningFindingRow } from "./SecretScanningFindingRow";
import { SecretScanningUpdateFindingModal } from "./SecretScanningUpdateFindingModal";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
ProjectPermissionSecretScanningFindingActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
enum FindingsOrderBy {
ResourceName = "resource-name",
@@ -85,6 +92,8 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => {
status: initStatus ? [initStatus] : []
});
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const {
search,
setSearch,
@@ -276,6 +285,20 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => {
<Table>
<THead>
<Tr>
<Th className="">
<Checkbox
id="checkbox-select-all"
isChecked={selectedRows.length === filteredFindings.length}
onCheckedChange={() =>
setSelectedRows((sr) =>
sr.length === filteredFindings.length ? [] : filteredFindings.map((f) => f.id)
)
}
onClick={(e) => {
e.stopPropagation();
}}
/>
</Th>
<Th className="min-w-[10rem]">Platform</Th>
<Th className="w-1/4">
<div className="flex items-center">
@@ -335,9 +358,15 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => {
<TBody>
{filteredFindings.slice(offset, perPage * page).map((finding) => (
<SecretScanningFindingRow
isSelected={selectedRows.includes(finding.id)}
onToggleSelect={(v) =>
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])}
/>
))}
</TBody>
@@ -365,8 +394,37 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => {
<SecretScanningUpdateFindingModal
isOpen={popUp.updateFinding.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("updateFinding", isOpen)}
finding={popUp.updateFinding.data}
findings={popUp.updateFinding.data}
/>
{selectedRows.length > 0 && (
<div className="mt-4 flex items-center justify-between rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-2 pl-4">
<span>
{selectedRows.length} finding{selectedRows.length === 1 ? "" : "s"} selected
</span>
<div className="flex gap-2">
<ProjectPermissionCan
I={ProjectPermissionSecretScanningFindingActions.Update}
a={ProjectPermissionSub.SecretScanningFindings}
>
{(isAllowed) => (
<Button
onClick={() =>
handlePopUpOpen(
"updateFinding",
findings.filter((f) => selectedRows.includes(f.id))
)
}
colorSchema="secondary"
isDisabled={!isAllowed}
>
Update Status
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
)}
</div>
);
};

View File

@@ -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<typeof FormSchema>;
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<FormType>({
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) => {
<FormControl label="Status" isError={Boolean(error)} errorText={error?.message}>
<Select
value={value}
placeholder="Select status..."
onValueChange={onChange}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
dropdownContainerClassName="max-w-none"
icon={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[value].icon}
iconClassName={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[value].className}
icon={value ? SECRET_SCANNING_FINDING_STATUS_ICON_MAP[value].icon : undefined}
iconClassName={
value ? SECRET_SCANNING_FINDING_STATUS_ICON_MAP[value].className : undefined
}
>
{Object.values(SecretScanningFindingStatus).map((status) => {
return (
@@ -128,13 +139,13 @@ const Content = ({ finding, onComplete }: ContentProps) => {
);
};
export const SecretScanningUpdateFindingModal = ({ finding, isOpen, onOpenChange }: Props) => {
if (!finding) return null;
export const SecretScanningUpdateFindingModal = ({ findings, isOpen, onOpenChange }: Props) => {
if (!findings?.length) return null;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Update Finding" subTitle="Update the status or leave remarks">
<Content finding={finding} onComplete={() => onOpenChange(false)} />
<Content findings={findings || []} onComplete={() => onOpenChange(false)} />
</ModalContent>
</Modal>
);