improvement: add secret tag/metadata search functionality to single env view dashboard

This commit is contained in:
Scott Wilson
2025-07-18 18:22:11 -07:00
parent b7b059bb50
commit 90c341cf53
6 changed files with 48 additions and 13 deletions
@@ -904,7 +904,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
projectId, projectId,
path: secretPath, path: secretPath,
search, search,
tagSlugs: tags tagSlugs: tags,
includeTagsInSearch: true,
includeMetadataInSearch: true
}); });
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
@@ -924,7 +926,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
search, search,
limit: remainingLimit, limit: remainingLimit,
offset: adjustedOffset, offset: adjustedOffset,
tagSlugs: tags tagSlugs: tags,
includeTagsInSearch: true,
includeMetadataInSearch: true
}) })
).secrets; ).secrets;
} }
@@ -415,6 +415,8 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
filters?: { filters?: {
search?: string; search?: string;
tagSlugs?: string[]; tagSlugs?: string[];
includeTagsInSearch?: boolean;
includeMetadataInSearch?: boolean;
} }
) => { ) => {
try { try {
@@ -433,17 +435,27 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
.whereIn("folderId", folderIds) .whereIn("folderId", folderIds)
.where((bd) => { .where((bd) => {
if (filters?.search) { 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) => { .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 // only need to join tags if filtering by tag slugs
const slugs = filters?.tagSlugs?.filter(Boolean); const slugs = filters?.tagSlugs?.filter(Boolean);
if (slugs && slugs.length > 0) { if ((slugs && slugs.length > 0) || filters?.includeTagsInSearch) {
void query void query
.leftJoin( .leftJoin(
TableName.SecretV2JnTag, TableName.SecretV2JnTag,
@@ -454,12 +466,24 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
TableName.SecretTag, TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${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; const secrets = await query;
// @ts-expect-error not inferred by knex
return Number(secrets[0]?.count ?? 0); return Number(secrets[0]?.count ?? 0);
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "get folder secret count" }); throw new DatabaseError({ error, name: "get folder secret count" });
@@ -485,12 +509,14 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
.whereIn(`${TableName.SecretV2}.folderId`, folderIds) .whereIn(`${TableName.SecretV2}.folderId`, folderIds)
.where((bd) => { .where((bd) => {
if (filters?.search) { if (filters?.search) {
void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`);
if (filters?.includeTagsInSearch) { if (filters?.includeTagsInSearch) {
void bd.orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`);
}
if (filters?.includeMetadataInSearch) {
void bd void bd
.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`) .orWhereILike(`${TableName.ResourceMetadata}.key`, `%${filters?.search}%`)
.orWhereILike(`${TableName.SecretTag}.slug`, `%${filters?.search}%`); .orWhereILike(`${TableName.ResourceMetadata}.value`, `%${filters?.search}%`);
} else {
void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`);
} }
} }
@@ -355,6 +355,7 @@ export type TFindSecretsByFolderIdsFilter = {
tagSlugs?: string[]; tagSlugs?: string[];
metadataFilter?: { key?: string; value?: string }[]; metadataFilter?: { key?: string; value?: string }[];
includeTagsInSearch?: boolean; includeTagsInSearch?: boolean;
includeMetadataInSearch?: boolean;
keys?: string[]; keys?: string[];
}; };
@@ -1128,6 +1128,8 @@ export const secretServiceFactory = ({
| "environment" | "environment"
| "tagSlugs" | "tagSlugs"
| "search" | "search"
| "includeTagsInSearch"
| "includeMetadataInSearch"
>) => { >) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
@@ -212,6 +212,8 @@ export type TGetSecretsRawDTO = {
limit?: number; limit?: number;
search?: string; search?: string;
keys?: string[]; keys?: string[];
includeTagsInSearch?: boolean;
includeMetadataInSearch?: boolean;
} & TProjectPermission; } & TProjectPermission;
export type TGetSecretAccessListDTO = { export type TGetSecretAccessListDTO = {
@@ -53,7 +53,7 @@ export const SecretSearchInput = ({
}} }}
autoComplete="off" 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" 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} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
/> />