mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 21:27:10 +00:00
Merge pull request #3491 from Infisical/feat/improveSecretReferenceWarning
feat(secrets-ui): Add direct reference warning on secrets updates and add secret sync warning on deletion
This commit is contained in:
@@ -1541,6 +1541,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const secretSyncService = secretSyncServiceFactory({
|
const secretSyncService = secretSyncServiceFactory({
|
||||||
secretSyncDAL,
|
secretSyncDAL,
|
||||||
|
secretImportDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: z
|
secrets: z
|
||||||
.object({
|
.object({
|
||||||
secretId: z.string(),
|
secretId: z.string(),
|
||||||
referencedSecretKey: z.string()
|
referencedSecretKey: z.string(),
|
||||||
|
referencedSecretEnv: z.string()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -166,6 +167,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
usedBySecretSyncs: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
destination: z.string(),
|
||||||
|
environment: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
path: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
totalFolderCount: z.number().optional(),
|
totalFolderCount: z.number().optional(),
|
||||||
totalDynamicSecretCount: z.number().optional(),
|
totalDynamicSecretCount: z.number().optional(),
|
||||||
totalSecretCount: z.number().optional(),
|
totalSecretCount: z.number().optional(),
|
||||||
@@ -500,6 +511,24 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const usedBySecretSyncs: { name: string; destination: string; environment: string; id: string; path: 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,
|
||||||
|
id: sync.id,
|
||||||
|
path: sync.folder?.path || "/"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
folders,
|
folders,
|
||||||
dynamicSecrets,
|
dynamicSecrets,
|
||||||
@@ -512,6 +541,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalSecretCount,
|
totalSecretCount,
|
||||||
totalSecretRotationCount,
|
totalSecretRotationCount,
|
||||||
importedByEnvs,
|
importedByEnvs,
|
||||||
|
usedBySecretSyncs,
|
||||||
totalCount:
|
totalCount:
|
||||||
(totalFolderCount ?? 0) +
|
(totalFolderCount ?? 0) +
|
||||||
(totalDynamicSecretCount ?? 0) +
|
(totalDynamicSecretCount ?? 0) +
|
||||||
@@ -611,6 +641,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalFolderCount: z.number().optional(),
|
totalFolderCount: z.number().optional(),
|
||||||
totalDynamicSecretCount: z.number().optional(),
|
totalDynamicSecretCount: z.number().optional(),
|
||||||
totalSecretCount: z.number().optional(),
|
totalSecretCount: z.number().optional(),
|
||||||
|
usedBySecretSyncs: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
destination: z.string(),
|
||||||
|
environment: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
path: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
importedBy: z
|
importedBy: z
|
||||||
.object({
|
.object({
|
||||||
environment: z.object({
|
environment: z.object({
|
||||||
@@ -624,7 +664,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: z
|
secrets: z
|
||||||
.object({
|
.object({
|
||||||
secretId: z.string(),
|
secretId: z.string(),
|
||||||
referencedSecretKey: z.string()
|
referencedSecretKey: z.string(),
|
||||||
|
referencedSecretEnv: z.string()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -904,6 +945,18 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets
|
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: sync.environment?.name || environment,
|
||||||
|
id: sync.id,
|
||||||
|
path: sync.folder?.path || "/"
|
||||||
|
}));
|
||||||
|
|
||||||
if (secrets?.length || secretRotations?.length) {
|
if (secrets?.length || secretRotations?.length) {
|
||||||
const secretCount =
|
const secretCount =
|
||||||
(secrets?.length ?? 0) +
|
(secrets?.length ?? 0) +
|
||||||
@@ -950,6 +1003,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalSecretCount,
|
totalSecretCount,
|
||||||
totalSecretRotationCount,
|
totalSecretRotationCount,
|
||||||
importedBy,
|
importedBy,
|
||||||
|
usedBySecretSyncs,
|
||||||
totalCount:
|
totalCount:
|
||||||
(totalImportCount ?? 0) +
|
(totalImportCount ?? 0) +
|
||||||
(totalFolderCount ?? 0) +
|
(totalFolderCount ?? 0) +
|
||||||
|
|||||||
@@ -171,6 +171,19 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFolderImports = async (secretPath: string, environmentId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const folderImports = await (tx || db.replicaNode())(TableName.SecretImport)
|
||||||
|
.where({ importPath: secretPath, importEnv: environmentId })
|
||||||
|
.join(TableName.SecretFolder, `${TableName.SecretImport}.folderId`, `${TableName.SecretFolder}.id`)
|
||||||
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.select(db.ref("id").withSchema(TableName.SecretFolder).as("folderId"));
|
||||||
|
return folderImports;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "get secret imports" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getFolderIsImportedBy = async (
|
const getFolderIsImportedBy = async (
|
||||||
secretPath: string,
|
secretPath: string,
|
||||||
environmentId: string,
|
environmentId: string,
|
||||||
@@ -203,7 +216,8 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
db.ref("id").withSchema(TableName.SecretFolder).as("folderId"),
|
db.ref("id").withSchema(TableName.SecretFolder).as("folderId"),
|
||||||
db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey")
|
db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey"),
|
||||||
|
db.ref("environment").withSchema(TableName.SecretReferenceV2).as("referencedSecretEnv")
|
||||||
);
|
);
|
||||||
|
|
||||||
const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({
|
const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({
|
||||||
@@ -214,13 +228,14 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const secretResults = secretReferences.map(
|
const secretResults = secretReferences.map(
|
||||||
({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey }) => ({
|
({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey, referencedSecretEnv }) => ({
|
||||||
envName,
|
envName,
|
||||||
envSlug,
|
envSlug,
|
||||||
secretId,
|
secretId,
|
||||||
folderName,
|
folderName,
|
||||||
folderId,
|
folderId,
|
||||||
referencedSecretKey
|
referencedSecretKey,
|
||||||
|
referencedSecretEnv
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -235,6 +250,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
secrets: {
|
secrets: {
|
||||||
secretId: string;
|
secretId: string;
|
||||||
referencedSecretKey: string;
|
referencedSecretKey: string;
|
||||||
|
referencedSecretEnv: string;
|
||||||
}[];
|
}[];
|
||||||
folderId: string;
|
folderId: string;
|
||||||
folderImported: boolean;
|
folderImported: boolean;
|
||||||
@@ -264,7 +280,11 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
if ("secretId" in item && item.secretId) {
|
if ("secretId" in item && item.secretId) {
|
||||||
updatedAcc[env].folders[folder].secrets = [
|
updatedAcc[env].folders[folder].secrets = [
|
||||||
...updatedAcc[env].folders[folder].secrets,
|
...updatedAcc[env].folders[folder].secrets,
|
||||||
{ secretId: item.secretId, referencedSecretKey: item.referencedSecretKey }
|
{
|
||||||
|
secretId: item.secretId,
|
||||||
|
referencedSecretKey: item.referencedSecretKey,
|
||||||
|
referencedSecretEnv: item.referencedSecretEnv
|
||||||
|
}
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
updatedAcc[env].folders[folder].folderImported = true;
|
updatedAcc[env].folders[folder].folderImported = true;
|
||||||
@@ -309,6 +329,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
findLastImportPosition,
|
findLastImportPosition,
|
||||||
updateAllPosition,
|
updateAllPosition,
|
||||||
getProjectImportCount,
|
getProjectImportCount,
|
||||||
getFolderIsImportedBy
|
getFolderIsImportedBy,
|
||||||
|
getFolderImports
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -808,7 +808,7 @@ export const secretImportServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
secrets
|
secrets
|
||||||
}: TGetSecretImportsDTO & {
|
}: TGetSecretImportsDTO & {
|
||||||
secrets: { secretKey: string; secretValue: string }[] | undefined;
|
secrets: { secretKey: string; secretValue: string; id: string }[] | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
@@ -877,7 +877,8 @@ export const secretImportServiceFactory = ({
|
|||||||
)
|
)
|
||||||
.map((otherSecret) => ({
|
.map((otherSecret) => ({
|
||||||
secretId: secret.secretKey,
|
secretId: secret.secretKey,
|
||||||
referencedSecretKey: otherSecret.secretKey
|
referencedSecretKey: otherSecret.secretKey,
|
||||||
|
referencedSecretEnv: environment
|
||||||
}));
|
}));
|
||||||
}) || [];
|
}) || [];
|
||||||
if (locallyReferenced.length > 0) {
|
if (locallyReferenced.length > 0) {
|
||||||
|
|||||||
@@ -56,11 +56,12 @@ export type FolderResult = {
|
|||||||
export type SecretResult = {
|
export type SecretResult = {
|
||||||
secretId: string;
|
secretId: string;
|
||||||
referencedSecretKey: string;
|
referencedSecretKey: string;
|
||||||
|
referencedSecretEnv: string;
|
||||||
} & FolderResult;
|
} & FolderResult;
|
||||||
|
|
||||||
export type FolderInfo = {
|
export type FolderInfo = {
|
||||||
folderName: string;
|
folderName: string;
|
||||||
secrets?: { secretId: string; referencedSecretKey: string }[];
|
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||||
folderId: string;
|
folderId: string;
|
||||||
folderImported: boolean;
|
folderImported: boolean;
|
||||||
envSlug?: string;
|
envSlug?: string;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
TDeleteSecretSyncDTO,
|
TDeleteSecretSyncDTO,
|
||||||
TFindSecretSyncByIdDTO,
|
TFindSecretSyncByIdDTO,
|
||||||
TFindSecretSyncByNameDTO,
|
TFindSecretSyncByNameDTO,
|
||||||
|
TListSecretSyncsByFolderId,
|
||||||
TListSecretSyncsByProjectId,
|
TListSecretSyncsByProjectId,
|
||||||
TSecretSync,
|
TSecretSync,
|
||||||
TTriggerSecretSyncImportSecretsByIdDTO,
|
TTriggerSecretSyncImportSecretsByIdDTO,
|
||||||
@@ -31,12 +32,14 @@ import {
|
|||||||
TUpdateSecretSyncDTO
|
TUpdateSecretSyncDTO
|
||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||||
import { TSecretSyncDALFactory } from "./secret-sync-dal";
|
import { TSecretSyncDALFactory } from "./secret-sync-dal";
|
||||||
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps";
|
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps";
|
||||||
import { TSecretSyncQueueFactory } from "./secret-sync-queue";
|
import { TSecretSyncQueueFactory } from "./secret-sync-queue";
|
||||||
|
|
||||||
type TSecretSyncServiceFactoryDep = {
|
type TSecretSyncServiceFactoryDep = {
|
||||||
secretSyncDAL: TSecretSyncDALFactory;
|
secretSyncDAL: TSecretSyncDALFactory;
|
||||||
|
secretImportDAL: TSecretImportDALFactory;
|
||||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
@@ -53,6 +56,7 @@ export type TSecretSyncServiceFactory = ReturnType<typeof secretSyncServiceFacto
|
|||||||
export const secretSyncServiceFactory = ({
|
export const secretSyncServiceFactory = ({
|
||||||
secretSyncDAL,
|
secretSyncDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
|
secretImportDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
@@ -85,6 +89,37 @@ export const secretSyncServiceFactory = ({
|
|||||||
return secretSyncs as TSecretSync[];
|
return secretSyncs as TSecretSync[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listSecretSyncsBySecretPath = async (
|
||||||
|
{ projectId, secretPath, environment }: 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 folderImports = await secretImportDAL.getFolderImports(secretPath, folder.envId);
|
||||||
|
|
||||||
|
const secretSyncs = await secretSyncDAL.find({
|
||||||
|
$in: {
|
||||||
|
folderId: folderImports.map((folderImport) => folderImport.folderId).concat(folder.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return secretSyncs as TSecretSync[];
|
||||||
|
};
|
||||||
|
|
||||||
const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => {
|
const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => {
|
||||||
const secretSync = await secretSyncDAL.findById(syncId);
|
const secretSync = await secretSyncDAL.findById(syncId);
|
||||||
|
|
||||||
@@ -518,6 +553,7 @@ export const secretSyncServiceFactory = ({
|
|||||||
return {
|
return {
|
||||||
listSecretSyncOptions,
|
listSecretSyncOptions,
|
||||||
listSecretSyncsByProjectId,
|
listSecretSyncsByProjectId,
|
||||||
|
listSecretSyncsBySecretPath,
|
||||||
findSecretSyncById,
|
findSecretSyncById,
|
||||||
findSecretSyncByName,
|
findSecretSyncByName,
|
||||||
createSecretSync,
|
createSecretSync,
|
||||||
|
|||||||
@@ -144,6 +144,13 @@ export type TListSecretSyncsByProjectId = {
|
|||||||
destination?: SecretSync;
|
destination?: SecretSync;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TListSecretSyncsByFolderId = {
|
||||||
|
projectId: string;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
destination?: SecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
export type TFindSecretSyncByIdDTO = {
|
export type TFindSecretSyncByIdDTO = {
|
||||||
syncId: string;
|
syncId: string;
|
||||||
destination: SecretSync;
|
destination: SecretSync;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type Props = {
|
|||||||
formContent?: ReactNode;
|
formContent?: ReactNode;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
deletionMessage?: ReactNode;
|
deletionMessage?: ReactNode;
|
||||||
|
buttonColorSchema?: "danger" | "primary" | "secondary" | "gray" | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteActionModal = ({
|
export const DeleteActionModal = ({
|
||||||
@@ -32,6 +33,7 @@ export const DeleteActionModal = ({
|
|||||||
buttonText = "Delete",
|
buttonText = "Delete",
|
||||||
formContent,
|
formContent,
|
||||||
deletionMessage,
|
deletionMessage,
|
||||||
|
buttonColorSchema = "danger",
|
||||||
children
|
children
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const [inputData, setInputData] = useState("");
|
const [inputData, setInputData] = useState("");
|
||||||
@@ -67,7 +69,7 @@ export const DeleteActionModal = ({
|
|||||||
<div className="mx-2 flex items-center">
|
<div className="mx-2 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
colorSchema="danger"
|
colorSchema={buttonColorSchema}
|
||||||
isDisabled={!(deleteKey === inputData) || isLoading}
|
isDisabled={!(deleteKey === inputData) || isLoading}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|||||||
@@ -25,9 +25,18 @@ export type DashboardProjectSecretsOverviewResponse = {
|
|||||||
totalUniqueFoldersInPage: number;
|
totalUniqueFoldersInPage: number;
|
||||||
totalUniqueSecretImportsInPage: number;
|
totalUniqueSecretImportsInPage: number;
|
||||||
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
|
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
|
||||||
|
usedBySecretSyncs?: UsedBySecretSyncs[];
|
||||||
totalUniqueSecretRotationsInPage: number;
|
totalUniqueSecretRotationsInPage: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UsedBySecretSyncs = {
|
||||||
|
name: string;
|
||||||
|
destination: string;
|
||||||
|
environment: string;
|
||||||
|
id: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type DashboardProjectSecretsDetailsResponse = {
|
export type DashboardProjectSecretsDetailsResponse = {
|
||||||
imports?: TSecretImport[];
|
imports?: TSecretImport[];
|
||||||
folders?: TSecretFolder[];
|
folders?: TSecretFolder[];
|
||||||
@@ -43,13 +52,14 @@ export type DashboardProjectSecretsDetailsResponse = {
|
|||||||
totalSecretRotationCount?: number;
|
totalSecretRotationCount?: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
importedBy?: ProjectSecretsImportedBy[];
|
importedBy?: ProjectSecretsImportedBy[];
|
||||||
|
usedBySecretSyncs?: UsedBySecretSyncs[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProjectSecretsImportedBy = {
|
export type ProjectSecretsImportedBy = {
|
||||||
environment: { name: string; slug: string };
|
environment: { name: string; slug: string };
|
||||||
folders: {
|
folders: {
|
||||||
name: string;
|
name: string;
|
||||||
secrets?: { secretId: string; referencedSecretKey: string }[];
|
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||||
isImported: boolean;
|
isImported: boolean;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ import {
|
|||||||
useUpdateSecretV3
|
useUpdateSecretV3
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { useGetProjectSecretsOverview } from "@app/hooks/api/dashboard/queries";
|
import { useGetProjectSecretsOverview } from "@app/hooks/api/dashboard/queries";
|
||||||
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
import { DashboardSecretsOrderBy, ProjectSecretsImportedBy } from "@app/hooks/api/dashboard/types";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
||||||
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
|
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
|
||||||
@@ -274,7 +274,8 @@ export const OverviewPage = () => {
|
|||||||
totalUniqueSecretImportsInPage,
|
totalUniqueSecretImportsInPage,
|
||||||
totalUniqueDynamicSecretsInPage,
|
totalUniqueDynamicSecretsInPage,
|
||||||
totalUniqueSecretRotationsInPage,
|
totalUniqueSecretRotationsInPage,
|
||||||
importedByEnvs
|
importedByEnvs,
|
||||||
|
usedBySecretSyncs
|
||||||
} = overview ?? {};
|
} = overview ?? {};
|
||||||
|
|
||||||
const secretImportsShaped = secretImports
|
const secretImportsShaped = secretImports
|
||||||
@@ -726,6 +727,98 @@ export const OverviewPage = () => {
|
|||||||
}
|
}
|
||||||
}, [routerSearch.search]);
|
}, [routerSearch.search]);
|
||||||
|
|
||||||
|
const selectedKeysCount = Object.keys(selectedEntries.secret).length;
|
||||||
|
|
||||||
|
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<string, ProjectSecretsImportedBy[]> = {};
|
||||||
|
|
||||||
|
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<string, (typeof allFolders)[number][]> = {};
|
||||||
|
allFolders.forEach((folder) => {
|
||||||
|
if (!foldersByName[folder.name]) foldersByName[folder.name] = [];
|
||||||
|
foldersByName[folder.name].push(folder);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedFolders = Object.entries(foldersByName).map(([name, foldersData]) => {
|
||||||
|
const isImported = foldersData.some((folder) => folder.isImported);
|
||||||
|
const allSecrets = foldersData.flatMap((folder) => folder.secrets || []);
|
||||||
|
|
||||||
|
const uniqueSecrets: {
|
||||||
|
secretId: string;
|
||||||
|
referencedSecretKey: string;
|
||||||
|
referencedSecretEnv: string;
|
||||||
|
}[] = [];
|
||||||
|
const secretIds = new Set<string>();
|
||||||
|
|
||||||
|
allSecrets
|
||||||
|
.filter(
|
||||||
|
(secret) =>
|
||||||
|
!secretsToDeleteKeys ||
|
||||||
|
secretsToDeleteKeys.length === 0 ||
|
||||||
|
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 (!importedByEnvs) return [];
|
||||||
|
if (selectedKeysCount === 0) {
|
||||||
|
return filterAndMergeEnvironments(
|
||||||
|
visibleEnvs.map(({ slug }) => slug),
|
||||||
|
importedByEnvs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return filterAndMergeEnvironments(
|
||||||
|
Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries)),
|
||||||
|
importedByEnvs
|
||||||
|
);
|
||||||
|
}, [importedByEnvs, selectedEntries, selectedKeysCount]);
|
||||||
|
|
||||||
if (isProjectV3 && visibleEnvs.length > 0 && isOverviewLoading) {
|
if (isProjectV3 && visibleEnvs.length > 0 && isOverviewLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
@@ -1044,7 +1137,9 @@ export const OverviewPage = () => {
|
|||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
selectedEntries={selectedEntries}
|
selectedEntries={selectedEntries}
|
||||||
resetSelectedEntries={resetSelectedEntries}
|
resetSelectedEntries={resetSelectedEntries}
|
||||||
importedByEnvs={importedByEnvs}
|
importedBy={importedBy}
|
||||||
|
secretsToDeleteKeys={secretsToDeleteKeys}
|
||||||
|
usedBySecretSyncs={usedBySecretSyncs}
|
||||||
/>
|
/>
|
||||||
<div className="thin-scrollbar mt-4">
|
<div className="thin-scrollbar mt-4">
|
||||||
<TableContainer
|
<TableContainer
|
||||||
@@ -1261,6 +1356,7 @@ export const OverviewPage = () => {
|
|||||||
secretKey={key}
|
secretKey={key}
|
||||||
getSecretByKey={getSecretByKey}
|
getSecretByKey={getSecretByKey}
|
||||||
scrollOffset={debouncedScrollOffset}
|
scrollOffset={debouncedScrollOffset}
|
||||||
|
importedBy={importedBy}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<SecretNoAccessOverviewTableRow
|
<SecretNoAccessOverviewTableRow
|
||||||
|
|||||||
+62
-2
@@ -29,9 +29,10 @@ import {
|
|||||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import { SecretType } from "@app/hooks/api/types";
|
import { SecretType } from "@app/hooks/api/types";
|
||||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||||
|
import { CollapsibleSecretImports } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
defaultValue?: string | null;
|
defaultValue?: string | null;
|
||||||
@@ -54,6 +55,14 @@ type Props = {
|
|||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
|
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
|
||||||
isRotatedSecret?: boolean;
|
isRotatedSecret?: boolean;
|
||||||
|
importedBy?: {
|
||||||
|
environment: { name: string; slug: string };
|
||||||
|
folders: {
|
||||||
|
name: string;
|
||||||
|
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||||
|
isImported: boolean;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretEditRow = ({
|
export const SecretEditRow = ({
|
||||||
@@ -70,8 +79,13 @@ export const SecretEditRow = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
isVisible,
|
isVisible,
|
||||||
secretId,
|
secretId,
|
||||||
isRotatedSecret
|
isRotatedSecret,
|
||||||
|
importedBy
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||||
|
"editSecret"
|
||||||
|
] as const);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
@@ -115,6 +129,20 @@ export const SecretEditRow = ({
|
|||||||
if (isCreatable) {
|
if (isCreatable) {
|
||||||
await onSecretCreate(environment, secretName, value);
|
await onSecretCreate(environment, secretName, value);
|
||||||
} else {
|
} else {
|
||||||
|
if (
|
||||||
|
importedBy &&
|
||||||
|
importedBy.some(({ folders }) =>
|
||||||
|
folders?.some(({ secrets }) =>
|
||||||
|
secrets?.some(
|
||||||
|
({ referencedSecretKey, referencedSecretEnv }) =>
|
||||||
|
referencedSecretKey === secretName && referencedSecretEnv === environment
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
handlePopUpOpen("editSecret", { secretValue: value });
|
||||||
|
return;
|
||||||
|
}
|
||||||
await onSecretUpdate(
|
await onSecretUpdate(
|
||||||
environment,
|
environment,
|
||||||
secretName,
|
secretName,
|
||||||
@@ -133,6 +161,18 @@ export const SecretEditRow = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditSecret = async ({ secretValue }: { secretValue: string }) => {
|
||||||
|
await onSecretUpdate(
|
||||||
|
environment,
|
||||||
|
secretName,
|
||||||
|
secretValue,
|
||||||
|
isOverride ? SecretType.Personal : SecretType.Shared,
|
||||||
|
secretId
|
||||||
|
);
|
||||||
|
reset({ value: secretValue });
|
||||||
|
handlePopUpClose("editSecret");
|
||||||
|
};
|
||||||
|
|
||||||
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
|
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
|
||||||
permission,
|
permission,
|
||||||
ProjectPermissionSecretActions.ReadValue
|
ProjectPermissionSecretActions.ReadValue
|
||||||
@@ -325,6 +365,26 @@ export const SecretEditRow = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.editSecret.isOpen}
|
||||||
|
deleteKey="confirm"
|
||||||
|
buttonColorSchema="secondary"
|
||||||
|
buttonText="Save"
|
||||||
|
subTitle=""
|
||||||
|
title="Do you want to edit this secret?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("editSecret", isOpen)}
|
||||||
|
onDeleteApproved={() => handleEditSecret(popUp?.editSecret?.data)}
|
||||||
|
formContent={
|
||||||
|
importedBy &&
|
||||||
|
importedBy.length > 0 && (
|
||||||
|
<CollapsibleSecretImports
|
||||||
|
importedBy={importedBy}
|
||||||
|
secretsToDelete={[secretName]}
|
||||||
|
onlyReferences
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-1
@@ -50,6 +50,14 @@ type Props = {
|
|||||||
secretName: string
|
secretName: string
|
||||||
) => { secret?: SecretV3RawSanitized; environmentInfo?: WorkspaceEnv } | undefined;
|
) => { secret?: SecretV3RawSanitized; environmentInfo?: WorkspaceEnv } | undefined;
|
||||||
scrollOffset: number;
|
scrollOffset: number;
|
||||||
|
importedBy?: {
|
||||||
|
environment: { name: string; slug: string };
|
||||||
|
folders: {
|
||||||
|
name: string;
|
||||||
|
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||||
|
isImported: boolean;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretOverviewTableRow = ({
|
export const SecretOverviewTableRow = ({
|
||||||
@@ -64,7 +72,8 @@ export const SecretOverviewTableRow = ({
|
|||||||
getImportedSecretByKey,
|
getImportedSecretByKey,
|
||||||
scrollOffset,
|
scrollOffset,
|
||||||
onToggleSecretSelect,
|
onToggleSecretSelect,
|
||||||
isSelected
|
isSelected,
|
||||||
|
importedBy
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isFormExpanded, setIsFormExpanded] = useToggle();
|
const [isFormExpanded, setIsFormExpanded] = useToggle();
|
||||||
const totalCols = environments.length + 1; // secret key row
|
const totalCols = environments.length + 1; // secret key row
|
||||||
@@ -266,6 +275,7 @@ export const SecretOverviewTableRow = ({
|
|||||||
onSecretUpdate={onSecretUpdate}
|
onSecretUpdate={onSecretUpdate}
|
||||||
environment={slug}
|
environment={slug}
|
||||||
isRotatedSecret={secret?.isRotatedSecret}
|
isRotatedSecret={secret?.isRotatedSecret}
|
||||||
|
importedBy={importedBy}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
+16
-80
@@ -15,7 +15,7 @@ import {
|
|||||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
|
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
|
||||||
import { ProjectSecretsImportedBy } from "@app/hooks/api/dashboard/types";
|
import { ProjectSecretsImportedBy, UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||||
import {
|
import {
|
||||||
SecretType,
|
SecretType,
|
||||||
SecretV3RawSanitized,
|
SecretV3RawSanitized,
|
||||||
@@ -37,14 +37,18 @@ type Props = {
|
|||||||
[EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>;
|
[EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>;
|
||||||
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
|
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
|
||||||
};
|
};
|
||||||
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
|
importedBy?: ProjectSecretsImportedBy[] | null;
|
||||||
|
usedBySecretSyncs?: UsedBySecretSyncs[];
|
||||||
|
secretsToDeleteKeys: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectionPanel = ({
|
export const SelectionPanel = ({
|
||||||
secretPath,
|
secretPath,
|
||||||
resetSelectedEntries,
|
resetSelectedEntries,
|
||||||
selectedEntries,
|
selectedEntries,
|
||||||
importedByEnvs
|
importedBy,
|
||||||
|
secretsToDeleteKeys,
|
||||||
|
usedBySecretSyncs = []
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
@@ -81,80 +85,11 @@ export const SelectionPanel = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const secretsToDeleteKeys = useMemo(() => {
|
const usedBySecretSyncsFiltered = useMemo(() => {
|
||||||
return Object.values(selectedEntries.secret).flatMap((entries) =>
|
if (selectedKeysCount === 0 || usedBySecretSyncs.length === 0) return null;
|
||||||
Object.values(entries).map((secret) => secret.key)
|
const envs = Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries));
|
||||||
);
|
return usedBySecretSyncs.filter((syncItem) => envs.includes(syncItem.environment));
|
||||||
}, [selectedEntries]);
|
}, [selectedEntries, usedBySecretSyncs, selectedKeysCount]);
|
||||||
|
|
||||||
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<string, ProjectSecretsImportedBy[]> = {};
|
|
||||||
|
|
||||||
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<string, (typeof allFolders)[number][]> = {};
|
|
||||||
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<string>();
|
|
||||||
|
|
||||||
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 = () => {
|
const getDeleteModalTitle = () => {
|
||||||
if (selectedFolderCount > 0 && selectedKeysCount > 0) {
|
if (selectedFolderCount > 0 && selectedKeysCount > 0) {
|
||||||
@@ -326,11 +261,12 @@ export const SelectionPanel = ({
|
|||||||
onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)}
|
||||||
onDeleteApproved={handleBulkDelete}
|
onDeleteApproved={handleBulkDelete}
|
||||||
formContent={
|
formContent={
|
||||||
importedBy &&
|
((usedBySecretSyncsFiltered && usedBySecretSyncsFiltered.length > 0) ||
|
||||||
importedBy.some((element) => element.folders.length > 0) && (
|
(importedBy && importedBy.some((element) => element.folders.length > 0))) && (
|
||||||
<CollapsibleSecretImports
|
<CollapsibleSecretImports
|
||||||
importedBy={importedBy}
|
importedBy={importedBy || []}
|
||||||
secretsToDelete={secretsToDeleteKeys}
|
secretsToDelete={secretsToDeleteKeys}
|
||||||
|
usedBySecretSyncs={usedBySecretSyncsFiltered}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ const Page = () => {
|
|||||||
totalSecretCount = 0,
|
totalSecretCount = 0,
|
||||||
totalCount = 0,
|
totalCount = 0,
|
||||||
importedBy,
|
importedBy,
|
||||||
|
usedBySecretSyncs,
|
||||||
totalSecretRotationCount = 0
|
totalSecretRotationCount = 0
|
||||||
} = data ?? {};
|
} = data ?? {};
|
||||||
|
|
||||||
@@ -441,6 +442,7 @@ const Page = () => {
|
|||||||
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
|
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
|
||||||
protectedBranchPolicyName={boardPolicy?.name}
|
protectedBranchPolicyName={boardPolicy?.name}
|
||||||
importedBy={importedBy}
|
importedBy={importedBy}
|
||||||
|
usedBySecretSyncs={usedBySecretSyncs}
|
||||||
/>
|
/>
|
||||||
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
|
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
|
||||||
<div className="flex flex-col" id="dashboard">
|
<div className="flex flex-col" id="dashboard">
|
||||||
@@ -530,6 +532,7 @@ const Page = () => {
|
|||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
isProtectedBranch={isProtectedBranch}
|
isProtectedBranch={isProtectedBranch}
|
||||||
importedBy={importedBy}
|
importedBy={importedBy}
|
||||||
|
usedBySecretSyncs={usedBySecretSyncs}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />}
|
{noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />}
|
||||||
|
|||||||
+8
-4
@@ -69,6 +69,7 @@ import {
|
|||||||
dashboardKeys,
|
dashboardKeys,
|
||||||
fetchDashboardProjectSecretsByKeys
|
fetchDashboardProjectSecretsByKeys
|
||||||
} from "@app/hooks/api/dashboard/queries";
|
} from "@app/hooks/api/dashboard/queries";
|
||||||
|
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||||
import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries";
|
import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries";
|
||||||
import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types";
|
import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types";
|
||||||
@@ -113,11 +114,12 @@ type Props = {
|
|||||||
onVisibilityToggle: () => void;
|
onVisibilityToggle: () => void;
|
||||||
onToggleRowType: (rowType: RowType) => void;
|
onToggleRowType: (rowType: RowType) => void;
|
||||||
onClickRollbackMode: () => void;
|
onClickRollbackMode: () => void;
|
||||||
|
usedBySecretSyncs?: UsedBySecretSyncs[];
|
||||||
importedBy?: {
|
importedBy?: {
|
||||||
environment: { name: string; slug: string };
|
environment: { name: string; slug: string };
|
||||||
folders: {
|
folders: {
|
||||||
name: string;
|
name: string;
|
||||||
secrets?: { secretId: string; referencedSecretKey: string }[];
|
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||||
isImported: boolean;
|
isImported: boolean;
|
||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
@@ -139,7 +141,8 @@ export const ActionBar = ({
|
|||||||
onClickRollbackMode,
|
onClickRollbackMode,
|
||||||
onToggleRowType,
|
onToggleRowType,
|
||||||
protectedBranchPolicyName,
|
protectedBranchPolicyName,
|
||||||
importedBy
|
importedBy,
|
||||||
|
usedBySecretSyncs
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||||
"addFolder",
|
"addFolder",
|
||||||
@@ -1071,11 +1074,12 @@ export const ActionBar = ({
|
|||||||
onChange={(isOpen) => handlePopUpToggle("bulkDeleteSecrets", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("bulkDeleteSecrets", isOpen)}
|
||||||
onDeleteApproved={handleSecretBulkDelete}
|
onDeleteApproved={handleSecretBulkDelete}
|
||||||
formContent={
|
formContent={
|
||||||
importedBy &&
|
((importedBy && importedBy.length > 0) ||
|
||||||
importedBy.length > 0 && (
|
(usedBySecretSyncs && usedBySecretSyncs?.length > 0)) && (
|
||||||
<CollapsibleSecretImports
|
<CollapsibleSecretImports
|
||||||
importedBy={importedBy}
|
importedBy={importedBy}
|
||||||
secretsToDelete={Object.values(selectedSecrets).map((s) => s.key)}
|
secretsToDelete={Object.values(selectedSecrets).map((s) => s.key)}
|
||||||
|
usedBySecretSyncs={usedBySecretSyncs}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-17
@@ -1,13 +1,16 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import React, { useMemo } from "react";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { Table, TBody, Td, Th, THead, Tr } from "@app/components/v2";
|
import { Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||||
|
|
||||||
enum ItemType {
|
enum ItemType {
|
||||||
Folder = "Folder",
|
Folder = "Folder",
|
||||||
Secret = "Secret"
|
Secret = "Secret",
|
||||||
|
SecretSync = "SecretSync"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FlatItem {
|
interface FlatItem {
|
||||||
@@ -17,6 +20,8 @@ interface FlatItem {
|
|||||||
reference: string;
|
reference: string;
|
||||||
id: string;
|
id: string;
|
||||||
environment: { name: string; slug: string };
|
environment: { name: string; slug: string };
|
||||||
|
tooltipText?: string;
|
||||||
|
destination?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CollapsibleSecretImportsProps {
|
interface CollapsibleSecretImportsProps {
|
||||||
@@ -24,16 +29,20 @@ interface CollapsibleSecretImportsProps {
|
|||||||
environment: { name: string; slug: string };
|
environment: { name: string; slug: string };
|
||||||
folders: {
|
folders: {
|
||||||
name: string;
|
name: string;
|
||||||
secrets?: { secretId: string; referencedSecretKey: string }[];
|
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||||
isImported: boolean;
|
isImported: boolean;
|
||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
|
usedBySecretSyncs?: UsedBySecretSyncs[] | null;
|
||||||
secretsToDelete: string[];
|
secretsToDelete: string[];
|
||||||
|
onlyReferences?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({
|
export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({
|
||||||
importedBy = [],
|
importedBy = [],
|
||||||
secretsToDelete
|
usedBySecretSyncs = [],
|
||||||
|
secretsToDelete,
|
||||||
|
onlyReferences
|
||||||
}) => {
|
}) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
@@ -51,6 +60,15 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePathClick = (item: FlatItem) => {
|
const handlePathClick = (item: FlatItem) => {
|
||||||
|
if (item.type === ItemType.SecretSync) {
|
||||||
|
window.open(
|
||||||
|
`/secret-manager/${currentWorkspace.id}/integrations/secret-syncs/${item.destination}/${item.id}`,
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let pathToNavigate;
|
let pathToNavigate;
|
||||||
if (item.type === ItemType.Folder) {
|
if (item.type === ItemType.Folder) {
|
||||||
pathToNavigate = item.path;
|
pathToNavigate = item.path;
|
||||||
@@ -70,7 +88,7 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
|||||||
|
|
||||||
importedBy.forEach((env) => {
|
importedBy.forEach((env) => {
|
||||||
env.folders.forEach((folder) => {
|
env.folders.forEach((folder) => {
|
||||||
if (folder.isImported) {
|
if (folder.isImported && !onlyReferences) {
|
||||||
items.push({
|
items.push({
|
||||||
type: ItemType.Folder,
|
type: ItemType.Folder,
|
||||||
path: folder.name,
|
path: folder.name,
|
||||||
@@ -103,7 +121,26 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add secret sync items
|
||||||
|
usedBySecretSyncs?.forEach((syncItem) => {
|
||||||
|
items.push({
|
||||||
|
type: ItemType.SecretSync,
|
||||||
|
destination: syncItem.destination,
|
||||||
|
path: syncItem.path,
|
||||||
|
id: syncItem.id,
|
||||||
|
reference: "Secret Sync",
|
||||||
|
environment: { name: syncItem.environment, slug: "" },
|
||||||
|
tooltipText: `Currently used by Secret Sync: ${syncItem.name}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return items.sort((a, b) => {
|
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);
|
const envCompare = a.environment.name.localeCompare(b.environment.name);
|
||||||
if (envCompare !== 0) return envCompare;
|
if (envCompare !== 0) return envCompare;
|
||||||
|
|
||||||
@@ -119,7 +156,7 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
|||||||
|
|
||||||
return aPath.localeCompare(bPath);
|
return aPath.localeCompare(bPath);
|
||||||
});
|
});
|
||||||
}, [importedBy]);
|
}, [importedBy, usedBySecretSyncs, secretsToDelete, onlyReferences]);
|
||||||
|
|
||||||
const hasImportedItems = importedBy.some((element) => {
|
const hasImportedItems = importedBy.some((element) => {
|
||||||
if (element.folders && element.folders.length > 0) {
|
if (element.folders && element.folders.length > 0) {
|
||||||
@@ -135,19 +172,33 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!hasImportedItems) {
|
const hasSecretSyncItems = usedBySecretSyncs && usedBySecretSyncs.length > 0;
|
||||||
|
|
||||||
|
if (!hasImportedItems && !hasSecretSyncItems) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const alertColors = onlyReferences
|
||||||
|
? {
|
||||||
|
border: "border-yellow-700/30",
|
||||||
|
bg: "bg-yellow-900/20",
|
||||||
|
text: "text-yellow-500"
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
border: "border-red-700/30",
|
||||||
|
bg: "bg-red-900/20",
|
||||||
|
text: "text-red-500"
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 w-full">
|
<div className="mb-4 w-full">
|
||||||
<div className="mb-4 rounded-md border border-red-700/30 bg-red-900/20">
|
<div className={`mb-4 rounded-md border ${alertColors.border} ${alertColors.bg}`}>
|
||||||
<div className="flex items-start gap-3 p-4">
|
<div className="flex items-start gap-3 p-4">
|
||||||
<div className="mt-0.5 flex-shrink-0 text-red-500">
|
<div className={`mt-0.5 flex-shrink-0 ${alertColors.text}`}>
|
||||||
<FontAwesomeIcon icon={faWarning} className="h-5 w-5" aria-hidden="true" />
|
<FontAwesomeIcon icon={faWarning} className="h-5 w-5" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<p className="text-sm font-semibold text-red-500">
|
<p className={`text-sm font-semibold ${alertColors.text}`}>
|
||||||
The following resources will be affected by this change
|
The following resources will be affected by this change
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,14 +219,36 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => handlePathClick(item)}
|
onClick={() => handlePathClick(item)}
|
||||||
className="cursor-pointer hover:bg-mineshaft-700"
|
className="cursor-pointer hover:bg-mineshaft-700"
|
||||||
title={`Navigate to ${item.path}`}
|
title={
|
||||||
|
item.type === ItemType.SecretSync
|
||||||
|
? "Navigate to Secret Sync"
|
||||||
|
: `Navigate to ${item.path}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Td>
|
<Td>
|
||||||
<FontAwesomeIcon
|
<Tooltip
|
||||||
icon={item.type === ItemType.Secret ? faKey : faFileImport}
|
className="max-w-md"
|
||||||
className={`h-4 w-4 ${item.type === ItemType.Secret ? "text-gray-400" : "text-green-700"}`}
|
content={item.tooltipText}
|
||||||
aria-hidden="true"
|
isDisabled={!item.tooltipText}
|
||||||
/>
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
item.type === ItemType.Secret
|
||||||
|
? faKey
|
||||||
|
: item.type === ItemType.Folder
|
||||||
|
? faFileImport
|
||||||
|
: faSync
|
||||||
|
}
|
||||||
|
className={`h-4 w-4 ${
|
||||||
|
item.type === ItemType.Secret
|
||||||
|
? "text-gray-400"
|
||||||
|
: item.type === ItemType.Folder
|
||||||
|
? "text-green-700"
|
||||||
|
: "text-mineshaft-300"
|
||||||
|
}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="px-4">{item.environment.name}</Td>
|
<Td className="px-4">{item.environment.name}</Td>
|
||||||
<Td className="truncate px-4">{truncatePath(item.path)}</Td>
|
<Td className="truncate px-4">{truncatePath(item.path)}</Td>
|
||||||
|
|||||||
+51
-2
@@ -4,6 +4,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
DeleteActionModal,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@@ -31,7 +32,7 @@ import {
|
|||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||||
import { WsTag } from "@app/hooks/api/types";
|
import { WsTag } from "@app/hooks/api/types";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
@@ -55,6 +56,7 @@ import {
|
|||||||
SecretActionType,
|
SecretActionType,
|
||||||
TFormSchema
|
TFormSchema
|
||||||
} from "./SecretListView.utils";
|
} from "./SecretListView.utils";
|
||||||
|
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||||
|
|
||||||
const hiddenValue = "******";
|
const hiddenValue = "******";
|
||||||
|
|
||||||
@@ -75,6 +77,14 @@ type Props = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
handleSecretShare: () => void;
|
handleSecretShare: () => void;
|
||||||
|
importedBy?: {
|
||||||
|
environment: { name: string; slug: string };
|
||||||
|
folders: {
|
||||||
|
name: string;
|
||||||
|
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||||
|
isImported: boolean;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretItem = memo(
|
export const SecretItem = memo(
|
||||||
@@ -90,8 +100,12 @@ export const SecretItem = memo(
|
|||||||
onToggleSecretSelect,
|
onToggleSecretSelect,
|
||||||
environment,
|
environment,
|
||||||
secretPath,
|
secretPath,
|
||||||
handleSecretShare
|
handleSecretShare,
|
||||||
|
importedBy
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||||
|
"editSecret"
|
||||||
|
] as const);
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const { isRotatedSecret } = secret;
|
const { isRotatedSecret } = secret;
|
||||||
@@ -213,9 +227,24 @@ export const SecretItem = memo(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (data: TFormSchema) => {
|
const handleFormSubmit = async (data: TFormSchema) => {
|
||||||
|
const hasDirectReferences = importedBy?.some(({ folders }) =>
|
||||||
|
folders?.some(({ secrets }) =>
|
||||||
|
secrets?.some(({ referencedSecretKey }) => referencedSecretKey === secret.key)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasDirectReferences) {
|
||||||
|
handlePopUpOpen("editSecret", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditSecret = async (data: TFormSchema) => {
|
||||||
|
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||||
|
handlePopUpClose("editSecret");
|
||||||
|
};
|
||||||
|
|
||||||
const handleTagSelect = (tag: WsTag) => {
|
const handleTagSelect = (tag: WsTag) => {
|
||||||
if (selectedTagsGroupById?.[tag.id]) {
|
if (selectedTagsGroupById?.[tag.id]) {
|
||||||
const tagPos = selectedTags.findIndex(({ id }) => id === tag.id);
|
const tagPos = selectedTags.findIndex(({ id }) => id === tag.id);
|
||||||
@@ -706,6 +735,26 @@ export const SecretItem = memo(
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.editSecret.isOpen}
|
||||||
|
deleteKey="confirm"
|
||||||
|
buttonColorSchema="secondary"
|
||||||
|
buttonText="Save"
|
||||||
|
subTitle=""
|
||||||
|
title="Do you want to edit this secret?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("editSecret", isOpen)}
|
||||||
|
onDeleteApproved={() => handleEditSecret(popUp?.editSecret?.data)}
|
||||||
|
formContent={
|
||||||
|
importedBy &&
|
||||||
|
importedBy.length > 0 && (
|
||||||
|
<CollapsibleSecretImports
|
||||||
|
importedBy={importedBy}
|
||||||
|
secretsToDelete={[secret.key]}
|
||||||
|
onlyReferences
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-3
@@ -8,6 +8,7 @@ import { DeleteActionModal } from "@app/components/v2";
|
|||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
|
import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
|
||||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||||
|
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||||
@@ -29,11 +30,12 @@ type Props = {
|
|||||||
tags?: WsTag[];
|
tags?: WsTag[];
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
isProtectedBranch?: boolean;
|
isProtectedBranch?: boolean;
|
||||||
|
usedBySecretSyncs?: UsedBySecretSyncs[];
|
||||||
importedBy?: {
|
importedBy?: {
|
||||||
environment: { name: string; slug: string };
|
environment: { name: string; slug: string };
|
||||||
folders: {
|
folders: {
|
||||||
name: string;
|
name: string;
|
||||||
secrets?: { secretId: string; referencedSecretKey: string }[];
|
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||||
isImported: boolean;
|
isImported: boolean;
|
||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
@@ -47,6 +49,7 @@ export const SecretListView = ({
|
|||||||
tags: wsTags = [],
|
tags: wsTags = [],
|
||||||
isVisible,
|
isVisible,
|
||||||
isProtectedBranch = false,
|
isProtectedBranch = false,
|
||||||
|
usedBySecretSyncs,
|
||||||
importedBy
|
importedBy
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -366,6 +369,7 @@ export const SecretListView = ({
|
|||||||
onSaveSecret={handleSaveSecret}
|
onSaveSecret={handleSaveSecret}
|
||||||
onDeleteSecret={onDeleteSecret}
|
onDeleteSecret={onDeleteSecret}
|
||||||
onDetailViewSecret={onDetailViewSecret}
|
onDetailViewSecret={onDetailViewSecret}
|
||||||
|
importedBy={importedBy}
|
||||||
onCreateTag={onCreateTag}
|
onCreateTag={onCreateTag}
|
||||||
handleSecretShare={() =>
|
handleSecretShare={() =>
|
||||||
handlePopUpOpen("createSharedSecret", {
|
handlePopUpOpen("createSharedSecret", {
|
||||||
@@ -382,10 +386,11 @@ export const SecretListView = ({
|
|||||||
onDeleteApproved={handleSecretDelete}
|
onDeleteApproved={handleSecretDelete}
|
||||||
buttonText="Delete Secret"
|
buttonText="Delete Secret"
|
||||||
formContent={
|
formContent={
|
||||||
importedBy &&
|
((importedBy && importedBy.length > 0) ||
|
||||||
importedBy.length > 0 && (
|
(usedBySecretSyncs && usedBySecretSyncs?.length > 0)) && (
|
||||||
<CollapsibleSecretImports
|
<CollapsibleSecretImports
|
||||||
importedBy={importedBy}
|
importedBy={importedBy}
|
||||||
|
usedBySecretSyncs={usedBySecretSyncs}
|
||||||
secretsToDelete={[(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key || ""]}
|
secretsToDelete={[(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key || ""]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user