diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts
index 8ea4fb48d..5d75a6d2c 100644
--- a/backend/src/server/routes/v1/dashboard-router.ts
+++ b/backend/src/server/routes/v1/dashboard-router.ts
@@ -142,6 +142,34 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
})
.array()
.optional(),
+ importedByEnvs: z
+ .object({
+ environment: z.string(),
+ importedBy: z
+ .object({
+ environment: z.object({
+ name: z.string(),
+ slug: z.string()
+ }),
+ folders: z
+ .object({
+ name: z.string(),
+ isImported: z.boolean(),
+ secrets: z
+ .object({
+ secretId: z.string(),
+ referencedSecretKey: z.string()
+ })
+ .array()
+ .optional()
+ })
+ .array()
+ })
+ .array()
+ .optional()
+ })
+ .array()
+ .optional(),
totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(),
@@ -471,6 +499,28 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
}
+ const importedByEnvs = [];
+
+ for await (const environment of environments) {
+ const importedBy = await server.services.secretImport.getFolderIsImportedBy({
+ path: secretPath,
+ environment,
+ projectId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ secrets: secrets?.filter((s) => s.environment === environment)
+ });
+
+ if (importedBy) {
+ importedByEnvs.push({
+ environment,
+ importedBy
+ });
+ }
+ }
+
return {
folders,
dynamicSecrets,
@@ -482,6 +532,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalImportCount,
totalSecretCount,
totalSecretRotationCount,
+ importedByEnvs,
totalCount:
(totalFolderCount ?? 0) +
(totalDynamicSecretCount ?? 0) +
diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts
index c88760edc..e80874f8e 100644
--- a/frontend/src/hooks/api/dashboard/types.ts
+++ b/frontend/src/hooks/api/dashboard/types.ts
@@ -24,6 +24,7 @@ export type DashboardProjectSecretsOverviewResponse = {
totalUniqueDynamicSecretsInPage: number;
totalUniqueFoldersInPage: number;
totalUniqueSecretImportsInPage: number;
+ importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
totalUniqueSecretRotationsInPage: number;
};
@@ -41,13 +42,15 @@ export type DashboardProjectSecretsDetailsResponse = {
totalSecretCount?: number;
totalSecretRotationCount?: number;
totalCount: number;
- importedBy?: {
- environment: { name: string; slug: string };
- folders: {
- name: string;
- secrets?: { secretId: string; referencedSecretKey: string }[];
- isImported: boolean;
- }[];
+ importedBy?: ProjectSecretsImportedBy[];
+};
+
+export type ProjectSecretsImportedBy = {
+ environment: { name: string; slug: string };
+ folders: {
+ name: string;
+ secrets?: { secretId: string; referencedSecretKey: string }[];
+ isImported: boolean;
}[];
};
diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
index 4ee50ed4f..5fe31a7b3 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
@@ -272,7 +272,8 @@ export const OverviewPage = () => {
totalUniqueSecretsInPage,
totalUniqueSecretImportsInPage,
totalUniqueDynamicSecretsInPage,
- totalUniqueSecretRotationsInPage
+ totalUniqueSecretRotationsInPage,
+ importedByEnvs
} = overview ?? {};
const secretImportsShaped = secretImports
@@ -1042,6 +1043,7 @@ export const OverviewPage = () => {
secretPath={secretPath}
selectedEntries={selectedEntries}
resetSelectedEntries={resetSelectedEntries}
+ importedByEnvs={importedByEnvs}
/>
>;
[EntryType.SECRET]: Record>;
};
+ importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
};
-export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntries }: Props) => {
+export const SelectionPanel = ({
+ secretPath,
+ resetSelectedEntries,
+ selectedEntries,
+ importedByEnvs
+}: Props) => {
const { permission } = useProjectPermission();
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
@@ -72,6 +81,81 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
)
);
+ const secretsToDeleteKeys = useMemo(() => {
+ return Object.values(selectedEntries.secret).flatMap((entries) =>
+ Object.values(entries).map((secret) => secret.key)
+ );
+ }, [selectedEntries]);
+
+ const filterAndMergeEnvironments = (
+ envNames: string[],
+ envs: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]
+ ): ProjectSecretsImportedBy[] => {
+ const filteredEnvs = envs.filter((env) => envNames.includes(env.environment));
+
+ if (filteredEnvs.length === 0) return [];
+
+ const allImportedBy = filteredEnvs.flatMap((env) => env.importedBy);
+ const groupedBySlug: Record = {};
+
+ allImportedBy.forEach((item) => {
+ const { slug } = item.environment;
+ if (!groupedBySlug[slug]) groupedBySlug[slug] = [];
+ groupedBySlug[slug].push(item);
+ });
+
+ const mergedImportedBy = Object.values(groupedBySlug).map((group) => {
+ const { environment } = group[0];
+ const allFolders = group.flatMap((item) => item.folders);
+
+ const foldersByName: Record = {};
+ allFolders.forEach((folder) => {
+ if (!foldersByName[folder.name]) foldersByName[folder.name] = [];
+ foldersByName[folder.name].push(folder);
+ });
+
+ const mergedFolders = Object.entries(foldersByName).map(([name, folders]) => {
+ const isImported = folders.some((folder) => folder.isImported);
+ const allSecrets = folders.flatMap((folder) => folder.secrets || []);
+
+ const uniqueSecrets: { secretId: string; referencedSecretKey: string }[] = [];
+ const secretIds = new Set();
+
+ allSecrets
+ .filter((secret) => secretsToDeleteKeys.includes(secret.referencedSecretKey))
+ .forEach((secret) => {
+ if (!secretIds.has(secret.secretId)) {
+ secretIds.add(secret.secretId);
+ uniqueSecrets.push(secret);
+ }
+ });
+
+ return {
+ name,
+ isImported,
+ ...(uniqueSecrets.length > 0 ? { secrets: uniqueSecrets } : {})
+ };
+ });
+
+ return {
+ environment,
+ folders: mergedFolders.filter(
+ (folder) => folder.isImported || (folder.secrets && folder.secrets.length > 0)
+ )
+ };
+ });
+
+ return mergedImportedBy;
+ };
+
+ const importedBy = useMemo(() => {
+ if (selectedKeysCount === 0 || !importedByEnvs) return null;
+ return filterAndMergeEnvironments(
+ Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries)),
+ importedByEnvs
+ );
+ }, [importedByEnvs, selectedEntries, selectedKeysCount]);
+
const getDeleteModalTitle = () => {
if (selectedFolderCount > 0 && selectedKeysCount > 0) {
return "Do you want to delete the selected secrets and folders across environments?";
@@ -241,6 +325,15 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
title={getDeleteModalTitle()}
onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)}
onDeleteApproved={handleBulkDelete}
+ formContent={
+ importedBy &&
+ importedBy.some((element) => element.folders.length > 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 4c598bfe6..c62f15d95 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
@@ -81,12 +81,12 @@ import {
useSelectedSecrets
} from "../../SecretMainPage.store";
import { Filter, RowType } from "../../SecretMainPage.types";
+import { CollapsibleSecretImports } from "../SecretListView/CollapsibleSecretImports";
import { ReplicateFolderFromBoard } from "./ReplicateFolderFromBoard/ReplicateFolderFromBoard";
import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
import { CreateSecretImportForm } from "./CreateSecretImportForm";
import { FolderForm } from "./FolderForm";
import { MoveSecretsModal } from "./MoveSecretsModal";
-import { CollapsibleSecretImports } from "../SecretListView/CollapsibleSecretImports";
type TParsedEnv = Record;
type TParsedFolderEnv = Record<