From bc74c44f9741c1bd18fe5b10e4b12d6bed259d54 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 19 Sep 2024 16:36:11 -0700 Subject: [PATCH] refactor: move overview resource env determination logic to the client side to preserve ordering of resources --- .../dynamic-secret/dynamic-secret-dal.ts | 13 +++- .../dynamic-secret/dynamic-secret-service.ts | 10 +-- .../src/server/routes/v3/dashboard-router.ts | 44 +++++-------- .../secret-folder/secret-folder-dal.ts | 12 +++- .../secret-folder/secret-folder-service.ts | 14 +---- .../secret-v2-bridge-service.ts | 14 +---- frontend/src/hooks/api/dashboard/queries.tsx | 15 +---- frontend/src/hooks/api/dashboard/types.ts | 8 +-- frontend/src/hooks/utils/secrets-overview.tsx | 63 ++++++++----------- .../IdentitySection/IdentityTable.tsx | 2 +- .../components/IdentityTab/IdentityTab.tsx | 2 +- .../views/SecretMainPage/SecretMainPage.tsx | 2 +- .../SecretOverviewPage/SecretOverviewPage.tsx | 18 ++---- 13 files changed, 80 insertions(+), 137 deletions(-) diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts index a7bfd7e55..2a0dba37d 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts @@ -36,14 +36,21 @@ export const dynamicSecretDALFactory = (db: TDbClient) => { .whereIn("folderId", folderIds) .where((bd) => { if (search) { - void bd.whereILike("name", `%${search}%`); + void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`); } }) + .leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`) + .leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .select( selectAllTableCols(TableName.DynamicSecret), - db.raw(`DENSE_RANK() OVER (ORDER BY "name" ${orderDirection ?? OrderByDirection.ASC}) as rank`) + db.ref("slug").withSchema(TableName.Environment).as("environment"), + db.raw( + `DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${ + orderDirection ?? OrderByDirection.ASC + }) as rank` + ) ) - .orderBy(orderBy, orderDirection); + .orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection); if (limit) { const rankOffset = offset + 1; diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 8f8e5dd5c..970397cad 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -486,15 +486,7 @@ export const dynamicSecretServiceFactory = ({ ...params }); - // map dynamic secrets to their respective env - const folderEnvMap: Map = new Map(folders.map((folder) => [folder.id, folder.environment.slug])); - const data: { [key: string]: Array<(typeof dynamicSecretCfg)[number]> } = {}; - dynamicSecretCfg.forEach((secret) => { - const slug = folderEnvMap.get(secret.folderId); - data[slug!] = [...(data[slug!] ?? []), secret]; - }); - - return data; + return dynamicSecretCfg; }; return { diff --git a/backend/src/server/routes/v3/dashboard-router.ts b/backend/src/server/routes/v3/dashboard-router.ts index a2ec8eb9a..684445698 100644 --- a/backend/src/server/routes/v3/dashboard-router.ts +++ b/backend/src/server/routes/v3/dashboard-router.ts @@ -73,25 +73,21 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - folders: z.record(z.string(), SecretFoldersSchema.array()).optional(), - dynamicSecrets: z.record(z.string(), SanitizedDynamicSecretSchema.array()).optional(), - secrets: z - .record( - z.string(), - secretRawSchema - .extend({ - secretPath: z.string().optional(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - color: true - }) - .extend({ name: z.string() }) - .array() - .optional() - }) + folders: SecretFoldersSchema.extend({ environment: z.string() }).array().optional(), + dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(), + secrets: secretRawSchema + .extend({ + secretPath: z.string().optional(), + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }) + .extend({ name: z.string() }) .array() - ) + .optional() + }) + .array() .optional(), totalFolderCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(), @@ -166,9 +162,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }); // get the count of unique folder names to properly adjust remaining limit - const uniqueFolderCount = new Set( - Object.values(folders).flatMap((folderGroup) => folderGroup.flatMap((folder) => folder.name)) - ).size; + const uniqueFolderCount = new Set(folders.map((folder) => folder.name)).size; remainingLimit -= uniqueFolderCount; adjustedOffset = 0; @@ -207,11 +201,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }); // get the count of unique dynamic secret names to properly adjust remaining limit - const uniqueDynamicSecretsCount = new Set( - Object.values(dynamicSecrets).flatMap((dynamicSecretGroup) => - dynamicSecretGroup.flatMap((dynamicSecret) => dynamicSecret.name) - ) - ).size; + const uniqueDynamicSecretsCount = new Set(dynamicSecrets.map((dynamicSecret) => dynamicSecret.name)).size; remainingLimit -= uniqueDynamicSecretsCount; adjustedOffset = 0; @@ -250,7 +240,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }); for await (const environment of environments) { - const secretCountFromEnv = secrets[environment]?.length; + const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length; if (secretCountFromEnv) { await server.services.auditLog.createAuditLog({ diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index 5142f93b9..6365bd824 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -411,14 +411,20 @@ export const secretFolderDALFactory = (db: TDbClient) => { .where("isReserved", false) .where((bd) => { if (search) { - void bd.whereILike("name", `%${search}%`); + void bd.whereILike(`${TableName.SecretFolder}.name`, `%${search}%`); } }) + .leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`) .select( selectAllTableCols(TableName.SecretFolder), - db.raw(`DENSE_RANK() OVER (ORDER BY "name" ${orderDirection ?? OrderByDirection.ASC}) as rank`) + db.raw( + `DENSE_RANK() OVER (ORDER BY ${TableName.SecretFolder}."name" ${ + orderDirection ?? OrderByDirection.ASC + }) as rank` + ), + db.ref("slug").withSchema(TableName.Environment).as("environment") ) - .orderBy(orderBy, orderDirection); + .orderBy(`${TableName.SecretFolder}.${orderBy}`, orderDirection); if (limit) { const rankOffset = offset + 1; // ranks start from 1 diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index 6450e158b..d63518a80 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import path from "path"; import { v4 as uuidv4, validate as uuidValidate } from "uuid"; -import { TSecretFolders, TSecretFoldersInsert } from "@app/db/schemas"; +import { TSecretFoldersInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; @@ -446,13 +446,12 @@ export const secretFolderServiceFactory = ({ await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); const envs = await projectEnvDAL.findBySlugs(projectId, environments); - const data: { [key: string]: TSecretFolders[] } = {}; if (!envs.length) throw new BadRequestError({ message: "Environment(s) not found", name: "get project folder count" }); const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath); - if (!parentFolders.length) return data; + if (!parentFolders.length) return []; const folders = await folderDAL.findByMultiEnv({ environmentIds: envs.map((env) => env.id), @@ -460,14 +459,7 @@ export const secretFolderServiceFactory = ({ ...params }); - // reorganize folders into respective envs - const envMap: Map = new Map(envs.map((env) => [env.id, env.slug])); - folders.forEach((folder) => { - const slug = envMap.get(folder.envId); - data[slug!] = [...(data[slug!] ?? []), folder]; - }); - - return data; + return folders; }; // get the unique count of folders within a project path diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 1d360602d..d7f895084 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -550,14 +550,8 @@ export const secretV2BridgeServiceFactory = ({ const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); - const data: { [key: string]: typeof decryptedSecrets } = {}; - if (!folders.length) { - environments.forEach((env) => { - data[env] = []; - }); - - return data; + return []; } paths = folders.map((folder) => ({ folderId: folder.id, path, environment: folder.environment.slug })); @@ -593,11 +587,7 @@ export const secretV2BridgeServiceFactory = ({ ) ); - decryptedSecrets.forEach((secret) => { - data[secret.environment] = [...(data[secret.environment] ?? []), secret]; - }); - - return data; + return decryptedSecrets; }; const getSecrets = async ({ diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index b33c86458..38c07b6cf 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -15,7 +15,6 @@ import { } from "@app/hooks/api/dashboard/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries"; -import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; export const dashboardKeys = { all: () => ["dashboard"] as const, @@ -170,21 +169,9 @@ export const useGetProjectSecretsOverview = ( select: useCallback((data: Awaited>) => { const { secrets, ...select } = data; - let sanitizedSecrets: Record> = {}; - - if (secrets) { - sanitizedSecrets = {}; - Object.entries(secrets).forEach(([env, rawSecrets]) => { - sanitizedSecrets[env] = mergePersonalSecrets(rawSecrets).reduce< - Record - >((prev, curr) => ({ ...prev, [curr.key]: curr }), {}); - }); - } return { ...select, - ...(secrets && { - secrets: sanitizedSecrets - }) + secrets: secrets ? mergePersonalSecrets(secrets) : undefined }; }, []), keepPreviousData: true diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 4d2f09945..08e75ee3b 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -5,9 +5,9 @@ import { TSecretImport } from "@app/hooks/api/secretImports/types"; import { SecretV3Raw, SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; export type DashboardProjectSecretsOverviewResponse = { - folders?: { [key: string]: TSecretFolder[] }; - dynamicSecrets?: { [key: string]: TDynamicSecret[] }; - secrets?: { [key: string]: SecretV3Raw[] }; + folders?: (TSecretFolder & { environment: string })[]; + dynamicSecrets?: (TDynamicSecret & { environment: string })[]; + secrets?: SecretV3Raw[]; totalSecretCount?: number; totalFolderCount?: number; totalDynamicSecretCount?: number; @@ -30,7 +30,7 @@ export type DashboardProjectSecretsOverview = Omit< DashboardProjectSecretsOverviewResponse, "secrets" > & { - secrets?: Record>; + secrets?: SecretV3RawSanitized[]; }; export type DashboardProjectSecretsDetails = Omit< diff --git a/frontend/src/hooks/utils/secrets-overview.tsx b/frontend/src/hooks/utils/secrets-overview.tsx index 26cd2d21a..b84fb3902 100644 --- a/frontend/src/hooks/utils/secrets-overview.tsx +++ b/frontend/src/hooks/utils/secrets-overview.tsx @@ -1,34 +1,32 @@ import { useCallback, useMemo } from "react"; import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types"; -import { OrderByDirection } from "@app/hooks/api/generic/types"; -export const useFolderOverview = ( - folders: DashboardProjectSecretsOverview["folders"], - orderDirection: OrderByDirection -) => { +export const useFolderOverview = (folders: DashboardProjectSecretsOverview["folders"]) => { const folderNames = useMemo(() => { const names = new Set(); - Object.values(folders ?? {})?.forEach((folderGroup) => { - folderGroup.forEach((folder) => { - names.add(folder.name); - }); + folders?.forEach((folder) => { + names.add(folder.name); }); - return [...names].sort((a, b) => - orderDirection === OrderByDirection.ASC ? a.localeCompare(b) : b.localeCompare(a) - ); + return [...names]; }, [folders]); const isFolderPresentInEnv = useCallback( (name: string, env: string) => { - return Boolean(folders?.[env]?.find(({ name: folderName }) => folderName === name)); + return Boolean( + folders?.find( + ({ name: folderName, environment }) => folderName === name && environment === env + ) + ); }, [folders] ); const getFolderByNameAndEnv = useCallback( (name: string, env: string) => { - return folders?.[env]?.find(({ name: folderName }) => folderName === name); + return folders?.find( + ({ name: folderName, environment }) => folderName === name && environment === env + ); }, [folders] ); @@ -37,25 +35,23 @@ export const useFolderOverview = ( }; export const useDynamicSecretOverview = ( - dynamicSecrets: DashboardProjectSecretsOverview["dynamicSecrets"], - orderDirection: OrderByDirection + dynamicSecrets: DashboardProjectSecretsOverview["dynamicSecrets"] ) => { const dynamicSecretNames = useMemo(() => { const names = new Set(); - Object.values(dynamicSecrets ?? {})?.forEach((folderGroup) => { - folderGroup.forEach((folder) => { - names.add(folder.name); - }); + dynamicSecrets?.forEach((dynamicSecret) => { + names.add(dynamicSecret.name); }); - return [...names].sort((a, b) => - orderDirection === OrderByDirection.ASC ? a.localeCompare(b) : b.localeCompare(a) - ); + return [...names]; }, [dynamicSecrets]); const isDynamicSecretPresentInEnv = useCallback( (name: string, env: string) => { return Boolean( - dynamicSecrets?.[env]?.find(({ name: dynamicSecretName }) => dynamicSecretName === name) + dynamicSecrets?.find( + ({ name: dynamicSecretName, environment }) => + dynamicSecretName === name && environment === env + ) ); }, [dynamicSecrets] @@ -64,32 +60,23 @@ export const useDynamicSecretOverview = ( return { dynamicSecretNames, isDynamicSecretPresentInEnv }; }; -export const useSecretOverview = ( - secrets: DashboardProjectSecretsOverview["secrets"], - orderDirection: OrderByDirection -) => { +export const useSecretOverview = (secrets: DashboardProjectSecretsOverview["secrets"]) => { const secKeys = useMemo(() => { const keys = new Set(); - Object.values(secrets ?? {})?.forEach((secretGroup) => { - Object.keys(secretGroup || {}).forEach((key) => { - keys.add(key); - }); - }); - return [...keys].sort((a, b) => - orderDirection === OrderByDirection.ASC ? a.localeCompare(b) : b.localeCompare(a) - ); + secrets?.forEach((secret) => keys.add(secret.key)); + return [...keys]; }, [secrets]); const getEnvSecretKeyCount = useCallback( (env: string) => { - return Object.keys(secrets?.[env] || {}).length; + return secrets?.filter((secret) => secret.env === env).length ?? 0; }, [secrets] ); const getSecretByKey = useCallback( (env: string, key: string) => { - const sec = secrets?.[env]?.[key]; + const sec = secrets?.find((s) => s.env === env && s.key === key); return sec; }, [secrets] diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index a6e52f7c1..2f7768100 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -277,7 +277,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { })} - {!isLoading && data && data.totalCount > INIT_PER_PAGE && ( + {!isLoading && data && ( - {!isLoading && data && data.totalCount > INIT_PER_PAGE && ( + {!isLoading && data && ( { {!canReadSecret && folders?.length === 0 && } - {!isDetailsLoading && totalCount > INIT_PER_PAGE && ( + {!isDetailsLoading && ( { totalDynamicSecretCount } = overview ?? {}; - const { folderNames, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview( - folders, - orderDirection - ); + const { folderNames, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview(folders); - const { dynamicSecretNames, isDynamicSecretPresentInEnv } = useDynamicSecretOverview( - dynamicSecrets, - orderDirection - ); + const { dynamicSecretNames, isDynamicSecretPresentInEnv } = + useDynamicSecretOverview(dynamicSecrets); - const { secKeys, getSecretByKey, getEnvSecretKeyCount } = useSecretOverview( - secrets, - orderDirection - ); + const { secKeys, getSecretByKey, getEnvSecretKeyCount } = useSecretOverview(secrets); const { mutateAsync: createSecretV3 } = useCreateSecretV3(); const { mutateAsync: updateSecretV3 } = useUpdateSecretV3(); @@ -961,7 +953,7 @@ export const SecretOverviewPage = () => { - {!isOverviewLoading && totalCount > INIT_PER_PAGE && ( + {!isOverviewLoading && (