From 90c341cf536a6d8c03e835be0ef0dfa0aa8f27fd Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 18 Jul 2025 18:22:11 -0700 Subject: [PATCH] improvement: add secret tag/metadata search functionality to single env view dashboard --- .../src/server/routes/v1/dashboard-router.ts | 8 +++- .../secret-v2-bridge/secret-v2-bridge-dal.ts | 46 +++++++++++++++---- .../secret-v2-bridge-types.ts | 1 + backend/src/services/secret/secret-service.ts | 2 + backend/src/services/secret/secret-types.ts | 2 + .../SecretSearchInput/SecretSearchInput.tsx | 2 +- 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index c466be087..26b41b586 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -904,7 +904,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { projectId, path: secretPath, search, - tagSlugs: tags + tagSlugs: tags, + includeTagsInSearch: true, + includeMetadataInSearch: true }); if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { @@ -924,7 +926,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { search, limit: remainingLimit, offset: adjustedOffset, - tagSlugs: tags + tagSlugs: tags, + includeTagsInSearch: true, + includeMetadataInSearch: true }) ).secrets; } diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index cd2773172..1047a09cb 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -415,6 +415,8 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { filters?: { search?: string; tagSlugs?: string[]; + includeTagsInSearch?: boolean; + includeMetadataInSearch?: boolean; } ) => { try { @@ -433,17 +435,27 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { .whereIn("folderId", folderIds) .where((bd) => { if (filters?.search) { - void bd.whereILike("key", `%${filters?.search}%`); + void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`); + if (filters?.includeTagsInSearch) { + void bd.orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`); + } + if (filters?.includeMetadataInSearch) { + void bd + .orWhereILike(`${TableName.ResourceMetadata}.key`, `%${filters?.search}%`) + .orWhereILike(`${TableName.ResourceMetadata}.value`, `%${filters?.search}%`); + } } }) .where((bd) => { - void bd.whereNull("userId").orWhere({ userId: userId || null }); + void bd + .whereNull(`${TableName.SecretV2}.userId`) + .orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null }); }) - .countDistinct("key"); + .countDistinct(`${TableName.SecretV2}.key`); // only need to join tags if filtering by tag slugs const slugs = filters?.tagSlugs?.filter(Boolean); - if (slugs && slugs.length > 0) { + if ((slugs && slugs.length > 0) || filters?.includeTagsInSearch) { void query .leftJoin( TableName.SecretV2JnTag, @@ -454,12 +466,24 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { TableName.SecretTag, `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` - ) - .whereIn("slug", slugs); + ); + + if (slugs?.length) { + void query.whereIn("slug", slugs); + } + } + + if (filters?.includeMetadataInSearch) { + void query.leftJoin( + TableName.ResourceMetadata, + `${TableName.SecretV2}.id`, + `${TableName.ResourceMetadata}.secretId` + ); } const secrets = await query; + // @ts-expect-error not inferred by knex return Number(secrets[0]?.count ?? 0); } catch (error) { throw new DatabaseError({ error, name: "get folder secret count" }); @@ -485,12 +509,14 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { .whereIn(`${TableName.SecretV2}.folderId`, folderIds) .where((bd) => { if (filters?.search) { + void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`); if (filters?.includeTagsInSearch) { + void bd.orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`); + } + if (filters?.includeMetadataInSearch) { void bd - .whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`) - .orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`); - } else { - void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`); + .orWhereILike(`${TableName.ResourceMetadata}.key`, `%${filters?.search}%`) + .orWhereILike(`${TableName.ResourceMetadata}.value`, `%${filters?.search}%`); } } diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index f4171b1a7..ce994225b 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -355,6 +355,7 @@ export type TFindSecretsByFolderIdsFilter = { tagSlugs?: string[]; metadataFilter?: { key?: string; value?: string }[]; includeTagsInSearch?: boolean; + includeMetadataInSearch?: boolean; keys?: string[]; }; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 084423fa9..a4a168d62 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1128,6 +1128,8 @@ export const secretServiceFactory = ({ | "environment" | "tagSlugs" | "search" + | "includeTagsInSearch" + | "includeMetadataInSearch" >) => { const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 12b8e7175..afa8d9569 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -212,6 +212,8 @@ export type TGetSecretsRawDTO = { limit?: number; search?: string; keys?: string[]; + includeTagsInSearch?: boolean; + includeMetadataInSearch?: boolean; } & TProjectPermission; export type TGetSecretAccessListDTO = { diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/SecretSearchInput.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/SecretSearchInput.tsx index 9c8562e06..9a69bfb9c 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/SecretSearchInput.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/SecretSearchInput.tsx @@ -53,7 +53,7 @@ export const SecretSearchInput = ({ }} autoComplete="off" className="input text-md h-[2.3rem] w-full rounded-md rounded-l-none bg-mineshaft-800 py-[0.375rem] pl-2.5 pr-8 text-gray-400 placeholder-mineshaft-50 placeholder-opacity-50 outline-none duration-200 placeholder:text-sm hover:ring-bunker-400/60 focus:bg-mineshaft-700/80 focus:ring-1 focus:ring-primary-400/50" - placeholder="Search by secret/folder name..." + placeholder="Search by secret, folder, tag or metadata..." value={value} onChange={(e) => onChange(e.target.value)} />