diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts
index 5a52d4748..846e25eb8 100644
--- a/backend/src/server/routes/v1/dashboard-router.ts
+++ b/backend/src/server/routes/v1/dashboard-router.ts
@@ -166,6 +166,14 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
})
.array()
.optional(),
+ usedBySecretSyncs: z
+ .object({
+ name: z.string(),
+ destination: z.string(),
+ environment: z.string()
+ })
+ .array()
+ .optional(),
totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(),
@@ -500,6 +508,21 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
}
+ const usedBySecretSyncs: { name: string; destination: string; environment: string }[] = [];
+ for await (const environment of environments) {
+ const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath(
+ { projectId, secretPath, environment },
+ req.permission
+ );
+ secretSyncs.forEach((sync) => {
+ usedBySecretSyncs.push({
+ name: sync.name,
+ destination: sync.destination,
+ environment
+ });
+ });
+ }
+
return {
folders,
dynamicSecrets,
@@ -512,6 +535,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalSecretCount,
totalSecretRotationCount,
importedByEnvs,
+ usedBySecretSyncs,
totalCount:
(totalFolderCount ?? 0) +
(totalDynamicSecretCount ?? 0) +
@@ -605,6 +629,14 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(),
+ usedBySecretSyncs: z
+ .object({
+ name: z.string(),
+ destination: z.string(),
+ environment: z.string()
+ })
+ .array()
+ .optional(),
importedBy: z
.object({
environment: z.object({
@@ -898,6 +930,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets
});
+ const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath(
+ { projectId, secretPath, environment },
+ req.permission
+ );
+ const usedBySecretSyncs = secretSyncs.map((sync) => ({
+ name: sync.name,
+ destination: sync.destination,
+ environment: environment
+ }));
+
if (secrets?.length || secretRotations?.length) {
const secretCount =
(secrets?.length ?? 0) +
@@ -944,6 +986,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalSecretCount,
totalSecretRotationCount,
importedBy,
+ usedBySecretSyncs,
totalCount:
(totalImportCount ?? 0) +
(totalFolderCount ?? 0) +
diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts
index 14a1a1cf0..a19ebe6f7 100644
--- a/backend/src/services/secret-sync/secret-sync-service.ts
+++ b/backend/src/services/secret-sync/secret-sync-service.ts
@@ -23,6 +23,7 @@ import {
TDeleteSecretSyncDTO,
TFindSecretSyncByIdDTO,
TFindSecretSyncByNameDTO,
+ TListSecretSyncsByFolderId,
TListSecretSyncsByProjectId,
TSecretSync,
TTriggerSecretSyncImportSecretsByIdDTO,
@@ -85,6 +86,34 @@ export const secretSyncServiceFactory = ({
return secretSyncs as TSecretSync[];
};
+ const listSecretSyncsBySecretPath = async (
+ { projectId, secretPath, environment, destination }: TListSecretSyncsByFolderId,
+ actor: OrgServiceActor
+ ) => {
+ const { permission } = await permissionService.getProjectPermission({
+ actor: actor.type,
+ actorId: actor.id,
+ actorAuthMethod: actor.authMethod,
+ actorOrgId: actor.orgId,
+ actionProjectType: ActionProjectType.SecretManager,
+ projectId
+ });
+
+ if (permission.cannot(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs)) {
+ return [];
+ }
+
+ const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
+ if (!folder) return [];
+
+ const secretSyncs = await secretSyncDAL.find({
+ ...(destination && { destination }),
+ folderId: folder.id
+ });
+
+ return secretSyncs as TSecretSync[];
+ };
+
const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => {
const secretSync = await secretSyncDAL.findById(syncId);
@@ -518,6 +547,7 @@ export const secretSyncServiceFactory = ({
return {
listSecretSyncOptions,
listSecretSyncsByProjectId,
+ listSecretSyncsBySecretPath,
findSecretSyncById,
findSecretSyncByName,
createSecretSync,
diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts
index 03e92a57a..7787fe166 100644
--- a/backend/src/services/secret-sync/secret-sync-types.ts
+++ b/backend/src/services/secret-sync/secret-sync-types.ts
@@ -134,6 +134,13 @@ export type TListSecretSyncsByProjectId = {
destination?: SecretSync;
};
+export type TListSecretSyncsByFolderId = {
+ projectId: string;
+ secretPath: string;
+ environment: string;
+ destination?: SecretSync;
+};
+
export type TFindSecretSyncByIdDTO = {
syncId: string;
destination: SecretSync;
diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts
index e80874f8e..4315faecc 100644
--- a/frontend/src/hooks/api/dashboard/types.ts
+++ b/frontend/src/hooks/api/dashboard/types.ts
@@ -25,6 +25,7 @@ export type DashboardProjectSecretsOverviewResponse = {
totalUniqueFoldersInPage: number;
totalUniqueSecretImportsInPage: number;
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
+ usedBySecretSyncs?: { name: string; destination: string; environment: string }[];
totalUniqueSecretRotationsInPage: number;
};
@@ -43,6 +44,7 @@ export type DashboardProjectSecretsDetailsResponse = {
totalSecretRotationCount?: number;
totalCount: number;
importedBy?: ProjectSecretsImportedBy[];
+ usedBySecretSyncs?: { name: string; destination: string; environment: string }[];
};
export type ProjectSecretsImportedBy = {
diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
index f974a7f1a..6d30af10b 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
@@ -274,7 +274,8 @@ export const OverviewPage = () => {
totalUniqueSecretImportsInPage,
totalUniqueDynamicSecretsInPage,
totalUniqueSecretRotationsInPage,
- importedByEnvs
+ importedByEnvs,
+ usedBySecretSyncs
} = overview ?? {};
const secretImportsShaped = secretImports
@@ -1045,6 +1046,7 @@ export const OverviewPage = () => {
selectedEntries={selectedEntries}
resetSelectedEntries={resetSelectedEntries}
importedByEnvs={importedByEnvs}
+ usedBySecretSyncs={usedBySecretSyncs}
/>
>;
};
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
+ usedBySecretSyncs?: { name: string; destination: string; environment: string }[];
};
export const SelectionPanel = ({
secretPath,
resetSelectedEntries,
selectedEntries,
- importedByEnvs
+ importedByEnvs,
+ usedBySecretSyncs
}: Props) => {
const { permission } = useProjectPermission();
@@ -156,6 +158,12 @@ export const SelectionPanel = ({
);
}, [importedByEnvs, selectedEntries, selectedKeysCount]);
+ const usedBySecretSyncsFiltered = useMemo(() => {
+ if (selectedKeysCount === 0 || !usedBySecretSyncs) return null;
+ const envs = Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries));
+ return usedBySecretSyncs.filter((syncItem) => envs.includes(syncItem.environment));
+ }, [selectedEntries, usedBySecretSyncs, selectedKeysCount]);
+
const getDeleteModalTitle = () => {
if (selectedFolderCount > 0 && selectedKeysCount > 0) {
return "Do you want to delete the selected secrets and folders across environments?";
@@ -331,6 +339,7 @@ export const SelectionPanel = ({
)
}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
index 63398d152..d28f28392 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx
@@ -220,6 +220,7 @@ const Page = () => {
totalSecretCount = 0,
totalCount = 0,
importedBy,
+ usedBySecretSyncs,
totalSecretRotationCount = 0
} = data ?? {};
@@ -441,6 +442,7 @@ const Page = () => {
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
protectedBranchPolicyName={boardPolicy?.name}
importedBy={importedBy}
+ usedBySecretSyncs={usedBySecretSyncs}
/>
@@ -530,6 +532,7 @@ const Page = () => {
secretPath={secretPath}
isProtectedBranch={isProtectedBranch}
importedBy={importedBy}
+ usedBySecretSyncs={usedBySecretSyncs}
/>
)}
{noAccessSecretCount > 0 && }
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
index 4ac4ac804..92dca2023 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
@@ -113,6 +113,7 @@ type Props = {
onVisibilityToggle: () => void;
onToggleRowType: (rowType: RowType) => void;
onClickRollbackMode: () => void;
+ usedBySecretSyncs?: { environment: string; name: string; destination: string }[];
importedBy?: {
environment: { name: string; slug: string };
folders: {
@@ -139,7 +140,8 @@ export const ActionBar = ({
onClickRollbackMode,
onToggleRowType,
protectedBranchPolicyName,
- importedBy
+ importedBy,
+ usedBySecretSyncs
}: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"addFolder",
@@ -1076,6 +1078,7 @@ export const ActionBar = ({
s.key)}
+ usedBySecretSyncs={usedBySecretSyncs}
/>
)
}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx
index 9a66b23f5..7cb035e79 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx
@@ -1,5 +1,6 @@
+/* eslint-disable no-nested-ternary */
import React, { useMemo } from "react";
-import { faFileImport, faKey, faWarning } from "@fortawesome/free-solid-svg-icons";
+import { faFileImport, faKey, faSync, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Table, TBody, Td, Th, THead, Tr } from "@app/components/v2";
@@ -7,7 +8,8 @@ import { useWorkspace } from "@app/context";
enum ItemType {
Folder = "Folder",
- Secret = "Secret"
+ Secret = "Secret",
+ SecretSync = "SecretSync"
}
interface FlatItem {
@@ -28,11 +30,19 @@ interface CollapsibleSecretImportsProps {
isImported: boolean;
}[];
}[];
+ usedBySecretSyncs?:
+ | {
+ name: string;
+ destination: string;
+ environment: string;
+ }[]
+ | null;
secretsToDelete: string[];
}
export const CollapsibleSecretImports: React.FC = ({
importedBy = [],
+ usedBySecretSyncs = [],
secretsToDelete
}) => {
const { currentWorkspace } = useWorkspace();
@@ -51,6 +61,14 @@ export const CollapsibleSecretImports: React.FC =
};
const handlePathClick = (item: FlatItem) => {
+ if (item.type === ItemType.SecretSync) {
+ window.open(
+ `/secret-manager/${currentWorkspace.id}/integrations?selectedTab=secret-syncs`,
+ "_blank"
+ );
+ return;
+ }
+
let pathToNavigate;
if (item.type === ItemType.Folder) {
pathToNavigate = item.path;
@@ -103,7 +121,24 @@ export const CollapsibleSecretImports: React.FC =
});
});
+ // Add secret sync items
+ usedBySecretSyncs?.forEach((syncItem) => {
+ items.push({
+ type: ItemType.SecretSync,
+ path: syncItem.destination,
+ id: `secret-sync-${syncItem.name}-${syncItem.destination}`,
+ reference: "Secret Sync",
+ environment: { name: syncItem.environment, slug: "" }
+ });
+ });
+
return items.sort((a, b) => {
+ if (a.type === ItemType.SecretSync && b.type !== ItemType.SecretSync) return 1;
+ if (a.type !== ItemType.SecretSync && b.type === ItemType.SecretSync) return -1;
+
+ if (a.type === ItemType.SecretSync && b.type === ItemType.SecretSync) {
+ return a.path.localeCompare(b.path);
+ }
const envCompare = a.environment.name.localeCompare(b.environment.name);
if (envCompare !== 0) return envCompare;
@@ -119,7 +154,7 @@ export const CollapsibleSecretImports: React.FC =
return aPath.localeCompare(bPath);
});
- }, [importedBy]);
+ }, [importedBy, usedBySecretSyncs, secretsToDelete]);
const hasImportedItems = importedBy.some((element) => {
if (element.folders && element.folders.length > 0) {
@@ -135,7 +170,9 @@ export const CollapsibleSecretImports: React.FC =
return false;
});
- if (!hasImportedItems) {
+ const hasSecretSyncItems = usedBySecretSyncs && usedBySecretSyncs.length > 0;
+
+ if (!hasImportedItems && !hasSecretSyncItems) {
return null;
}
@@ -168,12 +205,28 @@ export const CollapsibleSecretImports: React.FC =
key={item.id}
onClick={() => handlePathClick(item)}
className="cursor-pointer hover:bg-mineshaft-700"
- title={`Navigate to ${item.path}`}
+ title={
+ item.type === ItemType.SecretSync
+ ? "Navigate to Secret Syncs"
+ : `Navigate to ${item.path}`
+ }
>
|
|
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx
index bb6420606..a08c2aa18 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx
@@ -29,6 +29,7 @@ type Props = {
tags?: WsTag[];
isVisible?: boolean;
isProtectedBranch?: boolean;
+ usedBySecretSyncs?: { environment: string; name: string; destination: string }[];
importedBy?: {
environment: { name: string; slug: string };
folders: {
@@ -47,6 +48,7 @@ export const SecretListView = ({
tags: wsTags = [],
isVisible,
isProtectedBranch = false,
+ usedBySecretSyncs,
importedBy
}: Props) => {
const queryClient = useQueryClient();
@@ -372,6 +374,7 @@ export const SecretListView = ({
importedBy.length > 0 && (
)