mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 23:26:09 +00:00
improvement(secret-scanning): Multi-select actions
This commit is contained in:
+19
-1
@@ -9,6 +9,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
|||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@@ -34,11 +35,18 @@ import {
|
|||||||
} from "@app/hooks/api/secretScanningV2";
|
} from "@app/hooks/api/secretScanningV2";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
isSelected: boolean;
|
||||||
|
onToggleSelect: (e: boolean) => void;
|
||||||
finding: TSecretScanningFinding;
|
finding: TSecretScanningFinding;
|
||||||
onUpdate: (finding: TSecretScanningFinding) => void;
|
onUpdate: (finding: TSecretScanningFinding) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretScanningFindingRow = ({ finding, onUpdate }: Props) => {
|
export const SecretScanningFindingRow = ({
|
||||||
|
isSelected,
|
||||||
|
onToggleSelect,
|
||||||
|
finding,
|
||||||
|
onUpdate
|
||||||
|
}: Props) => {
|
||||||
const {
|
const {
|
||||||
resourceName,
|
resourceName,
|
||||||
id,
|
id,
|
||||||
@@ -84,6 +92,16 @@ export const SecretScanningFindingRow = ({ finding, onUpdate }: Props) => {
|
|||||||
)}
|
)}
|
||||||
key={`resource-${id}`}
|
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">
|
<Td className="!min-w-[4rem] max-w-0">
|
||||||
<div className="flex w-full items-center">
|
<div className="flex w-full items-center">
|
||||||
<img
|
<img
|
||||||
|
|||||||
+60
-2
@@ -12,6 +12,8 @@ import { useSearch } from "@tanstack/react-router";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@@ -43,6 +45,11 @@ import {
|
|||||||
|
|
||||||
import { SecretScanningFindingRow } from "./SecretScanningFindingRow";
|
import { SecretScanningFindingRow } from "./SecretScanningFindingRow";
|
||||||
import { SecretScanningUpdateFindingModal } from "./SecretScanningUpdateFindingModal";
|
import { SecretScanningUpdateFindingModal } from "./SecretScanningUpdateFindingModal";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
ProjectPermissionSecretScanningFindingActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
|
||||||
enum FindingsOrderBy {
|
enum FindingsOrderBy {
|
||||||
ResourceName = "resource-name",
|
ResourceName = "resource-name",
|
||||||
@@ -85,6 +92,8 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => {
|
|||||||
status: initStatus ? [initStatus] : []
|
status: initStatus ? [initStatus] : []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [selectedRows, setSelectedRows] = useState<string[]>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
search,
|
search,
|
||||||
setSearch,
|
setSearch,
|
||||||
@@ -276,6 +285,20 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => {
|
|||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<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="min-w-[10rem]">Platform</Th>
|
||||||
<Th className="w-1/4">
|
<Th className="w-1/4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -335,9 +358,15 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => {
|
|||||||
<TBody>
|
<TBody>
|
||||||
{filteredFindings.slice(offset, perPage * page).map((finding) => (
|
{filteredFindings.slice(offset, perPage * page).map((finding) => (
|
||||||
<SecretScanningFindingRow
|
<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}
|
key={finding.id}
|
||||||
finding={finding}
|
finding={finding}
|
||||||
onUpdate={() => handlePopUpOpen("updateFinding", finding)}
|
onUpdate={() => handlePopUpOpen("updateFinding", [finding])}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TBody>
|
</TBody>
|
||||||
@@ -365,8 +394,37 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => {
|
|||||||
<SecretScanningUpdateFindingModal
|
<SecretScanningUpdateFindingModal
|
||||||
isOpen={popUp.updateFinding.isOpen}
|
isOpen={popUp.updateFinding.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+29
-18
@@ -21,52 +21,60 @@ import {
|
|||||||
} from "@app/hooks/api/secretScanningV2";
|
} from "@app/hooks/api/secretScanningV2";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
finding?: TSecretScanningFinding;
|
findings?: TSecretScanningFinding[];
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: (isOpen: boolean) => void;
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
||||||
remarks: z.string().max(256, "Cannot exceed 256 characters").optional(),
|
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 FormType = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
type ContentProps = {
|
type ContentProps = {
|
||||||
finding: TSecretScanningFinding;
|
findings: TSecretScanningFinding[];
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Content = ({ finding, onComplete }: ContentProps) => {
|
const Content = ({ findings, onComplete }: ContentProps) => {
|
||||||
const updateFinding = useUpdateSecretScanningFinding();
|
const updateFinding = useUpdateSecretScanningFinding();
|
||||||
|
|
||||||
|
const single = findings.length === 1;
|
||||||
|
|
||||||
const { handleSubmit, control } = useForm<FormType>({
|
const { handleSubmit, control } = useForm<FormType>({
|
||||||
resolver: zodResolver(FormSchema),
|
resolver: zodResolver(FormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
status: finding.status,
|
status: single ? findings[0].status : undefined,
|
||||||
remarks: finding.remarks ?? ""
|
remarks: single ? findings[0].remarks : undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async (data: FormType) => {
|
const onSubmit = async (data: FormType) => {
|
||||||
try {
|
try {
|
||||||
await updateFinding.mutateAsync({
|
for await (const finding of findings) {
|
||||||
...data,
|
// If a status wasn't set, don't update
|
||||||
findingId: finding.id,
|
if (data.status) {
|
||||||
projectId: finding.projectId
|
await updateFinding.mutateAsync({
|
||||||
});
|
...data,
|
||||||
|
status: data.status,
|
||||||
|
findingId: finding.id,
|
||||||
|
projectId: finding.projectId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "success",
|
type: "success",
|
||||||
text: "Finding status successfully updated"
|
text: `Finding status${single ? "" : "es"} successfully updated`
|
||||||
});
|
});
|
||||||
|
|
||||||
onComplete();
|
onComplete();
|
||||||
} catch {
|
} catch {
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "error",
|
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}>
|
<FormControl label="Status" isError={Boolean(error)} errorText={error?.message}>
|
||||||
<Select
|
<Select
|
||||||
value={value}
|
value={value}
|
||||||
|
placeholder="Select status..."
|
||||||
onValueChange={onChange}
|
onValueChange={onChange}
|
||||||
className="w-full border border-mineshaft-500 capitalize"
|
className="w-full border border-mineshaft-500 capitalize"
|
||||||
position="popper"
|
position="popper"
|
||||||
dropdownContainerClassName="max-w-none"
|
dropdownContainerClassName="max-w-none"
|
||||||
icon={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[value].icon}
|
icon={value ? SECRET_SCANNING_FINDING_STATUS_ICON_MAP[value].icon : undefined}
|
||||||
iconClassName={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[value].className}
|
iconClassName={
|
||||||
|
value ? SECRET_SCANNING_FINDING_STATUS_ICON_MAP[value].className : undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{Object.values(SecretScanningFindingStatus).map((status) => {
|
{Object.values(SecretScanningFindingStatus).map((status) => {
|
||||||
return (
|
return (
|
||||||
@@ -128,13 +139,13 @@ const Content = ({ finding, onComplete }: ContentProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretScanningUpdateFindingModal = ({ finding, isOpen, onOpenChange }: Props) => {
|
export const SecretScanningUpdateFindingModal = ({ findings, isOpen, onOpenChange }: Props) => {
|
||||||
if (!finding) return null;
|
if (!findings?.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<ModalContent title="Update Finding" subTitle="Update the status or leave remarks">
|
<ModalContent title="Update Finding" subTitle="Update the status or leave remarks">
|
||||||
<Content finding={finding} onComplete={() => onOpenChange(false)} />
|
<Content findings={findings || []} onComplete={() => onOpenChange(false)} />
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user