mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 09:28:06 +00:00
Addressed PR suggestions
This commit is contained in:
@@ -535,7 +535,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
await server.services.pit.processNewCommitRaw({
|
const result = await server.services.pit.processNewCommitRaw({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
@@ -549,6 +549,24 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
folders: req.body.changes.folders
|
folders: req.body.changes.folders
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.body.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.PIT_PROCESS_NEW_COMMIT_RAW,
|
||||||
|
metadata: {
|
||||||
|
commitId: result.commitId,
|
||||||
|
folderChanges: result.folderChanges,
|
||||||
|
approvalId: result.approvalId,
|
||||||
|
projectId: req.body.projectId,
|
||||||
|
environment: req.body.environment,
|
||||||
|
secretPath: req.body.secretPath,
|
||||||
|
message: req.body.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return { message: "success" };
|
return { message: "success" };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -449,6 +449,7 @@ export enum EventType {
|
|||||||
PIT_REVERT_COMMIT = "pit-revert-commit",
|
PIT_REVERT_COMMIT = "pit-revert-commit",
|
||||||
PIT_GET_FOLDER_STATE = "pit-get-folder-state",
|
PIT_GET_FOLDER_STATE = "pit-get-folder-state",
|
||||||
PIT_COMPARE_FOLDER_STATES = "pit-compare-folder-states",
|
PIT_COMPARE_FOLDER_STATES = "pit-compare-folder-states",
|
||||||
|
PIT_PROCESS_NEW_COMMIT_RAW = "pit-process-new-commit-raw",
|
||||||
SECRET_SCANNING_DATA_SOURCE_LIST = "secret-scanning-data-source-list",
|
SECRET_SCANNING_DATA_SOURCE_LIST = "secret-scanning-data-source-list",
|
||||||
SECRET_SCANNING_DATA_SOURCE_CREATE = "secret-scanning-data-source-create",
|
SECRET_SCANNING_DATA_SOURCE_CREATE = "secret-scanning-data-source-create",
|
||||||
SECRET_SCANNING_DATA_SOURCE_UPDATE = "secret-scanning-data-source-update",
|
SECRET_SCANNING_DATA_SOURCE_UPDATE = "secret-scanning-data-source-update",
|
||||||
@@ -3222,6 +3223,23 @@ interface PitCompareFolderStatesEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PitProcessNewCommitRawEvent {
|
||||||
|
type: EventType.PIT_PROCESS_NEW_COMMIT_RAW;
|
||||||
|
metadata: {
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
message: string;
|
||||||
|
folderChanges: {
|
||||||
|
create: string[];
|
||||||
|
update: string[];
|
||||||
|
delete: string[];
|
||||||
|
};
|
||||||
|
approvalId?: string;
|
||||||
|
commitId?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface SecretScanningDataSourceListEvent {
|
interface SecretScanningDataSourceListEvent {
|
||||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST;
|
type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -3658,6 +3676,7 @@ export type Event =
|
|||||||
| PitRevertCommitEvent
|
| PitRevertCommitEvent
|
||||||
| PitCompareFolderStatesEvent
|
| PitCompareFolderStatesEvent
|
||||||
| PitGetFolderStateEvent
|
| PitGetFolderStateEvent
|
||||||
|
| PitProcessNewCommitRawEvent
|
||||||
| SecretScanningDataSourceListEvent
|
| SecretScanningDataSourceListEvent
|
||||||
| SecretScanningDataSourceGetEvent
|
| SecretScanningDataSourceGetEvent
|
||||||
| SecretScanningDataSourceCreateEvent
|
| SecretScanningDataSourceCreateEvent
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
import { SecretProtectionType, TProcessNewCommitRawDTO } from "@app/services/secret/secret-types";
|
import { TProcessNewCommitRawDTO } from "@app/services/secret/secret-types";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
import { TSecretV2BridgeServiceFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-service";
|
import { TSecretV2BridgeServiceFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-service";
|
||||||
@@ -545,11 +545,7 @@ export const pitServiceFactory = ({
|
|||||||
|
|
||||||
// Check update operations
|
// Check update operations
|
||||||
...(changes.secrets?.update
|
...(changes.secrets?.update
|
||||||
?.filter(
|
?.filter((sec) => sec.newSecretName && sec.newSecretName !== sec.newSecretName.toUpperCase())
|
||||||
(sec) =>
|
|
||||||
sec.secretKey !== sec.secretKey.toUpperCase() ||
|
|
||||||
(sec.newSecretKey && sec.newSecretKey !== sec.newSecretKey.toUpperCase())
|
|
||||||
)
|
|
||||||
.map((sec) => sec.secretKey) ?? [])
|
.map((sec) => sec.secretKey) ?? [])
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -562,17 +558,22 @@ export const pitServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await folderCommitDAL.transaction(async (trx) => {
|
return await folderCommitDAL.transaction(async (trx) => {
|
||||||
const targetFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const targetFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath, trx);
|
||||||
if (!targetFolder)
|
if (!targetFolder)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`,
|
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`,
|
||||||
name: "CreateManySecret"
|
name: "CreateManySecret"
|
||||||
});
|
});
|
||||||
const commitChanges: TCommitResourceChangeDTO[] = [];
|
const commitChanges: TCommitResourceChangeDTO[] = [];
|
||||||
|
const folderChanges: { create: string[], update: string[], delete: string[] } = {
|
||||||
|
create: [],
|
||||||
|
update: [],
|
||||||
|
delete: []
|
||||||
|
};
|
||||||
|
|
||||||
if ((changes.folders?.create?.length ?? 0) > 0) {
|
if ((changes.folders?.create?.length ?? 0) > 0) {
|
||||||
await folderService.createManyFolders({
|
const createdFolders = await folderService.createManyFolders({
|
||||||
projectId,
|
projectId,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -588,10 +589,11 @@ export const pitServiceFactory = ({
|
|||||||
tx: trx,
|
tx: trx,
|
||||||
commitChanges
|
commitChanges
|
||||||
});
|
});
|
||||||
|
folderChanges.create.push(...createdFolders.folders.map((folder) => folder.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((changes.folders?.update?.length ?? 0) > 0) {
|
if ((changes.folders?.update?.length ?? 0) > 0) {
|
||||||
await folderService.updateManyFolders({
|
const updatedFolders = await folderService.updateManyFolders({
|
||||||
projectId,
|
projectId,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -608,10 +610,11 @@ export const pitServiceFactory = ({
|
|||||||
tx: trx,
|
tx: trx,
|
||||||
commitChanges
|
commitChanges
|
||||||
});
|
});
|
||||||
|
folderChanges.update.push(...updatedFolders.newFolders.map((folder) => folder.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((changes.folders?.delete?.length ?? 0) > 0) {
|
if ((changes.folders?.delete?.length ?? 0) > 0) {
|
||||||
await folderService.deleteManyFolders({
|
const deletedFolders = await folderService.deleteManyFolders({
|
||||||
projectId,
|
projectId,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -626,46 +629,60 @@ export const pitServiceFactory = ({
|
|||||||
tx: trx,
|
tx: trx,
|
||||||
commitChanges
|
commitChanges
|
||||||
});
|
});
|
||||||
|
folderChanges.delete.push(...deletedFolders.folders.map((folder) => folder.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (policy) {
|
if (policy) {
|
||||||
const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({
|
if (
|
||||||
policy,
|
(changes.secrets?.create?.length ?? 0) > 0 ||
|
||||||
secretPath,
|
(changes.secrets?.update?.length ?? 0) > 0 ||
|
||||||
environment,
|
(changes.secrets?.delete?.length ?? 0) > 0
|
||||||
projectId,
|
) {
|
||||||
actor,
|
const approval = await secretApprovalRequestService.generateSecretApprovalRequestV2Bridge({
|
||||||
actorId,
|
policy,
|
||||||
actorOrgId,
|
secretPath,
|
||||||
actorAuthMethod,
|
environment,
|
||||||
data: {
|
projectId,
|
||||||
[SecretOperations.Create]:
|
actor,
|
||||||
changes.secrets?.create?.map((el) => ({
|
actorId,
|
||||||
tagIds: el.tagIds,
|
actorOrgId,
|
||||||
secretValue: el.secretValue,
|
actorAuthMethod,
|
||||||
secretComment: el.secretComment,
|
data: {
|
||||||
metadata: el.metadata,
|
[SecretOperations.Create]:
|
||||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
changes.secrets?.create?.map((el) => ({
|
||||||
secretKey: el.secretKey,
|
tagIds: el.tagIds,
|
||||||
secretMetadata: el.secretMetadata
|
secretValue: el.secretValue,
|
||||||
})) ?? [],
|
secretComment: el.secretComment,
|
||||||
[SecretOperations.Update]:
|
metadata: el.metadata,
|
||||||
changes.secrets?.update?.map((el) => ({
|
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||||
tagIds: el.tagIds,
|
secretKey: el.secretKey,
|
||||||
secretValue: el.secretValue,
|
secretMetadata: el.secretMetadata
|
||||||
secretComment: el.secretComment,
|
})) ?? [],
|
||||||
metadata: el.metadata,
|
[SecretOperations.Update]:
|
||||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
changes.secrets?.update?.map((el) => ({
|
||||||
secretKey: el.secretKey,
|
tagIds: el.tagIds,
|
||||||
secretMetadata: el.secretMetadata
|
newSecretName: el.newSecretName,
|
||||||
})) ?? [],
|
secretValue: el.secretValue,
|
||||||
[SecretOperations.Delete]:
|
secretComment: el.secretComment,
|
||||||
changes.secrets?.delete?.map((el) => ({
|
metadata: el.metadata,
|
||||||
secretKey: el.secretKey
|
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||||
})) ?? []
|
secretKey: el.secretKey,
|
||||||
}
|
secretMetadata: el.secretMetadata
|
||||||
});
|
})) ?? [],
|
||||||
return { type: SecretProtectionType.Approval as const, approval };
|
[SecretOperations.Delete]:
|
||||||
|
changes.secrets?.delete?.map((el) => ({
|
||||||
|
secretKey: el.secretKey
|
||||||
|
})) ?? []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
approvalId: approval.id,
|
||||||
|
folderChanges
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
folderChanges
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((changes.secrets?.create?.length ?? 0) > 0) {
|
if ((changes.secrets?.create?.length ?? 0) > 0) {
|
||||||
@@ -712,7 +729,7 @@ export const pitServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (commitChanges?.length > 0) {
|
if (commitChanges?.length > 0) {
|
||||||
await folderCommitService.createCommit(
|
const commit = await folderCommitService.createCommit(
|
||||||
{
|
{
|
||||||
actor: {
|
actor: {
|
||||||
type: actor || ActorType.PLATFORM,
|
type: actor || ActorType.PLATFORM,
|
||||||
@@ -726,7 +743,14 @@ export const pitServiceFactory = ({
|
|||||||
},
|
},
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
return {
|
||||||
|
folderChanges,
|
||||||
|
commitId: commit?.id
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
folderChanges
|
||||||
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment }, tx);
|
||||||
if (!env) {
|
if (!env) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: `Environment with slug '${environment}' in project with ID '${projectId}' not found`,
|
message: `Environment with slug '${environment}' in project with ID '${projectId}' not found`,
|
||||||
|
|||||||
@@ -558,7 +558,7 @@ export type TProcessNewCommitRawDTO = {
|
|||||||
}[];
|
}[];
|
||||||
update?: {
|
update?: {
|
||||||
secretKey: string;
|
secretKey: string;
|
||||||
newSecretKey?: string;
|
newSecretName?: string;
|
||||||
secretValue?: string;
|
secretValue?: string;
|
||||||
secretComment?: string;
|
secretComment?: string;
|
||||||
skipMultilineEncoding?: boolean;
|
skipMultilineEncoding?: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useBlocker } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
type TNavigationBlockerReturn = {
|
||||||
|
isBlocked: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNavigationBlocker = (
|
||||||
|
shouldBlock: boolean,
|
||||||
|
message: string = "Are you sure you want to leave? You may have unsaved changes."
|
||||||
|
): TNavigationBlockerReturn => {
|
||||||
|
const blockerFn = useCallback(() => {
|
||||||
|
if (!shouldBlock) return false;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
const confirmed = window.confirm(message);
|
||||||
|
|
||||||
|
return !confirmed;
|
||||||
|
}, [shouldBlock, message]);
|
||||||
|
|
||||||
|
useBlocker(blockerFn);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isBlocked: shouldBlock
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -138,24 +138,6 @@ const Page = () => {
|
|||||||
}
|
}
|
||||||
}, [isBatchMode, workspaceId, environment, secretPath, loadPendingChanges]);
|
}, [isBatchMode, workspaceId, environment, secretPath, loadPendingChanges]);
|
||||||
|
|
||||||
const handleCreateCommit = async (changes: PendingChanges, message: string) => {
|
|
||||||
try {
|
|
||||||
await createCommit({
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
pendingChanges: changes,
|
|
||||||
message
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to commit changes",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const canReadSecret = hasSecretReadValueOrDescribePermission(
|
const canReadSecret = hasSecretReadValueOrDescribePermission(
|
||||||
permission,
|
permission,
|
||||||
ProjectPermissionSecretActions.DescribeSecret,
|
ProjectPermissionSecretActions.DescribeSecret,
|
||||||
@@ -299,6 +281,30 @@ const Page = () => {
|
|||||||
});
|
});
|
||||||
const isProtectedBranch = Boolean(boardPolicy);
|
const isProtectedBranch = Boolean(boardPolicy);
|
||||||
|
|
||||||
|
const handleCreateCommit = async (changes: PendingChanges, message: string) => {
|
||||||
|
try {
|
||||||
|
await createCommit({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
pendingChanges: changes,
|
||||||
|
message
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
text: isProtectedBranch
|
||||||
|
? "Requested changes have been sent for review"
|
||||||
|
: "Changes committed successfully",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to commit changes",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: snapshotList,
|
data: snapshotList,
|
||||||
isFetchingNextPage: isFetchingNextSnapshotList,
|
isFetchingNextPage: isFetchingNextSnapshotList,
|
||||||
@@ -538,7 +544,7 @@ const Page = () => {
|
|||||||
value: change.secretValue,
|
value: change.secretValue,
|
||||||
comment: change.secretComment || "",
|
comment: change.secretComment || "",
|
||||||
skipMultilineEncoding: change.skipMultilineEncoding || false,
|
skipMultilineEncoding: change.skipMultilineEncoding || false,
|
||||||
tags: change.tags?.map((tag) => ({ id: tag.id, slug: tag.slug })) || [],
|
tags: change.tags || [],
|
||||||
secretMetadata: change.secretMetadata || [],
|
secretMetadata: change.secretMetadata || [],
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
@@ -568,7 +574,16 @@ const Page = () => {
|
|||||||
: mergedSecrets[updateIndex].skipMultilineEncoding,
|
: mergedSecrets[updateIndex].skipMultilineEncoding,
|
||||||
secretMetadata: change.secretMetadata || mergedSecrets[updateIndex].secretMetadata,
|
secretMetadata: change.secretMetadata || mergedSecrets[updateIndex].secretMetadata,
|
||||||
isPending: true,
|
isPending: true,
|
||||||
pendingAction: PendingAction.Update
|
pendingAction: PendingAction.Update,
|
||||||
|
tags:
|
||||||
|
change.tags?.map((tag) => ({
|
||||||
|
id: tag.id,
|
||||||
|
slug: tag.slug,
|
||||||
|
projectId: workspaceId,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
__v: 0
|
||||||
|
})) || []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -114,9 +114,9 @@ const savePendingChangesToStorage = (
|
|||||||
) => {
|
) => {
|
||||||
const key = `${STORAGE_KEY}_${generateContextKey(workspaceId, environment, secretPath)}`;
|
const key = `${STORAGE_KEY}_${generateContextKey(workspaceId, environment, secretPath)}`;
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(key, JSON.stringify(changes));
|
sessionStorage.setItem(key, JSON.stringify(changes));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to save pending changes to localStorage:", error);
|
console.warn("Failed to save pending changes to sessionStorage:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ const loadPendingChangesFromStorage = (
|
|||||||
secretPath: string
|
secretPath: string
|
||||||
): PendingChanges => {
|
): PendingChanges => {
|
||||||
const key = `${STORAGE_KEY}_${generateContextKey(workspaceId, environment, secretPath)}`;
|
const key = `${STORAGE_KEY}_${generateContextKey(workspaceId, environment, secretPath)}`;
|
||||||
const stored = localStorage.getItem(key);
|
const stored = sessionStorage.getItem(key);
|
||||||
if (!stored) return { secrets: [], folders: [] };
|
if (!stored) return { secrets: [], folders: [] };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -144,7 +144,7 @@ const loadPendingChangesFromStorage = (
|
|||||||
folders: parsed.folders || []
|
folders: parsed.folders || []
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to parse pending changes from localStorage:", error);
|
console.warn("Failed to parse pending changes from sessionStorage:", error);
|
||||||
return { secrets: [], folders: [] };
|
return { secrets: [], folders: [] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -156,12 +156,122 @@ const clearPendingChangesFromStorage = (
|
|||||||
) => {
|
) => {
|
||||||
const key = `${STORAGE_KEY}_${generateContextKey(workspaceId, environment, secretPath)}`;
|
const key = `${STORAGE_KEY}_${generateContextKey(workspaceId, environment, secretPath)}`;
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem(key);
|
sessionStorage.removeItem(key);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to clear pending changes from localStorage:", error);
|
console.warn("Failed to clear pending changes from sessionStorage:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeValue = (value: any): string | boolean | undefined => {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const areValuesEqual = (value1: any, value2: any): boolean => {
|
||||||
|
const normalized1 = normalizeValue(value1);
|
||||||
|
const normalized2 = normalizeValue(value2);
|
||||||
|
|
||||||
|
if (normalized1 === undefined && normalized2 === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized1 === normalized2;
|
||||||
|
};
|
||||||
|
|
||||||
|
const areArraysEqual = (arr1: any[] | undefined, arr2: any[] | undefined): boolean => {
|
||||||
|
// Handle undefined/null arrays
|
||||||
|
if (!arr1 && !arr2) return true;
|
||||||
|
if (!arr1 || !arr2) return false;
|
||||||
|
|
||||||
|
// Compare lengths
|
||||||
|
if (arr1.length !== arr2.length) return false;
|
||||||
|
|
||||||
|
// Deep comparison using JSON stringify (for simple objects)
|
||||||
|
return JSON.stringify(arr1.sort()) === JSON.stringify(arr2.sort());
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanupRevertedSecretFields = (update: PendingSecretUpdate): PendingSecretUpdate | null => {
|
||||||
|
const cleaned = { ...update };
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
cleaned.secretValue !== undefined &&
|
||||||
|
!areValuesEqual(cleaned.secretValue, cleaned.originalValue)
|
||||||
|
) {
|
||||||
|
hasChanges = true;
|
||||||
|
} else {
|
||||||
|
cleaned.secretValue = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
cleaned.secretComment !== undefined &&
|
||||||
|
!areValuesEqual(cleaned.secretComment, cleaned.originalComment)
|
||||||
|
) {
|
||||||
|
hasChanges = true;
|
||||||
|
} else {
|
||||||
|
cleaned.secretComment = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
cleaned.skipMultilineEncoding !== undefined &&
|
||||||
|
cleaned.skipMultilineEncoding !== cleaned.originalSkipMultilineEncoding
|
||||||
|
) {
|
||||||
|
hasChanges = true;
|
||||||
|
} else {
|
||||||
|
cleaned.skipMultilineEncoding = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleaned.tags !== undefined && !areArraysEqual(cleaned.tags, cleaned.originalTags)) {
|
||||||
|
hasChanges = true;
|
||||||
|
} else {
|
||||||
|
cleaned.tags = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
cleaned.secretMetadata !== undefined &&
|
||||||
|
!areArraysEqual(cleaned.secretMetadata, cleaned.originalSecretMetadata)
|
||||||
|
) {
|
||||||
|
hasChanges = true;
|
||||||
|
} else {
|
||||||
|
cleaned.secretMetadata = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleaned.newSecretName !== undefined && cleaned.newSecretName !== cleaned.secretKey) {
|
||||||
|
hasChanges = true;
|
||||||
|
} else {
|
||||||
|
cleaned.newSecretName = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no changes remain, return null to indicate this update should be removed
|
||||||
|
return hasChanges ? cleaned : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanupRevertedFolderFields = (update: PendingFolderUpdate): PendingFolderUpdate | null => {
|
||||||
|
const cleaned = { ...update };
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
cleaned.folderName !== undefined &&
|
||||||
|
!areValuesEqual(cleaned.folderName, cleaned.originalFolderName)
|
||||||
|
) {
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
cleaned.description !== undefined &&
|
||||||
|
!areValuesEqual(cleaned.description, cleaned.originalDescription)
|
||||||
|
) {
|
||||||
|
hasChanges = true;
|
||||||
|
} else {
|
||||||
|
cleaned.description = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no changes remain, return null to indicate this update should be removed
|
||||||
|
return hasChanges ? cleaned : null;
|
||||||
|
};
|
||||||
|
|
||||||
type SelectedSecretState = {
|
type SelectedSecretState = {
|
||||||
selectedSecret: Record<string, SecretV3RawSanitized>;
|
selectedSecret: Record<string, SecretV3RawSanitized>;
|
||||||
action: {
|
action: {
|
||||||
@@ -189,38 +299,6 @@ const createSelectedSecretStore: StateCreator<CombinedState, [], [], SelectedSec
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const cleanupRevertedFields = (update: PendingSecretUpdate): PendingSecretUpdate => {
|
|
||||||
const cleaned = { ...update };
|
|
||||||
|
|
||||||
if (cleaned.secretValue === cleaned.originalValue) {
|
|
||||||
cleaned.secretValue = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleaned.secretComment === cleaned.originalComment) {
|
|
||||||
cleaned.secretComment = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleaned.skipMultilineEncoding === cleaned.originalSkipMultilineEncoding) {
|
|
||||||
cleaned.skipMultilineEncoding = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For arrays, compare stringified versions
|
|
||||||
if (JSON.stringify(cleaned.tags) === JSON.stringify(cleaned.originalTags)) {
|
|
||||||
cleaned.tags = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JSON.stringify(cleaned.secretMetadata) === JSON.stringify(cleaned.originalSecretMetadata)) {
|
|
||||||
cleaned.secretMetadata = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the new name is the same as original key, remove it
|
|
||||||
if (cleaned.newSecretName === cleaned.secretKey) {
|
|
||||||
cleaned.newSecretName = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleaned;
|
|
||||||
};
|
|
||||||
|
|
||||||
type BatchModeState = {
|
type BatchModeState = {
|
||||||
isBatchMode: boolean;
|
isBatchMode: boolean;
|
||||||
pendingChanges: PendingChanges;
|
pendingChanges: PendingChanges;
|
||||||
@@ -348,7 +426,7 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
|
|||||||
if (existingUpdateIndex >= 0) {
|
if (existingUpdateIndex >= 0) {
|
||||||
const existingUpdate = secretChanges[existingUpdateIndex] as PendingSecretUpdate;
|
const existingUpdate = secretChanges[existingUpdateIndex] as PendingSecretUpdate;
|
||||||
|
|
||||||
const improvedUpdate: PendingSecretUpdate = {
|
const mergedUpdate: PendingSecretUpdate = {
|
||||||
...existingUpdate,
|
...existingUpdate,
|
||||||
secretKey: existingUpdate.secretKey,
|
secretKey: existingUpdate.secretKey,
|
||||||
originalValue: existingUpdate.originalValue,
|
originalValue: existingUpdate.originalValue,
|
||||||
@@ -382,11 +460,23 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
|
|||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanedUpdate = cleanupRevertedFields(improvedUpdate);
|
// Clean up reverted fields and check if any changes remain
|
||||||
|
const cleanedUpdate = cleanupRevertedSecretFields(mergedUpdate);
|
||||||
|
|
||||||
secretChanges[existingUpdateIndex] = cleanedUpdate;
|
if (cleanedUpdate) {
|
||||||
|
// Still has changes, keep the update
|
||||||
|
secretChanges[existingUpdateIndex] = cleanedUpdate;
|
||||||
|
} else {
|
||||||
|
// No changes remain, remove the pending update
|
||||||
|
secretChanges.splice(existingUpdateIndex, 1);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
secretChanges.push(change);
|
// New update - clean it up before adding
|
||||||
|
const cleanedUpdate = cleanupRevertedSecretFields(change);
|
||||||
|
if (cleanedUpdate) {
|
||||||
|
secretChanges.push(cleanedUpdate);
|
||||||
|
}
|
||||||
|
// If cleanedUpdate is null, don't add it (no actual changes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -435,7 +525,7 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
|
|||||||
if (existingUpdateIndex >= 0) {
|
if (existingUpdateIndex >= 0) {
|
||||||
const existingUpdate = folderChanges[existingUpdateIndex] as PendingFolderUpdate;
|
const existingUpdate = folderChanges[existingUpdateIndex] as PendingFolderUpdate;
|
||||||
|
|
||||||
folderChanges[existingUpdateIndex] = {
|
const mergedUpdate: PendingFolderUpdate = {
|
||||||
...existingUpdate,
|
...existingUpdate,
|
||||||
originalFolderName: existingUpdate.originalFolderName,
|
originalFolderName: existingUpdate.originalFolderName,
|
||||||
originalDescription: existingUpdate.originalDescription,
|
originalDescription: existingUpdate.originalDescription,
|
||||||
@@ -449,8 +539,24 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
|
|||||||
|
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Clean up reverted fields and check if any changes remain
|
||||||
|
const cleanedUpdate = cleanupRevertedFolderFields(mergedUpdate);
|
||||||
|
|
||||||
|
if (cleanedUpdate) {
|
||||||
|
// Still has changes, keep the update
|
||||||
|
folderChanges[existingUpdateIndex] = cleanedUpdate;
|
||||||
|
} else {
|
||||||
|
// No changes remain, remove the pending update
|
||||||
|
folderChanges.splice(existingUpdateIndex, 1);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
folderChanges.push(change);
|
// New update - clean it up before adding
|
||||||
|
const cleanedUpdate = cleanupRevertedFolderFields(change);
|
||||||
|
if (cleanedUpdate) {
|
||||||
|
folderChanges.push(cleanedUpdate);
|
||||||
|
}
|
||||||
|
// If cleanedUpdate is null, don't add it (no actual changes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+26
-25
@@ -1,17 +1,9 @@
|
|||||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { faCodeCommit, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
|
import { faCodeCommit, faEye, faFolder, faKey, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import {
|
import { Badge, Button, IconButton, Input, Modal, ModalContent, Tooltip } from "@app/components/v2";
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
FontAwesomeSymbol,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalContent
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -20,7 +12,6 @@ import {
|
|||||||
useBatchMode,
|
useBatchMode,
|
||||||
useBatchModeActions
|
useBatchModeActions
|
||||||
} from "../../SecretMainPage.store";
|
} from "../../SecretMainPage.store";
|
||||||
import { FontAwesomeSpriteName } from "../SecretListView/SecretListView.utils";
|
|
||||||
|
|
||||||
interface CommitFormProps {
|
interface CommitFormProps {
|
||||||
onCommit: (changes: PendingChanges, commitMessage: string) => Promise<void>;
|
onCommit: (changes: PendingChanges, commitMessage: string) => Promise<void>;
|
||||||
@@ -384,15 +375,17 @@ const ChangeTable: React.FC<ChangeTableProps> = ({
|
|||||||
<span className="font-medium text-mineshaft-100">{getChangeName()}</span>
|
<span className="font-medium text-mineshaft-100">{getChangeName()}</span>
|
||||||
{getChangeBadge(change.type)}
|
{getChangeBadge(change.type)}
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
<Tooltip content="Discard change">
|
||||||
ariaLabel="delete-change"
|
<IconButton
|
||||||
variant="plain"
|
ariaLabel="delete-change"
|
||||||
colorSchema="danger"
|
variant="plain"
|
||||||
size="sm"
|
colorSchema="danger"
|
||||||
onClick={() => handleDeletePending(change.resourceType, change.id)}
|
size="sm"
|
||||||
>
|
onClick={() => handleDeletePending(change.resourceType, change.id)}
|
||||||
<FontAwesomeSymbol symbolName={FontAwesomeSpriteName.Close} className="h-4 w-4" />
|
>
|
||||||
</IconButton>
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{change.resourceType === "secret" ? renderSecretChanges() : renderFolderChanges()}
|
{change.resourceType === "secret" ? renderSecretChanges() : renderFolderChanges()}
|
||||||
</div>
|
</div>
|
||||||
@@ -434,7 +427,7 @@ export const CommitForm: React.FC<CommitFormProps> = ({
|
|||||||
<>
|
<>
|
||||||
{/* Floating Panel */}
|
{/* Floating Panel */}
|
||||||
{!isModalOpen && (
|
{!isModalOpen && (
|
||||||
<div className="fixed bottom-4 z-40 w-80 self-center rounded-lg border border-mineshaft-600 bg-mineshaft-800 shadow-2xl">
|
<div className="fixed bottom-4 z-40 w-64 self-center rounded-lg border border-mineshaft-600 bg-mineshaft-800 shadow-2xl">
|
||||||
<div className="flex w-full justify-center border-b border-mineshaft-600 px-4 py-3">
|
<div className="flex w-full justify-center border-b border-mineshaft-600 px-4 py-3">
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -447,14 +440,22 @@ export const CommitForm: React.FC<CommitFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-3">
|
<div className="flex justify-center gap-4 p-3">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setIsModalOpen(true)}
|
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
className="w-full"
|
onClick={() => clearAllPendingChanges({ workspaceId, environment, secretPath })}
|
||||||
isDisabled={totalChangesCount === 0}
|
isDisabled={totalChangesCount === 0}
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
>
|
>
|
||||||
Review & Commit
|
Discard
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faEye} />}
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
isDisabled={totalChangesCount === 0}
|
||||||
|
colorSchema="secondary"
|
||||||
|
>
|
||||||
|
Review
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+95
-10
@@ -38,7 +38,7 @@ import { WsTag } from "@app/hooks/api/types";
|
|||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { memo, useEffect } from "react";
|
import { memo, useEffect, useRef, useCallback } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import {
|
import {
|
||||||
@@ -118,6 +118,9 @@ export const SecretItem = memo(
|
|||||||
const { isRotatedSecret } = secret;
|
const { isRotatedSecret } = secret;
|
||||||
const { removePendingChange } = useBatchModeActions();
|
const { removePendingChange } = useBatchModeActions();
|
||||||
|
|
||||||
|
const autoSaveTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
const isAutoSavingRef = useRef(false);
|
||||||
|
|
||||||
const handleDeletePending = (pendingSecret: SecretV3RawSanitized) => {
|
const handleDeletePending = (pendingSecret: SecretV3RawSanitized) => {
|
||||||
removePendingChange(pendingSecret.id, "secret", {
|
removePendingChange(pendingSecret.id, "secret", {
|
||||||
workspaceId: currentWorkspace.id,
|
workspaceId: currentWorkspace.id,
|
||||||
@@ -166,7 +169,6 @@ export const SecretItem = memo(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const secretName = watch("key");
|
const secretName = watch("key");
|
||||||
|
|
||||||
const overrideAction = watch("overrideAction");
|
const overrideAction = watch("overrideAction");
|
||||||
const hasComment = Boolean(watch("comment"));
|
const hasComment = Boolean(watch("comment"));
|
||||||
|
|
||||||
@@ -182,6 +184,68 @@ export const SecretItem = memo(
|
|||||||
name: "tags"
|
name: "tags"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isOverriden =
|
||||||
|
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||||
|
const hasTagsApplied = Boolean(fields.length);
|
||||||
|
|
||||||
|
const autoSaveChanges = useCallback(
|
||||||
|
async (data: TFormSchema) => {
|
||||||
|
if (isAutoSavingRef.current) return;
|
||||||
|
if (
|
||||||
|
data.overrideAction === SecretActionType.Created ||
|
||||||
|
data.overrideAction === SecretActionType.Modified
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAutoSavingRef.current = true;
|
||||||
|
try {
|
||||||
|
const hasDirectReferences = importedBy?.some(({ folders }) =>
|
||||||
|
folders?.some(({ secrets }) =>
|
||||||
|
secrets?.some(({ referencedSecretKey }) => referencedSecretKey === secret.key)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasDirectReferences) {
|
||||||
|
await onSaveSecret(secret, { ...secret, ...data }, () => {
|
||||||
|
reset();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await onSaveSecret(secret, { ...secret, ...data }, () => {
|
||||||
|
reset();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Auto-save failed:", error);
|
||||||
|
} finally {
|
||||||
|
isAutoSavingRef.current = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[secret, onSaveSecret, importedBy, reset]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formValues = watch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoSaveTimeoutRef.current) {
|
||||||
|
clearTimeout(autoSaveTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDirty && !isSubmitting && !isAutoSavingRef.current) {
|
||||||
|
const debounceTime = isPending ? 500 : 1500;
|
||||||
|
|
||||||
|
autoSaveTimeoutRef.current = setTimeout(() => {
|
||||||
|
autoSaveChanges(formValues);
|
||||||
|
}, debounceTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (autoSaveTimeoutRef.current) {
|
||||||
|
clearTimeout(autoSaveTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [formValues, isDirty, isSubmitting, autoSaveChanges, isPending]);
|
||||||
|
|
||||||
const isReadOnly =
|
const isReadOnly =
|
||||||
hasSecretReadValueOrDescribePermission(
|
hasSecretReadValueOrDescribePermission(
|
||||||
permission,
|
permission,
|
||||||
@@ -202,6 +266,7 @@ export const SecretItem = memo(
|
|||||||
secretTags: selectedTagSlugs
|
secretTags: selectedTagSlugs
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const isReadOnlySecret =
|
const isReadOnlySecret =
|
||||||
isReadOnly || isRotatedSecret || (isPending && pendingAction !== PendingAction.Update);
|
isReadOnly || isRotatedSecret || (isPending && pendingAction !== PendingAction.Update);
|
||||||
|
|
||||||
@@ -216,10 +281,6 @@ export const SecretItem = memo(
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [isSecValueCopied]);
|
}, [isSecValueCopied]);
|
||||||
|
|
||||||
const isOverriden =
|
|
||||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
|
||||||
const hasTagsApplied = Boolean(fields.length);
|
|
||||||
|
|
||||||
const handleOverrideClick = () => {
|
const handleOverrideClick = () => {
|
||||||
if (isOverriden) {
|
if (isOverriden) {
|
||||||
// override need not be flagged delete if it was never saved in server
|
// override need not be flagged delete if it was never saved in server
|
||||||
@@ -284,6 +345,8 @@ export const SecretItem = memo(
|
|||||||
setIsSecValueCopied.on();
|
setIsSecValueCopied.on();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isInAutoSaveMode = isDirty && !isSubmitting && !isOverriden;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
<div
|
<div
|
||||||
@@ -292,6 +355,7 @@ export const SecretItem = memo(
|
|||||||
isDirty && "border-primary-400/50",
|
isDirty && "border-primary-400/50",
|
||||||
isRotatedSecret && "bg-mineshaft-700/60",
|
isRotatedSecret && "bg-mineshaft-700/60",
|
||||||
isPending && "bg-mineshaft-700/60",
|
isPending && "bg-mineshaft-700/60",
|
||||||
|
isInAutoSaveMode && "border-primary-400/75 bg-primary-900/20",
|
||||||
pendingAction === PendingAction.Delete && "border-l-2 border-l-red-600/75",
|
pendingAction === PendingAction.Delete && "border-l-2 border-l-red-600/75",
|
||||||
pendingAction === PendingAction.Update && "border-l-2 border-l-yellow-600/75",
|
pendingAction === PendingAction.Update && "border-l-2 border-l-yellow-600/75",
|
||||||
pendingAction === PendingAction.Create && "border-l-2 border-l-green-600/75"
|
pendingAction === PendingAction.Create && "border-l-2 border-l-green-600/75"
|
||||||
@@ -357,6 +421,14 @@ export const SecretItem = memo(
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
|
{isInAutoSaveMode && (
|
||||||
|
<Tooltip content="Auto-saving changes">
|
||||||
|
<div className="mr-2 flex items-center">
|
||||||
|
<Spinner className="h-3 w-3 text-primary" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
{secretValueHidden && !isOverriden && (
|
{secretValueHidden && !isOverriden && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
|
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
|
||||||
@@ -399,7 +471,6 @@ export const SecretItem = memo(
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Only allow to open the side panel if the secret is not in a pending create or delete state */}
|
|
||||||
{pendingAction !== PendingAction.Create && pendingAction !== PendingAction.Delete && (
|
{pendingAction !== PendingAction.Create && pendingAction !== PendingAction.Delete && (
|
||||||
<div
|
<div
|
||||||
key="actions"
|
key="actions"
|
||||||
@@ -460,7 +531,7 @@ export const SecretItem = memo(
|
|||||||
<ModalContent
|
<ModalContent
|
||||||
title="Secret Reference Details"
|
title="Secret Reference Details"
|
||||||
subTitle="Visual breakdown of secrets referenced by this secret."
|
subTitle="Visual breakdown of secrets referenced by this secret."
|
||||||
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<SecretReferenceTree
|
<SecretReferenceTree
|
||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
@@ -647,7 +718,21 @@ export const SecretItem = memo(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{!isDirty ? (
|
{isInAutoSaveMode ? (
|
||||||
|
<motion.div
|
||||||
|
key="auto-save-mode"
|
||||||
|
className="flex h-10 flex-shrink-0 items-center space-x-4 px-[0.64rem]"
|
||||||
|
initial={{ x: -10, opacity: 0 }}
|
||||||
|
animate={{ x: 0, opacity: 1 }}
|
||||||
|
exit={{ x: -10, opacity: 0 }}
|
||||||
|
>
|
||||||
|
<Tooltip content="Auto-saving changes...">
|
||||||
|
<div className="flex items-center text-primary">
|
||||||
|
<Spinner className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</motion.div>
|
||||||
|
) : !isDirty ? (
|
||||||
isPending ? (
|
isPending ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="options"
|
key="options"
|
||||||
@@ -717,7 +802,7 @@ export const SecretItem = memo(
|
|||||||
secretTags: selectedTagSlugs
|
secretTags: selectedTagSlugs
|
||||||
})}
|
})}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel={isRotatedSecret ? "Cannot Delete Rotated Secret" : "Delete"} // just using label for isRotatedSecret, disabled below
|
allowedLabel={isRotatedSecret ? "Cannot Delete Rotated Secret" : "Delete"}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
+7
-2
@@ -16,6 +16,7 @@ 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";
|
||||||
import { secretSnapshotKeys } from "@app/hooks/api/secretSnapshots/queries";
|
import { secretSnapshotKeys } from "@app/hooks/api/secretSnapshots/queries";
|
||||||
import { WsTag } from "@app/hooks/api/types";
|
import { WsTag } from "@app/hooks/api/types";
|
||||||
|
import { useNavigationBlocker } from "@app/hooks/useNavigationBlocker";
|
||||||
import { AddShareSecretModal } from "@app/pages/organization/SecretSharingPage/components/ShareSecret/AddShareSecretModal";
|
import { AddShareSecretModal } from "@app/pages/organization/SecretSharingPage/components/ShareSecret/AddShareSecretModal";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -85,6 +86,10 @@ export const SecretListView = ({
|
|||||||
const selectedSecrets = useSelectedSecrets();
|
const selectedSecrets = useSelectedSecrets();
|
||||||
const { toggle: toggleSelectedSecret } = useSelectedSecretActions();
|
const { toggle: toggleSelectedSecret } = useSelectedSecretActions();
|
||||||
const { isBatchMode, pendingChanges } = useBatchMode();
|
const { isBatchMode, pendingChanges } = useBatchMode();
|
||||||
|
useNavigationBlocker(
|
||||||
|
pendingChanges.secrets.length > 0 || pendingChanges.folders.length > 0,
|
||||||
|
"You have unsaved work. Are you sure you want to leave?"
|
||||||
|
);
|
||||||
const { addPendingChange } = useBatchModeActions();
|
const { addPendingChange } = useBatchModeActions();
|
||||||
|
|
||||||
const handleSecretOperation = async (
|
const handleSecretOperation = async (
|
||||||
@@ -226,7 +231,7 @@ export const SecretListView = ({
|
|||||||
async (
|
async (
|
||||||
orgSecret: SecretV3RawSanitized,
|
orgSecret: SecretV3RawSanitized,
|
||||||
modSecret: Omit<SecretV3RawSanitized, "tags"> & {
|
modSecret: Omit<SecretV3RawSanitized, "tags"> & {
|
||||||
tags?: { id: string; name?: string }[];
|
tags?: { id: string; name?: string; slug?: string }[];
|
||||||
secretMetadata?: { key: string; value: string }[];
|
secretMetadata?: { key: string; value: string }[];
|
||||||
},
|
},
|
||||||
cb?: () => void
|
cb?: () => void
|
||||||
@@ -347,7 +352,7 @@ export const SecretListView = ({
|
|||||||
...(!isSameTags && {
|
...(!isSameTags && {
|
||||||
originalTags:
|
originalTags:
|
||||||
trueOriginalSecret.tags?.map((tag) => ({ id: tag.id, slug: tag.slug })) || [],
|
trueOriginalSecret.tags?.map((tag) => ({ id: tag.id, slug: tag.slug })) || [],
|
||||||
tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || "" })) || []
|
tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || tag.slug || "" })) || []
|
||||||
}),
|
}),
|
||||||
...(JSON.stringify(secretMetadata) !==
|
...(JSON.stringify(secretMetadata) !==
|
||||||
JSON.stringify(trueOriginalSecret.secretMetadata) && {
|
JSON.stringify(trueOriginalSecret.secretMetadata) && {
|
||||||
|
|||||||
Reference in New Issue
Block a user