mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 19:28:33 +00:00
Merge pull request #4890 from Infisical/feat/ENG-3443
fix: group rotated secrets under rotation row on the UI dashboard
This commit is contained in:
@@ -214,7 +214,10 @@ export const secretRotationV2DALFactory = (
|
|||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const extendedQuery = baseSecretRotationV2Query({ filter, db, tx, options })
|
const { limit, offset = 0, sort, ...queryOptions } = options || {};
|
||||||
|
const baseOptions = { ...queryOptions };
|
||||||
|
|
||||||
|
const subquery = baseSecretRotationV2Query({ filter, db, tx, options: baseOptions })
|
||||||
.join(
|
.join(
|
||||||
TableName.SecretRotationV2SecretMapping,
|
TableName.SecretRotationV2SecretMapping,
|
||||||
`${TableName.SecretRotationV2SecretMapping}.rotationId`,
|
`${TableName.SecretRotationV2SecretMapping}.rotationId`,
|
||||||
@@ -233,6 +236,7 @@ export const secretRotationV2DALFactory = (
|
|||||||
)
|
)
|
||||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||||
.select(
|
.select(
|
||||||
|
selectAllTableCols(TableName.SecretRotationV2),
|
||||||
db.ref("id").withSchema(TableName.SecretV2).as("secretId"),
|
db.ref("id").withSchema(TableName.SecretV2).as("secretId"),
|
||||||
db.ref("key").withSchema(TableName.SecretV2).as("secretKey"),
|
db.ref("key").withSchema(TableName.SecretV2).as("secretKey"),
|
||||||
db.ref("version").withSchema(TableName.SecretV2).as("secretVersion"),
|
db.ref("version").withSchema(TableName.SecretV2).as("secretVersion"),
|
||||||
@@ -252,18 +256,31 @@ export const secretRotationV2DALFactory = (
|
|||||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue"),
|
||||||
|
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.SecretRotationV2}."createdAt" DESC) as rank`)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
void extendedQuery.where((query) => {
|
void subquery.where((qb) => {
|
||||||
void query
|
void qb
|
||||||
.whereILike(`${TableName.SecretV2}.key`, `%${search}%`)
|
.whereILike(`${TableName.SecretV2}.key`, `%${search}%`)
|
||||||
.orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`);
|
.orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretRotations = await extendedQuery;
|
let secretRotations: Awaited<typeof subquery>;
|
||||||
|
if (limit !== undefined) {
|
||||||
|
const rankOffset = offset + 1;
|
||||||
|
const queryWithLimit = (tx || db)
|
||||||
|
.with("inner", subquery)
|
||||||
|
.select("*")
|
||||||
|
.from("inner")
|
||||||
|
.where("inner.rank", ">=", rankOffset)
|
||||||
|
.andWhere("inner.rank", "<", rankOffset + limit);
|
||||||
|
secretRotations = (await queryWithLimit) as unknown as Awaited<typeof subquery>;
|
||||||
|
} else {
|
||||||
|
secretRotations = await subquery;
|
||||||
|
}
|
||||||
|
|
||||||
if (!secretRotations.length) return [];
|
if (!secretRotations.length) return [];
|
||||||
|
|
||||||
|
|||||||
@@ -624,7 +624,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretValueHidden: z.boolean(),
|
secretValueHidden: z.boolean(),
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.optional(),
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tags: SanitizedTagSchema.array().optional()
|
tags: SanitizedTagSchema.array().optional(),
|
||||||
|
reminder: RemindersSchema.extend({
|
||||||
|
recipients: z.string().array()
|
||||||
|
}).nullable()
|
||||||
})
|
})
|
||||||
.nullable()
|
.nullable()
|
||||||
.array()
|
.array()
|
||||||
@@ -743,6 +746,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>
|
ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>
|
||||||
>[number]["secrets"][number] & {
|
>[number]["secrets"][number] & {
|
||||||
isEmpty: boolean;
|
isEmpty: boolean;
|
||||||
|
reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null;
|
||||||
}
|
}
|
||||||
> | null)[];
|
> | null)[];
|
||||||
})[]
|
})[]
|
||||||
@@ -847,27 +851,38 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
|
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
|
||||||
secretRotations = (
|
const rawSecretRotations = await server.services.secretRotationV2.getDashboardSecretRotations(
|
||||||
await server.services.secretRotationV2.getDashboardSecretRotations(
|
{
|
||||||
{
|
projectId,
|
||||||
projectId,
|
search,
|
||||||
search,
|
orderBy,
|
||||||
orderBy,
|
orderDirection,
|
||||||
orderDirection,
|
environments: [environment],
|
||||||
environments: [environment],
|
secretPath,
|
||||||
secretPath,
|
limit: remainingLimit,
|
||||||
limit: remainingLimit,
|
offset: adjustedOffset
|
||||||
offset: adjustedOffset
|
},
|
||||||
},
|
req.permission
|
||||||
req.permission
|
);
|
||||||
)
|
|
||||||
).map((rotation) => ({
|
const allRotationSecretIds = rawSecretRotations
|
||||||
|
.flatMap((rotation) => rotation.secrets)
|
||||||
|
.filter((secret) => Boolean(secret))
|
||||||
|
.map((secret) => secret.id);
|
||||||
|
|
||||||
|
const rotationReminders =
|
||||||
|
allRotationSecretIds.length > 0
|
||||||
|
? await server.services.reminder.getRemindersForDashboard(allRotationSecretIds)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
secretRotations = rawSecretRotations.map((rotation) => ({
|
||||||
...rotation,
|
...rotation,
|
||||||
secrets: rotation.secrets.map((secret) =>
|
secrets: rotation.secrets.map((secret) =>
|
||||||
secret
|
secret
|
||||||
? {
|
? {
|
||||||
...secret,
|
...secret,
|
||||||
isEmpty: !secret.secretValue
|
isEmpty: !secret.secretValue,
|
||||||
|
reminder: rotationReminders[secret.id] ?? null
|
||||||
}
|
}
|
||||||
: secret
|
: secret
|
||||||
)
|
)
|
||||||
@@ -948,7 +963,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
search,
|
search,
|
||||||
tagSlugs: tags,
|
tagSlugs: tags,
|
||||||
includeTagsInSearch: true,
|
includeTagsInSearch: true,
|
||||||
includeMetadataInSearch: true
|
includeMetadataInSearch: true,
|
||||||
|
excludeRotatedSecrets: includeSecretRotations
|
||||||
});
|
});
|
||||||
|
|
||||||
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
||||||
@@ -970,7 +986,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
offset: adjustedOffset,
|
offset: adjustedOffset,
|
||||||
tagSlugs: tags,
|
tagSlugs: tags,
|
||||||
includeTagsInSearch: true,
|
includeTagsInSearch: true,
|
||||||
includeMetadataInSearch: true
|
includeMetadataInSearch: true,
|
||||||
|
excludeRotatedSecrets: includeSecretRotations
|
||||||
})
|
})
|
||||||
).secrets;
|
).secrets;
|
||||||
|
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
tagSlugs?: string[];
|
tagSlugs?: string[];
|
||||||
includeTagsInSearch?: boolean;
|
includeTagsInSearch?: boolean;
|
||||||
includeMetadataInSearch?: boolean;
|
includeMetadataInSearch?: boolean;
|
||||||
|
excludeRotatedSecrets?: boolean;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@@ -481,6 +482,10 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filters?.excludeRotatedSecrets) {
|
||||||
|
void query.whereNull(`${TableName.SecretRotationV2SecretMapping}.secretId`);
|
||||||
|
}
|
||||||
|
|
||||||
const secrets = await query;
|
const secrets = await query;
|
||||||
|
|
||||||
// @ts-expect-error not inferred by knex
|
// @ts-expect-error not inferred by knex
|
||||||
@@ -594,6 +599,11 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
void bd.whereIn(`${TableName.SecretTag}.slug`, slugs);
|
void bd.whereIn(`${TableName.SecretTag}.slug`, slugs);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.where((bd) => {
|
||||||
|
if (filters?.excludeRotatedSecrets) {
|
||||||
|
void bd.whereNull(`${TableName.SecretRotationV2SecretMapping}.secretId`);
|
||||||
|
}
|
||||||
|
})
|
||||||
.orderBy(
|
.orderBy(
|
||||||
filters?.orderBy === SecretsOrderBy.Name ? "key" : "id",
|
filters?.orderBy === SecretsOrderBy.Name ? "key" : "id",
|
||||||
filters?.orderDirection ?? OrderByDirection.ASC
|
filters?.orderDirection ?? OrderByDirection.ASC
|
||||||
|
|||||||
@@ -483,8 +483,8 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
});
|
});
|
||||||
if (!sharedSecretToModify)
|
if (!sharedSecretToModify)
|
||||||
throw new NotFoundError({ message: `Secret with name ${inputSecret.secretName} not found` });
|
throw new NotFoundError({ message: `Secret with name ${inputSecret.secretName} not found` });
|
||||||
if (sharedSecretToModify.isRotatedSecret && (inputSecret.newSecretName || inputSecret.secretValue))
|
if (sharedSecretToModify.isRotatedSecret && inputSecret.newSecretName)
|
||||||
throw new BadRequestError({ message: "Cannot update rotated secret name or value" });
|
throw new BadRequestError({ message: "Cannot update rotated secret name" });
|
||||||
secretId = sharedSecretToModify.id;
|
secretId = sharedSecretToModify.id;
|
||||||
secret = sharedSecretToModify;
|
secret = sharedSecretToModify;
|
||||||
}
|
}
|
||||||
@@ -888,6 +888,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
| "tagSlugs"
|
| "tagSlugs"
|
||||||
| "environment"
|
| "environment"
|
||||||
| "search"
|
| "search"
|
||||||
|
| "excludeRotatedSecrets"
|
||||||
>) => {
|
>) => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
@@ -1934,8 +1935,14 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
if (el.isRotatedSecret) {
|
if (el.isRotatedSecret) {
|
||||||
const input = secretsToUpdateGroupByPath[secretPath].find((i) => i.secretKey === el.key);
|
const input = secretsToUpdateGroupByPath[secretPath].find((i) => i.secretKey === el.key);
|
||||||
|
|
||||||
if (input && (input.newSecretName || input.secretValue))
|
if (input) {
|
||||||
throw new BadRequestError({ message: `Cannot update rotated secret name or value: ${el.key}` });
|
if (input.newSecretName) {
|
||||||
|
delete input.newSecretName;
|
||||||
|
}
|
||||||
|
if (input.secretValue !== undefined) {
|
||||||
|
delete input.secretValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2061,8 +2068,11 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
commitChanges,
|
commitChanges,
|
||||||
inputSecrets: secretsToUpdate.map((el) => {
|
inputSecrets: secretsToUpdate.map((el) => {
|
||||||
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
|
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
|
||||||
|
const shouldUpdateValue = !originalSecret.isRotatedSecret && typeof el.secretValue !== "undefined";
|
||||||
|
const shouldUpdateName = !originalSecret.isRotatedSecret && el.newSecretName;
|
||||||
|
|
||||||
const encryptedValue =
|
const encryptedValue =
|
||||||
typeof el.secretValue !== "undefined"
|
shouldUpdateValue && el.secretValue !== undefined
|
||||||
? {
|
? {
|
||||||
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob,
|
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob,
|
||||||
references: secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences
|
references: secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences
|
||||||
@@ -2077,7 +2087,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
||||||
),
|
),
|
||||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||||
key: el.newSecretName || el.secretKey,
|
key: shouldUpdateName ? el.newSecretName : el.secretKey,
|
||||||
tags: el.tagIds,
|
tags: el.tagIds,
|
||||||
secretMetadata: el.secretMetadata,
|
secretMetadata: el.secretMetadata,
|
||||||
...encryptedValue
|
...encryptedValue
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export type TGetSecretsDTO = {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
keys?: string[];
|
keys?: string[];
|
||||||
|
excludeRotatedSecrets?: boolean;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetSecretsMissingReadValuePermissionDTO = Omit<
|
export type TGetSecretsMissingReadValuePermissionDTO = Omit<
|
||||||
@@ -362,6 +363,7 @@ export type TFindSecretsByFolderIdsFilter = {
|
|||||||
includeTagsInSearch?: boolean;
|
includeTagsInSearch?: boolean;
|
||||||
includeMetadataInSearch?: boolean;
|
includeMetadataInSearch?: boolean;
|
||||||
keys?: string[];
|
keys?: string[];
|
||||||
|
excludeRotatedSecrets?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecretsRawByFolderMappingsDTO = {
|
export type TGetSecretsRawByFolderMappingsDTO = {
|
||||||
|
|||||||
@@ -1154,6 +1154,7 @@ export const secretServiceFactory = ({
|
|||||||
| "search"
|
| "search"
|
||||||
| "includeTagsInSearch"
|
| "includeTagsInSearch"
|
||||||
| "includeMetadataInSearch"
|
| "includeMetadataInSearch"
|
||||||
|
| "excludeRotatedSecrets"
|
||||||
>) => {
|
>) => {
|
||||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export type TGetSecretsRawDTO = {
|
|||||||
keys?: string[];
|
keys?: string[];
|
||||||
includeTagsInSearch?: boolean;
|
includeTagsInSearch?: boolean;
|
||||||
includeMetadataInSearch?: boolean;
|
includeMetadataInSearch?: boolean;
|
||||||
|
excludeRotatedSecrets?: boolean;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetSecretAccessListDTO = {
|
export type TGetSecretAccessListDTO = {
|
||||||
|
|||||||
@@ -685,12 +685,17 @@ const Page = () => {
|
|||||||
setDebouncedSearchFilter("");
|
setDebouncedSearchFilter("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMergedSecretsWithPending = () => {
|
const getMergedSecretsWithPending = (
|
||||||
|
paramSecrets?: (SecretV3RawSanitized | null)[]
|
||||||
|
): SecretV3RawSanitized[] => {
|
||||||
|
const sanitizedParamSecrets = paramSecrets?.filter(Boolean) as
|
||||||
|
| SecretV3RawSanitized[]
|
||||||
|
| undefined;
|
||||||
if (!isBatchMode || pendingChanges.secrets.length === 0) {
|
if (!isBatchMode || pendingChanges.secrets.length === 0) {
|
||||||
return secrets;
|
return sanitizedParamSecrets || secrets || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedSecrets = [...(secrets || [])] as (SecretV3RawSanitized & {
|
const mergedSecrets = [...(sanitizedParamSecrets || secrets || [])] as (SecretV3RawSanitized & {
|
||||||
originalKey?: string;
|
originalKey?: string;
|
||||||
})[];
|
})[];
|
||||||
|
|
||||||
@@ -1072,7 +1077,17 @@ const Page = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canReadSecretRotations && Boolean(secretRotations?.length) && (
|
{canReadSecretRotations && Boolean(secretRotations?.length) && (
|
||||||
<SecretRotationListView secretRotations={secretRotations} />
|
<SecretRotationListView
|
||||||
|
secretRotations={secretRotations}
|
||||||
|
colWidth={colWidth}
|
||||||
|
tags={tags}
|
||||||
|
projectId={projectId}
|
||||||
|
secretPath={secretPath}
|
||||||
|
isProtectedBranch={isProtectedBranch}
|
||||||
|
importedBy={importedBy}
|
||||||
|
usedBySecretSyncs={usedBySecretSyncs}
|
||||||
|
getMergedSecretsWithPending={getMergedSecretsWithPending}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{canReadSecret && Boolean(mergedSecrets?.length) && (
|
{canReadSecret && Boolean(mergedSecrets?.length) && (
|
||||||
<SecretListView
|
<SecretListView
|
||||||
|
|||||||
+8
-2
@@ -733,7 +733,7 @@ export const SecretItem = memo(
|
|||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="override-value"
|
ariaLabel="override-value"
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed || isRotatedSecret}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleOverrideClick}
|
onClick={handleOverrideClick}
|
||||||
@@ -742,7 +742,13 @@ export const SecretItem = memo(
|
|||||||
isOverridden && "w-5 text-primary"
|
isOverridden && "w-5 text-primary"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Tooltip content={`${isOverridden ? "Remove" : "Add"} Override`}>
|
<Tooltip
|
||||||
|
content={
|
||||||
|
isRotatedSecret
|
||||||
|
? "Unavailable for rotated secrets"
|
||||||
|
: `${isOverridden ? "Remove" : "Add"} Override`
|
||||||
|
}
|
||||||
|
>
|
||||||
<FontAwesomeSymbol
|
<FontAwesomeSymbol
|
||||||
symbolName={FontAwesomeSpriteName.Override}
|
symbolName={FontAwesomeSpriteName.Override}
|
||||||
className="h-3.5 w-3.5"
|
className="h-3.5 w-3.5"
|
||||||
|
|||||||
+29
-22
@@ -52,6 +52,7 @@ type Props = {
|
|||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
colWidth: number;
|
colWidth: number;
|
||||||
|
excludePendingCreates?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretListView = ({
|
export const SecretListView = ({
|
||||||
@@ -64,7 +65,8 @@ export const SecretListView = ({
|
|||||||
isProtectedBranch = false,
|
isProtectedBranch = false,
|
||||||
usedBySecretSyncs,
|
usedBySecretSyncs,
|
||||||
importedBy,
|
importedBy,
|
||||||
colWidth
|
colWidth,
|
||||||
|
excludePendingCreates = false
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
@@ -580,27 +582,32 @@ export const SecretListView = ({
|
|||||||
{FontAwesomeSpriteSymbols.map(({ icon, symbol }) => (
|
{FontAwesomeSpriteSymbols.map(({ icon, symbol }) => (
|
||||||
<FontAwesomeIcon icon={icon} symbol={symbol} key={`font-awesome-svg-spritie-${symbol}`} />
|
<FontAwesomeIcon icon={icon} symbol={symbol} key={`font-awesome-svg-spritie-${symbol}`} />
|
||||||
))}
|
))}
|
||||||
{secrets.map((secret) => (
|
{secrets
|
||||||
<SecretItem
|
.filter((secret) => {
|
||||||
colWidth={colWidth}
|
if (!excludePendingCreates) return true;
|
||||||
environment={environment}
|
return !secret.isPending || secret.pendingAction !== PendingAction.Create;
|
||||||
secretPath={secretPath}
|
})
|
||||||
tags={wsTags}
|
.map((secret) => (
|
||||||
isSelected={Boolean(selectedSecrets?.[secret.id])}
|
<SecretItem
|
||||||
onToggleSecretSelect={toggleSelectedSecret}
|
colWidth={colWidth}
|
||||||
isVisible={isVisible}
|
environment={environment}
|
||||||
secret={secret}
|
secretPath={secretPath}
|
||||||
key={secret.id}
|
tags={wsTags}
|
||||||
onSaveSecret={handleSaveSecret}
|
isSelected={Boolean(selectedSecrets?.[secret.id])}
|
||||||
onDeleteSecret={onDeleteSecret}
|
onToggleSecretSelect={toggleSelectedSecret}
|
||||||
onDetailViewSecret={onDetailViewSecret}
|
isVisible={isVisible}
|
||||||
importedBy={importedBy}
|
secret={secret}
|
||||||
onCreateTag={onCreateTag}
|
key={secret.id}
|
||||||
onShareSecret={onShareSecret}
|
onSaveSecret={handleSaveSecret}
|
||||||
isPending={secret.isPending}
|
onDeleteSecret={onDeleteSecret}
|
||||||
pendingAction={secret.pendingAction}
|
onDetailViewSecret={onDetailViewSecret}
|
||||||
/>
|
importedBy={importedBy}
|
||||||
))}
|
onCreateTag={onCreateTag}
|
||||||
|
onShareSecret={onShareSecret}
|
||||||
|
isPending={secret.isPending}
|
||||||
|
pendingAction={secret.pendingAction}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteSecret.isOpen}
|
isOpen={popUp.deleteSecret.isOpen}
|
||||||
deleteKey={(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}
|
deleteKey={(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}
|
||||||
|
|||||||
+60
-2
@@ -18,8 +18,11 @@ import { IconButton, Modal, ModalContent, TableContainer, Tag, Tooltip } from "@
|
|||||||
import { ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||||
|
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||||
|
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
import { SecretListView } from "../SecretListView";
|
||||||
import { SecretRotationSecretRow } from "./SecretRotationSecretRow";
|
import { SecretRotationSecretRow } from "./SecretRotationSecretRow";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -28,6 +31,23 @@ type Props = {
|
|||||||
onRotate: () => void;
|
onRotate: () => void;
|
||||||
onViewGeneratedCredentials: () => void;
|
onViewGeneratedCredentials: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
projectId: string;
|
||||||
|
secretPath?: string;
|
||||||
|
tags?: WsTag[];
|
||||||
|
isProtectedBranch?: boolean;
|
||||||
|
usedBySecretSyncs?: UsedBySecretSyncs[];
|
||||||
|
importedBy?: {
|
||||||
|
environment: { name: string; slug: string };
|
||||||
|
folders: {
|
||||||
|
name: string;
|
||||||
|
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||||
|
isImported: boolean;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
colWidth: number;
|
||||||
|
getMergedSecretsWithPending: (
|
||||||
|
paramSecrets?: (SecretV3RawSanitized | null)[]
|
||||||
|
) => SecretV3RawSanitized[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretRotationItem = ({
|
export const SecretRotationItem = ({
|
||||||
@@ -35,16 +55,40 @@ export const SecretRotationItem = ({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onRotate,
|
onRotate,
|
||||||
onViewGeneratedCredentials,
|
onViewGeneratedCredentials,
|
||||||
onDelete
|
onDelete,
|
||||||
|
projectId,
|
||||||
|
secretPath = "/",
|
||||||
|
tags = [],
|
||||||
|
isProtectedBranch = false,
|
||||||
|
usedBySecretSyncs,
|
||||||
|
importedBy,
|
||||||
|
colWidth,
|
||||||
|
getMergedSecretsWithPending
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { name, type, environment, folder, secrets, description } = secretRotation;
|
const { name, type, environment, folder, secrets, description } = secretRotation;
|
||||||
|
|
||||||
const { name: rotationType, image } = SECRET_ROTATION_MAP[type];
|
const { name: rotationType, image } = SECRET_ROTATION_MAP[type];
|
||||||
const [showSecrets, setShowSecrets] = useState(false);
|
const [showSecrets, setShowSecrets] = useState(false);
|
||||||
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={twMerge("group flex border-b border-mineshaft-600 hover:bg-mineshaft-700")}>
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
|
||||||
|
)}
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsExpanded(!isExpanded);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
aria-label={`${isExpanded ? "Collapse" : "Expand"} rotation secrets for ${name}`}
|
||||||
|
>
|
||||||
<div className="text- flex w-11 items-center py-2 pl-5 text-mineshaft-400">
|
<div className="text- flex w-11 items-center py-2 pl-5 text-mineshaft-400">
|
||||||
<FontAwesomeIcon icon={faRotate} />
|
<FontAwesomeIcon icon={faRotate} />
|
||||||
</div>
|
</div>
|
||||||
@@ -198,6 +242,20 @@ export const SecretRotationItem = ({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
{isExpanded && (
|
||||||
|
<SecretListView
|
||||||
|
colWidth={colWidth}
|
||||||
|
secrets={getMergedSecretsWithPending(secretRotation.secrets) || []}
|
||||||
|
tags={tags}
|
||||||
|
environment={environment.slug}
|
||||||
|
projectId={projectId}
|
||||||
|
secretPath={secretPath}
|
||||||
|
isProtectedBranch={isProtectedBranch}
|
||||||
|
importedBy={importedBy}
|
||||||
|
usedBySecretSyncs={usedBySecretSyncs}
|
||||||
|
excludePendingCreates
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Modal onOpenChange={setShowSecrets} isOpen={showSecrets}>
|
<Modal onOpenChange={setShowSecrets} isOpen={showSecrets}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
|
|||||||
+38
-1
@@ -3,15 +3,44 @@ import { EditSecretRotationV2Modal } from "@app/components/secret-rotations-v2/E
|
|||||||
import { RotateSecretRotationV2Modal } from "@app/components/secret-rotations-v2/RotateSecretRotationV2Modal";
|
import { RotateSecretRotationV2Modal } from "@app/components/secret-rotations-v2/RotateSecretRotationV2Modal";
|
||||||
import { ViewSecretRotationV2GeneratedCredentialsModal } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials";
|
import { ViewSecretRotationV2GeneratedCredentialsModal } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||||
|
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { SecretRotationItem } from "./SecretRotationItem";
|
import { SecretRotationItem } from "./SecretRotationItem";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
secretRotations?: TSecretRotationV2[];
|
secretRotations?: TSecretRotationV2[];
|
||||||
|
projectId: string;
|
||||||
|
secretPath?: string;
|
||||||
|
tags?: WsTag[];
|
||||||
|
isProtectedBranch?: boolean;
|
||||||
|
usedBySecretSyncs?: UsedBySecretSyncs[];
|
||||||
|
importedBy?: {
|
||||||
|
environment: { name: string; slug: string };
|
||||||
|
folders: {
|
||||||
|
name: string;
|
||||||
|
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||||
|
isImported: boolean;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
colWidth: number;
|
||||||
|
getMergedSecretsWithPending: (
|
||||||
|
secretParams?: (SecretV3RawSanitized | null)[]
|
||||||
|
) => SecretV3RawSanitized[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretRotationListView = ({ secretRotations }: Props) => {
|
export const SecretRotationListView = ({
|
||||||
|
secretRotations,
|
||||||
|
projectId,
|
||||||
|
secretPath = "/",
|
||||||
|
tags = [],
|
||||||
|
isProtectedBranch = false,
|
||||||
|
usedBySecretSyncs,
|
||||||
|
importedBy,
|
||||||
|
colWidth,
|
||||||
|
getMergedSecretsWithPending
|
||||||
|
}: Props) => {
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
"editSecretRotation",
|
"editSecretRotation",
|
||||||
"rotateSecretRotation",
|
"rotateSecretRotation",
|
||||||
@@ -31,6 +60,14 @@ export const SecretRotationListView = ({ secretRotations }: Props) => {
|
|||||||
handlePopUpOpen("viewSecretRotationGeneratedCredentials", secretRotation)
|
handlePopUpOpen("viewSecretRotationGeneratedCredentials", secretRotation)
|
||||||
}
|
}
|
||||||
onDelete={() => handlePopUpOpen("deleteSecretRotation", secretRotation)}
|
onDelete={() => handlePopUpOpen("deleteSecretRotation", secretRotation)}
|
||||||
|
colWidth={colWidth}
|
||||||
|
tags={tags}
|
||||||
|
projectId={projectId}
|
||||||
|
secretPath={secretPath}
|
||||||
|
isProtectedBranch={isProtectedBranch}
|
||||||
|
importedBy={importedBy}
|
||||||
|
usedBySecretSyncs={usedBySecretSyncs}
|
||||||
|
getMergedSecretsWithPending={getMergedSecretsWithPending}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<EditSecretRotationV2Modal
|
<EditSecretRotationV2Modal
|
||||||
|
|||||||
Reference in New Issue
Block a user