From 41477252603ca329eae178e51bb86829d14b9082 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 15 Jan 2025 01:43:31 +0100 Subject: [PATCH] feat(radar): pagination & filtering --- .../ee/routes/v1/secret-scanning-router.ts | 79 +++++++++- .../secret-scanning/secret-scanning-dal.ts | 76 +++++++++- .../secret-scanning-service.ts | 17 ++- .../secret-scanning/secret-scanning-types.ts | 25 +++- .../src/hooks/api/secretScanning/index.tsx | 1 + .../src/hooks/api/secretScanning/mutation.ts | 35 ++++- .../src/hooks/api/secretScanning/queries.ts | 56 +++++-- .../src/hooks/api/secretScanning/types.ts | 15 ++ .../SecretScanningPage/SecretScanningPage.tsx | 70 +++++++-- .../components/ExportSecretScansModal.tsx | 138 ++++++------------ .../components/SecretScanningFilters.tsx | 89 +++++++++++ .../SecretScanningPage/components/types.ts | 22 +++ 12 files changed, 495 insertions(+), 128 deletions(-) create mode 100644 frontend/src/pages/organization/SecretScanningPage/components/SecretScanningFilters.tsx create mode 100644 frontend/src/pages/organization/SecretScanningPage/components/types.ts diff --git a/backend/src/ee/routes/v1/secret-scanning-router.ts b/backend/src/ee/routes/v1/secret-scanning-router.ts index 89784600a..f144a6c00 100644 --- a/backend/src/ee/routes/v1/secret-scanning-router.ts +++ b/backend/src/ee/routes/v1/secret-scanning-router.ts @@ -1,9 +1,13 @@ import { z } from "zod"; import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas"; -import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types"; +import { + SecretScanningResolvedStatus, + SecretScanningRiskStatus +} from "@app/ee/services/secret-scanning/secret-scanning-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; +import { OrderByDirection } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -97,6 +101,45 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = } }); + server.route({ + url: "/organization/:organizationId/risks/export", + method: "GET", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ organizationId: z.string().trim() }), + querystring: z.object({ + repositoryNames: z + .string() + .optional() + .nullable() + .transform((val) => (val ? val.split(",") : undefined)), + resolvedStatus: z.nativeEnum(SecretScanningResolvedStatus).optional() + }), + response: { + 200: z.object({ + risks: SecretScanningGitRisksSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const risks = await server.services.secretScanning.getAllRisksByOrg({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + orgId: req.params.organizationId, + filter: { + repositoryNames: req.query.repositoryNames, + resolvedStatus: req.query.resolvedStatus + } + }); + return { risks }; + } + }); + server.route({ url: "/organization/:organizationId/risks", method: "GET", @@ -105,20 +148,46 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = }, schema: { params: z.object({ organizationId: z.string().trim() }), + + querystring: z.object({ + offset: z.coerce.number().min(0).default(0), + limit: z.coerce.number().min(1).max(20000).default(100), + orderBy: z.enum(["createdAt", "name"]).default("createdAt"), + orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.DESC), + repositoryNames: z + .string() + .optional() + .nullable() + .transform((val) => (val ? val.split(",") : undefined)), + resolvedStatus: z.nativeEnum(SecretScanningResolvedStatus).optional() + }), + response: { - 200: z.object({ risks: SecretScanningGitRisksSchema.array() }) + 200: z.object({ + risks: SecretScanningGitRisksSchema.array(), + totalCount: z.number(), + repos: z.array(z.string()) + }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { risks } = await server.services.secretScanning.getRisksByOrg({ + const { risks, totalCount, repos } = await server.services.secretScanning.getRisksByOrg({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - orgId: req.params.organizationId + orgId: req.params.organizationId, + filter: { + limit: req.query.limit, + offset: req.query.offset, + orderBy: req.query.orderBy, + orderDirection: req.query.orderDirection, + repositoryNames: req.query.repositoryNames, + resolvedStatus: req.query.resolvedStatus + } }); - return { risks }; + return { risks, totalCount, repos }; } }); diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-dal.ts b/backend/src/ee/services/secret-scanning/secret-scanning-dal.ts index 828322ad2..cef6f3176 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-dal.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-dal.ts @@ -1,9 +1,12 @@ -import { Knex } from "knex"; +import { Knex, KnexTimeoutError } from "knex"; import { TDbClient } from "@app/db"; import { TableName, TSecretScanningGitRisksInsert } from "@app/db/schemas"; -import { DatabaseError } from "@app/lib/errors"; -import { ormify } from "@app/lib/knex"; +import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; + +import { SecretScanningResolvedStatus, TGetOrgRisksDTO } from "./secret-scanning-types"; export type TSecretScanningDALFactory = ReturnType; @@ -19,5 +22,70 @@ export const secretScanningDALFactory = (db: TDbClient) => { } }; - return { ...gitRiskOrm, upsert }; + const findByOrgId = async (orgId: string, filter: TGetOrgRisksDTO["filter"], tx?: Knex) => { + try { + // Find statements + const sqlQuery = (tx || db.replicaNode())(TableName.SecretScanningGitRisk) + // eslint-disable-next-line func-names + .where(`${TableName.SecretScanningGitRisk}.orgId`, orgId); + + if (filter.repositoryNames) { + void sqlQuery.whereIn(`${TableName.SecretScanningGitRisk}.repositoryFullName`, filter.repositoryNames); + } + + if (filter.resolvedStatus) { + if (filter.resolvedStatus !== SecretScanningResolvedStatus.All) { + const isResolved = filter.resolvedStatus === SecretScanningResolvedStatus.Resolved; + + void sqlQuery.where(`${TableName.SecretScanningGitRisk}.isResolved`, isResolved); + } + } + + // Select statements + void sqlQuery + .select(selectAllTableCols(TableName.SecretScanningGitRisk)) + .limit(filter.limit) + .offset(filter.offset); + + if (filter.orderBy) { + const orderDirection = filter.orderDirection || OrderByDirection.ASC; + + void sqlQuery.orderBy(filter.orderBy, orderDirection); + } + + const countQuery = (tx || db.replicaNode())(TableName.SecretScanningGitRisk) + .where(`${TableName.SecretScanningGitRisk}.orgId`, orgId) + .count(); + + const uniqueReposQuery = (tx || db.replicaNode())(TableName.SecretScanningGitRisk) + .where(`${TableName.SecretScanningGitRisk}.orgId`, orgId) + .distinct("repositoryFullName") + .select("repositoryFullName"); + + // we timeout long running queries to prevent DB resource issues (2 minutes) + const docs = await sqlQuery.timeout(1000 * 120); + const uniqueRepos = await uniqueReposQuery.timeout(1000 * 120); + const totalCount = await countQuery; + + return { + risks: docs, + totalCount: Number(totalCount?.[0].count), + repos: uniqueRepos + .filter(Boolean) + .map((r) => r.repositoryFullName!) + .sort((a, b) => a.localeCompare(b)) + }; + } catch (error) { + if (error instanceof KnexTimeoutError) { + throw new GatewayTimeoutError({ + error, + message: "Failed to fetch audit logs due to timeout. Add more search filters." + }); + } + + throw new DatabaseError({ error }); + } + }; + + return { ...gitRiskOrm, upsert, findByOrgId }; }; diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts index 945164094..0ccafaf56 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts @@ -15,6 +15,7 @@ import { TSecretScanningDALFactory } from "./secret-scanning-dal"; import { TSecretScanningQueueFactory } from "./secret-scanning-queue"; import { SecretScanningRiskStatus, + TGetAllOrgRisksDTO, TGetOrgInstallStatusDTO, TGetOrgRisksDTO, TInstallAppSessionDTO, @@ -118,11 +119,22 @@ export const secretScanningServiceFactory = ({ return Boolean(appInstallation); }; - const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetOrgRisksDTO) => { + const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId, filter }: TGetOrgRisksDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); + // const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] }); + + const results = await secretScanningDAL.findByOrgId(orgId, filter); + + return results; + }; + + const getAllRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetAllOrgRisksDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); + const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] }); - return { risks }; + return risks; }; const updateRiskStatus = async ({ @@ -189,6 +201,7 @@ export const secretScanningServiceFactory = ({ linkInstallationToOrg, getOrgInstallationStatus, getRisksByOrg, + getAllRisksByOrg, updateRiskStatus, handleRepoPushEvent, handleRepoDeleteEvent diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-types.ts b/backend/src/ee/services/secret-scanning/secret-scanning-types.ts index dc83599af..d3212a73f 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-types.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-types.ts @@ -1,4 +1,4 @@ -import { TOrgPermission } from "@app/lib/types"; +import { OrderByDirection, TOrgPermission } from "@app/lib/types"; export enum SecretScanningRiskStatus { FalsePositive = "RESOLVED_FALSE_POSITIVE", @@ -7,6 +7,12 @@ export enum SecretScanningRiskStatus { Unresolved = "UNRESOLVED" } +export enum SecretScanningResolvedStatus { + All = "all", + Resolved = "resolved", + Unresolved = "unresolved" +} + export type TInstallAppSessionDTO = TOrgPermission; export type TLinkInstallSessionDTO = { @@ -16,7 +22,22 @@ export type TLinkInstallSessionDTO = { export type TGetOrgInstallStatusDTO = TOrgPermission; -export type TGetOrgRisksDTO = TOrgPermission; +type RiskFilter = { + offset: number; + limit: number; + orderBy?: "createdAt" | "name"; + orderDirection?: OrderByDirection; + repositoryNames?: string[]; + resolvedStatus?: SecretScanningResolvedStatus; +}; + +export type TGetOrgRisksDTO = { + filter: RiskFilter; +} & TOrgPermission; + +export type TGetAllOrgRisksDTO = { + filter: Omit; +} & TOrgPermission; export type TUpdateRiskStatusDTO = { riskId: string; diff --git a/frontend/src/hooks/api/secretScanning/index.tsx b/frontend/src/hooks/api/secretScanning/index.tsx index a7f4c2a76..319b37330 100644 --- a/frontend/src/hooks/api/secretScanning/index.tsx +++ b/frontend/src/hooks/api/secretScanning/index.tsx @@ -1,5 +1,6 @@ export { useCreateNewInstallationSession, + useExportSecretScanningRisks, useLinkGitAppInstallationWithOrg, useUpdateRiskStatus } from "./mutation"; diff --git a/frontend/src/hooks/api/secretScanning/mutation.ts b/frontend/src/hooks/api/secretScanning/mutation.ts index f469c872c..7298b9af1 100644 --- a/frontend/src/hooks/api/secretScanning/mutation.ts +++ b/frontend/src/hooks/api/secretScanning/mutation.ts @@ -2,7 +2,12 @@ import { useMutation } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { RiskStatus, TGitAppOrg, TSecretScanningGitRisks } from "./types"; +import { + RiskStatus, + SecretScanningResolvedStatus, + TGitAppOrg, + TSecretScanningGitRisks +} from "./types"; export const useCreateNewInstallationSession = () => { return useMutation<{ sessionId: string }, object, { organizationId: string }>({ @@ -43,3 +48,31 @@ export const useLinkGitAppInstallationWithOrg = () => { } }); }; + +export const useExportSecretScanningRisks = () => { + return useMutation< + TSecretScanningGitRisks[], + object, + { + orgId: string; + filter: { + repositoryNames?: string[]; + resolvedStatus?: SecretScanningResolvedStatus; + }; + } + >({ + mutationFn: async ({ filter, orgId }) => { + const params = new URLSearchParams({ + ...(filter.resolvedStatus && { resolvedStatus: filter.resolvedStatus }), + ...(filter.repositoryNames && { repositoryNames: filter.repositoryNames.join(",") }) + }); + + const { data } = await apiRequest.get<{ + risks: TSecretScanningGitRisks[]; + }>(`/api/v1/secret-scanning/organization/${orgId}/risks/export`, { + params + }); + return data.risks; + } + }); +}; diff --git a/frontend/src/hooks/api/secretScanning/queries.ts b/frontend/src/hooks/api/secretScanning/queries.ts index 2920be321..aa9b2043c 100644 --- a/frontend/src/hooks/api/secretScanning/queries.ts +++ b/frontend/src/hooks/api/secretScanning/queries.ts @@ -2,11 +2,19 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TSecretScanningGitRisks } from "./types"; +import { SecretScanningOrderBy, SecretScanningRiskFilter, TSecretScanningGitRisks } from "./types"; export const secretScanningQueryKeys = { getInstallationStatus: (orgId: string) => ["secret-scanning-installation-status", { orgId }], - getRisksByOrganizatio: (orgId: string) => ["secret-scanning-risks", { orgId }] + getRisksByOrganization: ( + orgId: string, + sort: { + offset: number; + limit: number; + orderBy: SecretScanningOrderBy; + }, + filter: SecretScanningRiskFilter + ) => ["secret-scanning-risks", { orgId, sort, filter }] }; const fetchSecretScanningInstallationStatus = async (organizationId: string) => { @@ -22,15 +30,43 @@ export const useGetSecretScanningInstallationStatus = (orgId: string) => queryFn: () => fetchSecretScanningInstallationStatus(orgId) }); -const fetchSecretScanningRisksByOrgId = async (oranizationId: string) => { - const { data } = await apiRequest.get<{ risks: TSecretScanningGitRisks[] }>( - `/api/v1/secret-scanning/organization/${oranizationId}/risks` - ); - return data.risks; +const fetchSecretScanningRisksByOrgId = async ( + organizationId: string, + sort: { + offset: number; + limit: number; + orderBy: SecretScanningOrderBy; + }, + filter: SecretScanningRiskFilter +) => { + const params = new URLSearchParams({ + offset: String(sort.offset), + limit: String(sort.limit), + orderBy: sort.orderBy, + ...(filter.resolvedStatus && { resolvedStatus: filter.resolvedStatus }), + ...(filter.repositoryNames && { repositoryNames: filter.repositoryNames.join(",") }) + }); + + const { data } = await apiRequest.get<{ + risks: TSecretScanningGitRisks[]; + totalCount: number; + repos: string[]; + }>(`/api/v1/secret-scanning/organization/${organizationId}/risks`, { + params + }); + return data; }; -export const useGetSecretScanningRisks = (orgId: string) => +export const useGetSecretScanningRisks = ( + orgId: string, + sort: { + offset: number; + limit: number; + orderBy: SecretScanningOrderBy; + }, + filter: SecretScanningRiskFilter +) => useQuery({ - queryKey: secretScanningQueryKeys.getRisksByOrganizatio(orgId), - queryFn: () => fetchSecretScanningRisksByOrgId(orgId) + queryKey: secretScanningQueryKeys.getRisksByOrganization(orgId, sort, filter), + queryFn: () => fetchSecretScanningRisksByOrgId(orgId, sort, filter) }); diff --git a/frontend/src/hooks/api/secretScanning/types.ts b/frontend/src/hooks/api/secretScanning/types.ts index 7c2cdc652..ba06b4bed 100644 --- a/frontend/src/hooks/api/secretScanning/types.ts +++ b/frontend/src/hooks/api/secretScanning/types.ts @@ -5,6 +5,21 @@ export enum RiskStatus { UNRESOLVED = "UNRESOLVED" } +export enum SecretScanningOrderBy { + CreatedAt = "createdAt" +} + +export enum SecretScanningResolvedStatus { + All = "all", + Resolved = "resolved", + Unresolved = "unresolved" +} + +export type SecretScanningRiskFilter = { + repositoryNames?: string[]; + resolvedStatus?: SecretScanningResolvedStatus; +}; + export type TSecretScanningGitRisks = { id: string; description: string; diff --git a/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx b/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx index 626d41da1..af8241350 100644 --- a/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx +++ b/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx @@ -1,9 +1,11 @@ import { useEffect } from "react"; import { Helmet } from "react-helmet"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useSearch } from "@tanstack/react-router"; import { OrgPermissionCan } from "@app/components/permissions"; -import { Button, NoticeBanner } from "@app/components/v2"; +import { Button, NoticeBanner, Pagination } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { OrgPermissionActions, @@ -12,28 +14,58 @@ import { useServerConfig } from "@app/context"; import { withPermission } from "@app/hoc"; -import { usePopUp } from "@app/hooks"; +import { usePagination, usePopUp } from "@app/hooks"; import { useCreateNewInstallationSession, useGetSecretScanningInstallationStatus, useGetSecretScanningRisks, useLinkGitAppInstallationWithOrg } from "@app/hooks/api/secretScanning"; +import { SecretScanningOrderBy } from "@app/hooks/api/secretScanning/types"; import { ExportSecretScansModal } from "./components/ExportSecretScansModal"; +import { SecretScanningFilter } from "./components/SecretScanningFilters"; +import { SecretScanningFilterFormData, secretScanningFilterFormSchema } from "./components/types"; import { SecretScanningLogsTable } from "./components"; +const PER_PAGE_INIT = 25; + export const SecretScanningPage = withPermission( () => { const queryParams = useSearch({ from: ROUTE_PATHS.Organization.SecretScanning.id }); + const { control, watch } = useForm({ + resolver: zodResolver(secretScanningFilterFormSchema), + defaultValues: {} + }); + const { config } = useServerConfig(); const { currentOrg } = useOrganization(); const organizationId = currentOrg.id; - const { isPending, data: gitRisks } = useGetSecretScanningRisks(organizationId); + const { offset, limit, orderBy, setPage, perPage, page, setPerPage } = usePagination( + SecretScanningOrderBy.CreatedAt, + { initPerPage: PER_PAGE_INIT } + ); + + const repositoryNames = watch("repositoryNames"); + const resolvedStatus = watch("resolved"); + + const { isPending, data: risksData } = useGetSecretScanningRisks( + organizationId, + { + offset, + limit, + orderBy + }, + { + repositoryNames: + repositoryNames?.length > 0 ? repositoryNames.map((repo) => repo.name) : undefined, + resolvedStatus + } + ); const { mutateAsync: linkGitAppInstallationWithOrganization } = useLinkGitAppInstallationWithOrg(); @@ -72,7 +104,7 @@ export const SecretScanningPage = withPermission( }; return ( -
+
Secret scanning @@ -141,21 +173,31 @@ export const SecretScanningPage = withPermission( )}
-
- +
+
- + + {!isPending && + risksData?.totalCount !== undefined && + risksData.totalCount >= PER_PAGE_INIT && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )}
diff --git a/frontend/src/pages/organization/SecretScanningPage/components/ExportSecretScansModal.tsx b/frontend/src/pages/organization/SecretScanningPage/components/ExportSecretScansModal.tsx index 4d583b61e..ea00a30d4 100644 --- a/frontend/src/pages/organization/SecretScanningPage/components/ExportSecretScansModal.tsx +++ b/frontend/src/pages/organization/SecretScanningPage/components/ExportSecretScansModal.tsx @@ -1,11 +1,12 @@ -import { useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import FileSaver from "file-saver"; import { z } from "zod"; import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; -import { TSecretScanningGitRisks } from "@app/hooks/api/secretScanning/types"; +import { useOrganization } from "@app/context"; +import { useExportSecretScanningRisks } from "@app/hooks/api/secretScanning"; +import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { convertJsonToCsv } from "@app/lib/fn/csv"; @@ -15,7 +16,7 @@ type Props = { popUpName: keyof UsePopUpState<["exportSecretScans"]>, state?: boolean ) => void; - gitRisks: TSecretScanningGitRisks[]; + repositories: string[]; }; enum ExportFormat { @@ -23,71 +24,52 @@ enum ExportFormat { Csv = "csv" } -enum ExportStatus { - All = "all", - Unresolved = "unresolved", - Resolved = "resolved" -} - const formSchema = z.object({ - githubOrganization: z.string().trim(), githubRepository: z.string().trim(), - status: z.nativeEnum(ExportStatus), + status: z.nativeEnum(SecretScanningResolvedStatus), exportFormat: z.nativeEnum(ExportFormat) }); type TFormSchema = z.infer; -export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, gitRisks }: Props) => { +export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, repositories }: Props) => { + const { currentOrg } = useOrganization(); + const { mutateAsync } = useExportSecretScanningRisks(); + const { control, handleSubmit, - watch, - formState: { isSubmitting } } = useForm({ resolver: zodResolver(formSchema), defaultValues: { exportFormat: ExportFormat.Json, - status: ExportStatus.All, - githubOrganization: "all", + status: SecretScanningResolvedStatus.All, githubRepository: "all" } }); - const selectedOrganization = watch("githubOrganization"); - - const uniqueOrganizations = useMemo(() => { - const organizations = gitRisks.map((risk) => risk.repositoryFullName.split("/")[0]); - - return Array.from(new Set(organizations)); - }, [gitRisks]); - - const uniqueRepositories = useMemo(() => { - const repositories = gitRisks - .filter((risk) => risk.repositoryFullName.split("/")[0] === selectedOrganization) - .map((risk) => risk.repositoryFullName.split("/")[1]); - - return Array.from(new Set(repositories)); - }, [gitRisks, selectedOrganization]); - const onFormSubmit = async (data: TFormSchema) => { + const gitRisks = await mutateAsync({ + orgId: currentOrg.id, + filter: { + repositoryNames: data.githubRepository !== "all" ? [data.githubRepository] : undefined, + resolvedStatus: data.status + } + }); + const filteredRisks = gitRisks .filter((risk) => // eslint-disable-next-line no-nested-ternary - data.status === ExportStatus.All + data.status === SecretScanningResolvedStatus.All ? true - : data.status === ExportStatus.Resolved + : data.status === SecretScanningResolvedStatus.Resolved ? risk.isResolved : !risk.isResolved ) .filter((risk) => { - if (data.githubOrganization === "all") return true; - - if (data.githubRepository === "all") - return risk.repositoryFullName.split("/")[0] === data.githubOrganization; - - return risk.repositoryFullName === `${data.githubOrganization}/${data.githubRepository}`; + if (data.githubRepository === "all") return true; + return risk.repositoryFullName === data.githubRepository; }); const formattedRisks = filteredRisks.map((risk) => { @@ -148,27 +130,6 @@ export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, gitRisks }: P ]} > - ( - - - - )} - /> - ( - + @@ -212,29 +172,27 @@ export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, gitRisks }: P )} /> - {selectedOrganization && selectedOrganization !== "all" && ( - ( - - - - )} - /> - )} + ( + + + + )} + /> diff --git a/frontend/src/pages/organization/SecretScanningPage/components/SecretScanningFilters.tsx b/frontend/src/pages/organization/SecretScanningPage/components/SecretScanningFilters.tsx new file mode 100644 index 000000000..a9222b27b --- /dev/null +++ b/frontend/src/pages/organization/SecretScanningPage/components/SecretScanningFilters.tsx @@ -0,0 +1,89 @@ +import { Control, Controller } from "react-hook-form"; +import { twMerge } from "tailwind-merge"; + +import { Button, FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2"; +import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { SecretScanningFilterFormData } from "./types"; + +type Props = { + control: Control; + repositories: string[]; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["exportSecretScans"]>, + state?: boolean + ) => void; +}; + +export const SecretScanningFilter = ({ repositories, control, handlePopUpToggle }: Props) => { + return ( +
+
+ ( + + ({ + name: repository + }))} + getOptionValue={(option) => option.name} + getOptionLabel={(option) => option.name} + /> + + )} + /> + + ( + + + + )} + /> +
+ +
+ +
+
+ ); +}; diff --git a/frontend/src/pages/organization/SecretScanningPage/components/types.ts b/frontend/src/pages/organization/SecretScanningPage/components/types.ts new file mode 100644 index 000000000..f5fd5a594 --- /dev/null +++ b/frontend/src/pages/organization/SecretScanningPage/components/types.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types"; + +export const secretScanningFilterFormSchema = z.object({ + repositoryNames: z.array(z.object({ name: z.string() })), + resolved: z + .nativeEnum(SecretScanningResolvedStatus) + .default(SecretScanningResolvedStatus.All) + .optional() +}); + +export type SecretScanningFilterFormData = z.infer; + +export type SetValueType = ( + name: keyof SecretScanningFilterFormData, + value: any, + options?: { + shouldValidate?: boolean; + shouldDirty?: boolean; + } +) => void;