mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 12:27:28 +00:00
Merge pull request #3782 from Infisical/ENG-2900
improvement(secret-scanning): Multi-select actions
This commit is contained in:
@@ -187,6 +187,56 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/findings",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SecretScanning],
|
||||||
|
description: "Update one or more Secret Scanning Findings in a batch.",
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
findingId: z.string().trim().min(1, "Finding ID required").describe(SecretScanningFindings.UPDATE.findingId),
|
||||||
|
status: z.nativeEnum(SecretScanningFindingStatus).optional().describe(SecretScanningFindings.UPDATE.status),
|
||||||
|
remarks: z.string().nullish().describe(SecretScanningFindings.UPDATE.remarks)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.max(500),
|
||||||
|
response: {
|
||||||
|
200: z.object({ findings: SecretScanningFindingSchema.array() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { body, permission } = req;
|
||||||
|
|
||||||
|
const updatedFindingPromises = body.map(async (findingUpdatePayload) => {
|
||||||
|
const { finding, projectId } = await server.services.secretScanningV2.updateSecretScanningFindingById(
|
||||||
|
findingUpdatePayload,
|
||||||
|
permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SECRET_SCANNING_FINDING_UPDATE,
|
||||||
|
metadata: findingUpdatePayload
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return finding;
|
||||||
|
});
|
||||||
|
|
||||||
|
const findings = await Promise.all(updatedFindingPromises);
|
||||||
|
|
||||||
|
return { findings };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/configs",
|
url: "/configs",
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export const useTriggerSecretScanningDataSource = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If possible, use useUpdateMultipleSecretScanningFinding instead.
|
||||||
export const useUpdateSecretScanningFinding = () => {
|
export const useUpdateSecretScanningFinding = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -140,6 +141,31 @@ export const useUpdateSecretScanningFinding = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUpdateMultipleSecretScanningFinding = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (findings: TUpdateSecretScanningFinding[]) => {
|
||||||
|
const { data } = await apiRequest.patch<TSecretScanningFindingResponse>(
|
||||||
|
"/api/v2/secret-scanning/findings",
|
||||||
|
findings
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.finding;
|
||||||
|
},
|
||||||
|
onSuccess: (_, findings) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: secretScanningV2Keys.listFindings(findings[0].projectId)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: secretScanningV2Keys.findingCount(findings[0].projectId)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: secretScanningV2Keys.dataSource()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useUpdateSecretScanningConfig = () => {
|
export const useUpdateSecretScanningConfig = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import {
|
|||||||
} from "@app/hooks/api/auth/queries";
|
} from "@app/hooks/api/auth/queries";
|
||||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
|
||||||
import { isLoggedIn } from "@app/hooks/api/reactQuery";
|
import { isLoggedIn } from "@app/hooks/api/reactQuery";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
+86
-3
@@ -11,7 +11,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { useSearch } from "@tanstack/react-router";
|
import { useSearch } from "@tanstack/react-router";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@@ -29,6 +32,10 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import {
|
||||||
|
ProjectPermissionSecretScanningFindingActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import {
|
import {
|
||||||
SECRET_SCANNING_DATA_SOURCE_MAP,
|
SECRET_SCANNING_DATA_SOURCE_MAP,
|
||||||
SECRET_SCANNING_FINDING_STATUS_ICON_MAP
|
SECRET_SCANNING_FINDING_STATUS_ICON_MAP
|
||||||
@@ -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,
|
||||||
@@ -159,6 +168,13 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => {
|
|||||||
setPage
|
setPage
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const currentPageData = useMemo(
|
||||||
|
() => filteredFindings.slice(offset, perPage * page),
|
||||||
|
[filteredFindings, offset, perPage, page]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentPageDataIds = useMemo(() => currentPageData.map((f) => f.id), [currentPageData]);
|
||||||
|
|
||||||
const handleSort = (column: FindingsOrderBy) => {
|
const handleSort = (column: FindingsOrderBy) => {
|
||||||
if (column === orderBy) {
|
if (column === orderBy) {
|
||||||
toggleOrderDirection();
|
toggleOrderDirection();
|
||||||
@@ -276,6 +292,37 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => {
|
|||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
|
<Th className="">
|
||||||
|
<Checkbox
|
||||||
|
id="checkbox-select-all"
|
||||||
|
isChecked={
|
||||||
|
currentPageDataIds.length > 0 &&
|
||||||
|
currentPageDataIds.every((id) => selectedRows.includes(id))
|
||||||
|
}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
const allCurrentlySelectedOnPage =
|
||||||
|
currentPageDataIds.length > 0 &&
|
||||||
|
currentPageDataIds.every((id) => selectedRows.includes(id));
|
||||||
|
|
||||||
|
if (allCurrentlySelectedOnPage) {
|
||||||
|
// Deselect all on current page
|
||||||
|
setSelectedRows((prev) =>
|
||||||
|
prev.filter((rowId) => !currentPageDataIds.includes(rowId))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Select all on current page
|
||||||
|
setSelectedRows((prev) => {
|
||||||
|
const newSelectedRows = new Set(prev);
|
||||||
|
currentPageDataIds.forEach((id) => newSelectedRows.add(id));
|
||||||
|
return Array.from(newSelectedRows);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
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">
|
||||||
@@ -333,11 +380,17 @@ export const SecretScanningFindingsTable = ({ findings }: Props) => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{filteredFindings.slice(offset, perPage * page).map((finding) => (
|
{currentPageData.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 +418,38 @@ 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}
|
onComplete={() => setSelectedRows([])}
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+59
-23
@@ -17,56 +17,75 @@ import { SECRET_SCANNING_FINDING_STATUS_ICON_MAP } from "@app/helpers/secretScan
|
|||||||
import {
|
import {
|
||||||
SecretScanningFindingStatus,
|
SecretScanningFindingStatus,
|
||||||
TSecretScanningFinding,
|
TSecretScanningFinding,
|
||||||
useUpdateSecretScanningFinding
|
useUpdateMultipleSecretScanningFinding
|
||||||
} 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;
|
||||||
|
onComplete?: () => 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 updateMultipleFindings = useUpdateMultipleSecretScanningFinding();
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
if (!data.status) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateFinding.mutateAsync({
|
if (findings.length > 1) {
|
||||||
...data,
|
await updateMultipleFindings.mutateAsync(
|
||||||
findingId: finding.id,
|
findings.map((f) => ({
|
||||||
projectId: finding.projectId
|
...data,
|
||||||
});
|
status: data.status!,
|
||||||
|
findingId: f.id,
|
||||||
|
projectId: f.projectId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await updateMultipleFindings.mutateAsync([
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
status: data.status,
|
||||||
|
findingId: findings[0].id,
|
||||||
|
projectId: findings[0].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 +100,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 (
|
||||||
@@ -114,8 +136,8 @@ const Content = ({ finding, onComplete }: ContentProps) => {
|
|||||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={updateFinding.isPending}
|
isLoading={updateMultipleFindings.isPending}
|
||||||
isDisabled={updateFinding.isPending}
|
isDisabled={updateMultipleFindings.isPending}
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
>
|
>
|
||||||
Update Status
|
Update Status
|
||||||
@@ -128,13 +150,27 @@ const Content = ({ finding, onComplete }: ContentProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretScanningUpdateFindingModal = ({ finding, isOpen, onOpenChange }: Props) => {
|
export const SecretScanningUpdateFindingModal = ({
|
||||||
if (!finding) return null;
|
findings,
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
onComplete
|
||||||
|
}: Props) => {
|
||||||
|
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
|
||||||
<Content finding={finding} onComplete={() => onOpenChange(false)} />
|
title={`Update Finding${findings.length === 1 ? "" : "s"}`}
|
||||||
|
subTitle="Update the status or leave remarks"
|
||||||
|
>
|
||||||
|
<Content
|
||||||
|
findings={findings}
|
||||||
|
onComplete={() => {
|
||||||
|
onOpenChange(false);
|
||||||
|
if (onComplete) onComplete();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user