mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
improvement(secret-scanning): Multi-select actions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user