refactor: move overview resource env determination logic to the client side to preserve ordering of resources

This commit is contained in:
Scott Wilson
2024-09-19 16:36:11 -07:00
parent d0656358a2
commit bc74c44f97
13 changed files with 80 additions and 137 deletions
@@ -36,14 +36,21 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
.whereIn("folderId", folderIds) .whereIn("folderId", folderIds)
.where((bd) => { .where((bd) => {
if (search) { 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( .select(
selectAllTableCols(TableName.DynamicSecret), 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) { if (limit) {
const rankOffset = offset + 1; const rankOffset = offset + 1;
@@ -486,15 +486,7 @@ export const dynamicSecretServiceFactory = ({
...params ...params
}); });
// map dynamic secrets to their respective env return dynamicSecretCfg;
const folderEnvMap: Map<string, string> = 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 { return {
@@ -73,25 +73,21 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
folders: z.record(z.string(), SecretFoldersSchema.array()).optional(), folders: SecretFoldersSchema.extend({ environment: z.string() }).array().optional(),
dynamicSecrets: z.record(z.string(), SanitizedDynamicSecretSchema.array()).optional(), dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(),
secrets: z secrets: secretRawSchema
.record( .extend({
z.string(), secretPath: z.string().optional(),
secretRawSchema tags: SecretTagsSchema.pick({
.extend({ id: true,
secretPath: z.string().optional(), slug: true,
tags: SecretTagsSchema.pick({ color: true
id: true, })
slug: true, .extend({ name: z.string() })
color: true
})
.extend({ name: z.string() })
.array()
.optional()
})
.array() .array()
) .optional()
})
.array()
.optional(), .optional(),
totalFolderCount: z.number().optional(), totalFolderCount: z.number().optional(),
totalDynamicSecretCount: 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 // get the count of unique folder names to properly adjust remaining limit
const uniqueFolderCount = new Set( const uniqueFolderCount = new Set(folders.map((folder) => folder.name)).size;
Object.values(folders).flatMap((folderGroup) => folderGroup.flatMap((folder) => folder.name))
).size;
remainingLimit -= uniqueFolderCount; remainingLimit -= uniqueFolderCount;
adjustedOffset = 0; 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 // get the count of unique dynamic secret names to properly adjust remaining limit
const uniqueDynamicSecretsCount = new Set( const uniqueDynamicSecretsCount = new Set(dynamicSecrets.map((dynamicSecret) => dynamicSecret.name)).size;
Object.values(dynamicSecrets).flatMap((dynamicSecretGroup) =>
dynamicSecretGroup.flatMap((dynamicSecret) => dynamicSecret.name)
)
).size;
remainingLimit -= uniqueDynamicSecretsCount; remainingLimit -= uniqueDynamicSecretsCount;
adjustedOffset = 0; adjustedOffset = 0;
@@ -250,7 +240,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}); });
for await (const environment of environments) { for await (const environment of environments) {
const secretCountFromEnv = secrets[environment]?.length; const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
if (secretCountFromEnv) { if (secretCountFromEnv) {
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -411,14 +411,20 @@ export const secretFolderDALFactory = (db: TDbClient) => {
.where("isReserved", false) .where("isReserved", false)
.where((bd) => { .where((bd) => {
if (search) { if (search) {
void bd.whereILike("name", `%${search}%`); void bd.whereILike(`${TableName.SecretFolder}.name`, `%${search}%`);
} }
}) })
.leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`)
.select( .select(
selectAllTableCols(TableName.SecretFolder), 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) { if (limit) {
const rankOffset = offset + 1; // ranks start from 1 const rankOffset = offset + 1; // ranks start from 1
@@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import path from "path"; import path from "path";
import { v4 as uuidv4, validate as uuidValidate } from "uuid"; 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; 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); await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
const envs = await projectEnvDAL.findBySlugs(projectId, environments); const envs = await projectEnvDAL.findBySlugs(projectId, environments);
const data: { [key: string]: TSecretFolders[] } = {};
if (!envs.length) if (!envs.length)
throw new BadRequestError({ message: "Environment(s) not found", name: "get project folder count" }); throw new BadRequestError({ message: "Environment(s) not found", name: "get project folder count" });
const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath); const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
if (!parentFolders.length) return data; if (!parentFolders.length) return [];
const folders = await folderDAL.findByMultiEnv({ const folders = await folderDAL.findByMultiEnv({
environmentIds: envs.map((env) => env.id), environmentIds: envs.map((env) => env.id),
@@ -460,14 +459,7 @@ export const secretFolderServiceFactory = ({
...params ...params
}); });
// reorganize folders into respective envs return folders;
const envMap: Map<string, string> = 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;
}; };
// get the unique count of folders within a project path // get the unique count of folders within a project path
@@ -550,14 +550,8 @@ export const secretV2BridgeServiceFactory = ({
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
const data: { [key: string]: typeof decryptedSecrets } = {};
if (!folders.length) { if (!folders.length) {
environments.forEach((env) => { return [];
data[env] = [];
});
return data;
} }
paths = folders.map((folder) => ({ folderId: folder.id, path, environment: folder.environment.slug })); paths = folders.map((folder) => ({ folderId: folder.id, path, environment: folder.environment.slug }));
@@ -593,11 +587,7 @@ export const secretV2BridgeServiceFactory = ({
) )
); );
decryptedSecrets.forEach((secret) => { return decryptedSecrets;
data[secret.environment] = [...(data[secret.environment] ?? []), secret];
});
return data;
}; };
const getSecrets = async ({ const getSecrets = async ({
+1 -14
View File
@@ -15,7 +15,6 @@ import {
} from "@app/hooks/api/dashboard/types"; } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries"; import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
export const dashboardKeys = { export const dashboardKeys = {
all: () => ["dashboard"] as const, all: () => ["dashboard"] as const,
@@ -170,21 +169,9 @@ export const useGetProjectSecretsOverview = (
select: useCallback((data: Awaited<ReturnType<typeof fetchProjectSecretsOverview>>) => { select: useCallback((data: Awaited<ReturnType<typeof fetchProjectSecretsOverview>>) => {
const { secrets, ...select } = data; const { secrets, ...select } = data;
let sanitizedSecrets: Record<string, Record<string, SecretV3RawSanitized>> = {};
if (secrets) {
sanitizedSecrets = {};
Object.entries(secrets).forEach(([env, rawSecrets]) => {
sanitizedSecrets[env] = mergePersonalSecrets(rawSecrets).reduce<
Record<string, SecretV3RawSanitized>
>((prev, curr) => ({ ...prev, [curr.key]: curr }), {});
});
}
return { return {
...select, ...select,
...(secrets && { secrets: secrets ? mergePersonalSecrets(secrets) : undefined
secrets: sanitizedSecrets
})
}; };
}, []), }, []),
keepPreviousData: true keepPreviousData: true
+4 -4
View File
@@ -5,9 +5,9 @@ import { TSecretImport } from "@app/hooks/api/secretImports/types";
import { SecretV3Raw, SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { SecretV3Raw, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
export type DashboardProjectSecretsOverviewResponse = { export type DashboardProjectSecretsOverviewResponse = {
folders?: { [key: string]: TSecretFolder[] }; folders?: (TSecretFolder & { environment: string })[];
dynamicSecrets?: { [key: string]: TDynamicSecret[] }; dynamicSecrets?: (TDynamicSecret & { environment: string })[];
secrets?: { [key: string]: SecretV3Raw[] }; secrets?: SecretV3Raw[];
totalSecretCount?: number; totalSecretCount?: number;
totalFolderCount?: number; totalFolderCount?: number;
totalDynamicSecretCount?: number; totalDynamicSecretCount?: number;
@@ -30,7 +30,7 @@ export type DashboardProjectSecretsOverview = Omit<
DashboardProjectSecretsOverviewResponse, DashboardProjectSecretsOverviewResponse,
"secrets" "secrets"
> & { > & {
secrets?: Record<string, Record<string, SecretV3RawSanitized>>; secrets?: SecretV3RawSanitized[];
}; };
export type DashboardProjectSecretsDetails = Omit< export type DashboardProjectSecretsDetails = Omit<
+25 -38
View File
@@ -1,34 +1,32 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types"; import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
export const useFolderOverview = ( export const useFolderOverview = (folders: DashboardProjectSecretsOverview["folders"]) => {
folders: DashboardProjectSecretsOverview["folders"],
orderDirection: OrderByDirection
) => {
const folderNames = useMemo(() => { const folderNames = useMemo(() => {
const names = new Set<string>(); const names = new Set<string>();
Object.values(folders ?? {})?.forEach((folderGroup) => { folders?.forEach((folder) => {
folderGroup.forEach((folder) => { names.add(folder.name);
names.add(folder.name);
});
}); });
return [...names].sort((a, b) => return [...names];
orderDirection === OrderByDirection.ASC ? a.localeCompare(b) : b.localeCompare(a)
);
}, [folders]); }, [folders]);
const isFolderPresentInEnv = useCallback( const isFolderPresentInEnv = useCallback(
(name: string, env: string) => { (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] [folders]
); );
const getFolderByNameAndEnv = useCallback( const getFolderByNameAndEnv = useCallback(
(name: string, env: string) => { (name: string, env: string) => {
return folders?.[env]?.find(({ name: folderName }) => folderName === name); return folders?.find(
({ name: folderName, environment }) => folderName === name && environment === env
);
}, },
[folders] [folders]
); );
@@ -37,25 +35,23 @@ export const useFolderOverview = (
}; };
export const useDynamicSecretOverview = ( export const useDynamicSecretOverview = (
dynamicSecrets: DashboardProjectSecretsOverview["dynamicSecrets"], dynamicSecrets: DashboardProjectSecretsOverview["dynamicSecrets"]
orderDirection: OrderByDirection
) => { ) => {
const dynamicSecretNames = useMemo(() => { const dynamicSecretNames = useMemo(() => {
const names = new Set<string>(); const names = new Set<string>();
Object.values(dynamicSecrets ?? {})?.forEach((folderGroup) => { dynamicSecrets?.forEach((dynamicSecret) => {
folderGroup.forEach((folder) => { names.add(dynamicSecret.name);
names.add(folder.name);
});
}); });
return [...names].sort((a, b) => return [...names];
orderDirection === OrderByDirection.ASC ? a.localeCompare(b) : b.localeCompare(a)
);
}, [dynamicSecrets]); }, [dynamicSecrets]);
const isDynamicSecretPresentInEnv = useCallback( const isDynamicSecretPresentInEnv = useCallback(
(name: string, env: string) => { (name: string, env: string) => {
return Boolean( return Boolean(
dynamicSecrets?.[env]?.find(({ name: dynamicSecretName }) => dynamicSecretName === name) dynamicSecrets?.find(
({ name: dynamicSecretName, environment }) =>
dynamicSecretName === name && environment === env
)
); );
}, },
[dynamicSecrets] [dynamicSecrets]
@@ -64,32 +60,23 @@ export const useDynamicSecretOverview = (
return { dynamicSecretNames, isDynamicSecretPresentInEnv }; return { dynamicSecretNames, isDynamicSecretPresentInEnv };
}; };
export const useSecretOverview = ( export const useSecretOverview = (secrets: DashboardProjectSecretsOverview["secrets"]) => {
secrets: DashboardProjectSecretsOverview["secrets"],
orderDirection: OrderByDirection
) => {
const secKeys = useMemo(() => { const secKeys = useMemo(() => {
const keys = new Set<string>(); const keys = new Set<string>();
Object.values(secrets ?? {})?.forEach((secretGroup) => { secrets?.forEach((secret) => keys.add(secret.key));
Object.keys(secretGroup || {}).forEach((key) => { return [...keys];
keys.add(key);
});
});
return [...keys].sort((a, b) =>
orderDirection === OrderByDirection.ASC ? a.localeCompare(b) : b.localeCompare(a)
);
}, [secrets]); }, [secrets]);
const getEnvSecretKeyCount = useCallback( const getEnvSecretKeyCount = useCallback(
(env: string) => { (env: string) => {
return Object.keys(secrets?.[env] || {}).length; return secrets?.filter((secret) => secret.env === env).length ?? 0;
}, },
[secrets] [secrets]
); );
const getSecretByKey = useCallback( const getSecretByKey = useCallback(
(env: string, key: string) => { (env: string, key: string) => {
const sec = secrets?.[env]?.[key]; const sec = secrets?.find((s) => s.env === env && s.key === key);
return sec; return sec;
}, },
[secrets] [secrets]
@@ -277,7 +277,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
})} })}
</TBody> </TBody>
</Table> </Table>
{!isLoading && data && data.totalCount > INIT_PER_PAGE && ( {!isLoading && data && (
<Pagination <Pagination
count={data.totalCount} count={data.totalCount}
page={page} page={page}
@@ -369,7 +369,7 @@ export const IdentityTab = withProjectPermission(
})} })}
</TBody> </TBody>
</Table> </Table>
{!isLoading && data && data.totalCount > INIT_PER_PAGE && ( {!isLoading && data && (
<Pagination <Pagination
count={data.totalCount} count={data.totalCount}
page={page} page={page}
@@ -358,7 +358,7 @@ export const SecretMainPage = () => {
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />} {!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
</div> </div>
</div> </div>
{!isDetailsLoading && totalCount > INIT_PER_PAGE && ( {!isDetailsLoading && (
<Pagination <Pagination
startAdornment={ startAdornment={
<SecretTableResourceCount <SecretTableResourceCount
@@ -242,20 +242,12 @@ export const SecretOverviewPage = () => {
totalDynamicSecretCount totalDynamicSecretCount
} = overview ?? {}; } = overview ?? {};
const { folderNames, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview( const { folderNames, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview(folders);
folders,
orderDirection
);
const { dynamicSecretNames, isDynamicSecretPresentInEnv } = useDynamicSecretOverview( const { dynamicSecretNames, isDynamicSecretPresentInEnv } =
dynamicSecrets, useDynamicSecretOverview(dynamicSecrets);
orderDirection
);
const { secKeys, getSecretByKey, getEnvSecretKeyCount } = useSecretOverview( const { secKeys, getSecretByKey, getEnvSecretKeyCount } = useSecretOverview(secrets);
secrets,
orderDirection
);
const { mutateAsync: createSecretV3 } = useCreateSecretV3(); const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3(); const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
@@ -961,7 +953,7 @@ export const SecretOverviewPage = () => {
</Tr> </Tr>
</TFoot> </TFoot>
</Table> </Table>
{!isOverviewLoading && totalCount > INIT_PER_PAGE && ( {!isOverviewLoading && (
<Pagination <Pagination
startAdornment={ startAdornment={
<SecretTableResourceCount <SecretTableResourceCount