Removed session storage logic and added a few UI improvements

This commit is contained in:
Carlos Monastyrski
2025-07-18 22:13:43 -03:00
parent f025509938
commit 80743997e1
6 changed files with 211 additions and 176 deletions
+17 -41
View File
@@ -440,46 +440,6 @@ export const useCreateCommit = () => {
} }
>({ >({
mutationFn: async ({ workspaceId, environment, secretPath, pendingChanges, message }) => { mutationFn: async ({ workspaceId, environment, secretPath, pendingChanges, message }) => {
const transformedSecretUpdates = pendingChanges.secrets
.filter((change) => change.type === PendingAction.Update)
.map((change: PendingSecretUpdate) => {
const updatePayload: {
secretKey: string;
newSecretName?: string;
secretValue?: string;
secretComment?: string;
skipMultilineEncoding?: boolean;
tagIds?: string[];
secretMetadata?: {
key: string;
value: string;
}[];
} = {
secretKey: change.secretKey
};
// Only include fields that actually changed
if (change.newSecretName) {
updatePayload.newSecretName = change.newSecretName;
}
if (change.secretValue !== undefined) {
updatePayload.secretValue = change.secretValue;
}
if (change.secretComment !== undefined) {
updatePayload.secretComment = change.secretComment;
}
if (change.skipMultilineEncoding !== undefined) {
updatePayload.skipMultilineEncoding = change.skipMultilineEncoding;
}
if (change.tags) {
updatePayload.tagIds = change.tags.map((tag) => tag.id);
}
if (change.secretMetadata) {
updatePayload.secretMetadata = change.secretMetadata;
}
return updatePayload;
});
const { data } = await apiRequest.post("/api/v1/pit/batch/commit", { const { data } = await apiRequest.post("/api/v1/pit/batch/commit", {
projectId: workspaceId, projectId: workspaceId,
environment, environment,
@@ -497,7 +457,23 @@ export const useCreateCommit = () => {
tagIds: change.tags?.map((tag) => tag.id), tagIds: change.tags?.map((tag) => tag.id),
secretMetadata: change.secretMetadata secretMetadata: change.secretMetadata
})) || [], })) || [],
update: transformedSecretUpdates || [], update:
pendingChanges.secrets
.filter((change) => change.type === PendingAction.Update)
.map((change: PendingSecretUpdate) => ({
secretKey: change.secretKey,
newSecretName: change.newSecretName,
secretValue: change.secretValue || change.existingSecret.value,
secretComment: change.secretComment || change.existingSecret.comment,
skipMultilineEncoding:
change.skipMultilineEncoding !== undefined
? change.skipMultilineEncoding
: change.existingSecret.skipMultilineEncoding,
tagIds:
change.tags?.map((tag) => tag.id) ||
change.existingSecret.tags?.map((tag) => tag.id),
secretMetadata: change.secretMetadata || change.existingSecret.secretMetadata
})) || [],
delete: delete:
pendingChanges.secrets.filter((change) => change.type === PendingAction.Delete) || [] pendingChanges.secrets.filter((change) => change.type === PendingAction.Delete) || []
}, },
@@ -417,7 +417,9 @@ const Page = () => {
imports?.length || imports?.length ||
dynamicSecrets?.length || dynamicSecrets?.length ||
secretRotations?.length || secretRotations?.length ||
noAccessSecretCount noAccessSecretCount ||
pendingChanges.secrets.length ||
pendingChanges.folders.length
); );
useEffect(() => { useEffect(() => {
@@ -604,8 +606,8 @@ const Page = () => {
secretMetadata: change.secretMetadata || mergedSecrets[updateIndex].secretMetadata, secretMetadata: change.secretMetadata || mergedSecrets[updateIndex].secretMetadata,
isPending: true, isPending: true,
pendingAction: PendingAction.Update, pendingAction: PendingAction.Update,
tags: tags: change.tags
change.tags?.map((tag) => ({ ? change.tags?.map((tag) => ({
id: tag.id, id: tag.id,
slug: tag.slug, slug: tag.slug,
projectId: workspaceId, projectId: workspaceId,
@@ -613,6 +615,7 @@ const Page = () => {
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
__v: 0 __v: 0
})) || [] })) || []
: mergedSecrets[updateIndex].tags
}; };
} }
break; break;
@@ -935,19 +938,28 @@ const Page = () => {
folders?.length === 0 && <PermissionDeniedBanner />} folders?.length === 0 && <PermissionDeniedBanner />}
</div> </div>
</div> </div>
{!isDetailsLoading && totalCount > 0 && ( {!isDetailsLoading &&
(totalCount > 0 ||
pendingChanges.secrets.length > 0 ||
pendingChanges.folders.length > 0) && (
<Pagination <Pagination
startAdornment={ startAdornment={
<SecretTableResourceCount <SecretTableResourceCount
dynamicSecretCount={totalDynamicSecretCount} dynamicSecretCount={totalDynamicSecretCount}
importCount={totalImportCount} importCount={totalImportCount}
secretCount={totalSecretCount} secretCount={
folderCount={totalFolderCount} totalSecretCount +
pendingChanges.secrets.filter((s) => s.type === PendingAction.Create).length
}
folderCount={
totalFolderCount +
pendingChanges.folders.filter((f) => f.type === PendingAction.Create).length
}
secretRotationCount={totalSecretRotationCount} secretRotationCount={totalSecretRotationCount}
/> />
} }
className="rounded-b-md border-t border-solid border-t-mineshaft-600" className="rounded-b-md border-t border-solid border-t-mineshaft-600"
count={totalCount} count={totalCount + pendingChanges.secrets.length + pendingChanges.folders.length}
page={page} page={page}
perPage={perPage} perPage={perPage}
onChangePage={(newPage) => setPage(newPage)} onChangePage={(newPage) => setPage(newPage)}
@@ -4,6 +4,7 @@ import { useRouter } from "@tanstack/react-router";
import { createStore, StateCreator, StoreApi, useStore } from "zustand"; import { createStore, StateCreator, StoreApi, useStore } from "zustand";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { createNotification } from "@app/components/notifications";
import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { PendingAction } from "@app/hooks/api/secretFolders/types";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
@@ -45,6 +46,7 @@ export interface PendingSecretUpdate extends BasePendingChange {
tags?: { id: string; slug: string }[]; tags?: { id: string; slug: string }[];
originalSecretMetadata?: { key: string; value: string }[]; originalSecretMetadata?: { key: string; value: string }[];
secretMetadata?: { key: string; value: string }[]; secretMetadata?: { key: string; value: string }[];
existingSecret: SecretV3RawSanitized;
} }
export interface PendingSecretDelete extends BasePendingChange { export interface PendingSecretDelete extends BasePendingChange {
@@ -100,68 +102,6 @@ export interface BatchContext {
secretPath: string; secretPath: string;
} }
const STORAGE_KEY = "infisical_pending_changes";
const generateContextKey = (workspaceId: string, environment: string, secretPath: string) => {
return `${workspaceId}_${environment}_${secretPath}`;
};
const savePendingChangesToStorage = (
changes: PendingChanges,
workspaceId: string,
environment: string,
secretPath: string
) => {
const key = `${STORAGE_KEY}_${generateContextKey(workspaceId, environment, secretPath)}`;
try {
sessionStorage.setItem(key, JSON.stringify(changes));
} catch (error) {
console.warn("Failed to save pending changes to sessionStorage:", error);
}
};
const loadPendingChangesFromStorage = (
workspaceId: string,
environment: string,
secretPath: string
): PendingChanges => {
const key = `${STORAGE_KEY}_${generateContextKey(workspaceId, environment, secretPath)}`;
const stored = sessionStorage.getItem(key);
if (!stored) return { secrets: [], folders: [] };
try {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed)) {
return {
secrets: parsed.filter(
(change: any) => !change.resourceType || change.resourceType === "secret"
),
folders: []
};
}
return {
secrets: parsed.secrets || [],
folders: parsed.folders || []
};
} catch (error) {
console.warn("Failed to parse pending changes from sessionStorage:", error);
return { secrets: [], folders: [] };
}
};
const clearPendingChangesFromStorage = (
workspaceId: string,
environment: string,
secretPath: string
) => {
const key = `${STORAGE_KEY}_${generateContextKey(workspaceId, environment, secretPath)}`;
try {
sessionStorage.removeItem(key);
} catch (error) {
console.warn("Failed to clear pending changes from sessionStorage:", error);
}
};
const normalizeValue = (value: any): string | boolean | undefined => { const normalizeValue = (value: any): string | boolean | undefined => {
if (value === null || value === undefined || value === "") { if (value === null || value === undefined || value === "") {
return undefined; return undefined;
@@ -207,7 +147,8 @@ const cleanupRevertedSecretFields = (update: PendingSecretUpdate): PendingSecret
if ( if (
cleaned.secretComment !== undefined && cleaned.secretComment !== undefined &&
!areValuesEqual(cleaned.secretComment, cleaned.originalComment) (!areValuesEqual(cleaned.secretComment, cleaned.originalComment) ||
!areValuesEqual(cleaned.secretComment, cleaned.existingSecret.comment))
) { ) {
hasChanges = true; hasChanges = true;
} else { } else {
@@ -302,6 +243,7 @@ const createSelectedSecretStore: StateCreator<CombinedState, [], [], SelectedSec
type BatchModeState = { type BatchModeState = {
isBatchMode: boolean; isBatchMode: boolean;
pendingChanges: PendingChanges; pendingChanges: PendingChanges;
pendingChangesByContext: Map<string, PendingChanges>;
existingSecretKeys: Set<string>; existingSecretKeys: Set<string>;
existingFolderNames: Set<string>; existingFolderNames: Set<string>;
currentContext: BatchContext | null; currentContext: BatchContext | null;
@@ -315,16 +257,32 @@ type BatchModeState = {
}; };
}; };
const generateContextKey = (workspaceId: string, environment: string, secretPath: string) => {
return `${workspaceId}_${environment}_${secretPath}`;
};
const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState> = (set, get) => ({ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState> = (set, get) => ({
isBatchMode: true, // Always enabled by default isBatchMode: true, // Always enabled by default
pendingChanges: { secrets: [], folders: [] }, pendingChanges: { secrets: [], folders: [] },
pendingChangesByContext: new Map<string, PendingChanges>(),
currentContext: null, currentContext: null,
existingSecretKeys: new Set<string>(), existingSecretKeys: new Set<string>(),
existingFolderNames: new Set<string>(), existingFolderNames: new Set<string>(),
batchActions: { batchActions: {
addPendingChange: (change: PendingChange, context: BatchContext) => addPendingChange: (change: PendingChange, context: BatchContext) =>
set((state) => { set((state) => {
const newChanges = { ...state.pendingChanges }; const contextKey = generateContextKey(
context.workspaceId,
context.environment,
context.secretPath
);
// Get existing changes for this context or create new empty state
const existingChanges = state.pendingChangesByContext.get(contextKey) || {
secrets: [],
folders: []
};
const newChanges = { ...existingChanges };
if (change.resourceType === "folder") { if (change.resourceType === "folder") {
const existingFolder = const existingFolder =
@@ -332,6 +290,10 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
newChanges.folders.some((f) => f.folderName === change.folderName); newChanges.folders.some((f) => f.folderName === change.folderName);
if (change.type === PendingAction.Create && existingFolder) { if (change.type === PendingAction.Create && existingFolder) {
createNotification({
text: "Another folder with same name already exists",
type: "error"
});
return { pendingChanges: newChanges }; return { pendingChanges: newChanges };
} }
if ( if (
@@ -339,6 +301,10 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
change.folderName !== change.originalFolderName && change.folderName !== change.originalFolderName &&
existingFolder existingFolder
) { ) {
createNotification({
text: "Another folder with same name already exists",
type: "error"
});
return { pendingChanges: newChanges }; return { pendingChanges: newChanges };
} }
} }
@@ -346,9 +312,19 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
if (change.resourceType === "secret") { if (change.resourceType === "secret") {
const existingSecret = const existingSecret =
state.existingSecretKeys.has(change.secretKey) || state.existingSecretKeys.has(change.secretKey) ||
newChanges.secrets.some((s) => (s.secretKey === change.secretKey && s.type !== PendingAction.Create) || (change.type === PendingAction.Create && change.originalKey !== change.secretKey && s.secretKey === change.secretKey)); newChanges.secrets.some(
(s) =>
(s.secretKey === change.secretKey && s.type !== PendingAction.Create) ||
(change.type === PendingAction.Create &&
change.originalKey !== change.secretKey &&
s.secretKey === change.secretKey)
);
if (change.type === PendingAction.Create && existingSecret) { if (change.type === PendingAction.Create && existingSecret) {
createNotification({
text: "Another secret with same name already exists",
type: "error"
});
return { pendingChanges: newChanges }; return { pendingChanges: newChanges };
} }
@@ -366,6 +342,10 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
)); ));
if (existingNewSecretName) { if (existingNewSecretName) {
createNotification({
text: "Another secret with same name already exists",
type: "error"
});
return { pendingChanges: newChanges }; return { pendingChanges: newChanges };
} }
} }
@@ -425,7 +405,6 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
if (existingUpdateIndex >= 0) { if (existingUpdateIndex >= 0) {
const existingUpdate = secretChanges[existingUpdateIndex] as PendingSecretUpdate; const existingUpdate = secretChanges[existingUpdateIndex] as PendingSecretUpdate;
const mergedUpdate: PendingSecretUpdate = { const mergedUpdate: PendingSecretUpdate = {
...existingUpdate, ...existingUpdate,
secretKey: existingUpdate.secretKey, secretKey: existingUpdate.secretKey,
@@ -456,7 +435,7 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
change.secretMetadata !== undefined change.secretMetadata !== undefined
? change.secretMetadata ? change.secretMetadata
: existingUpdate.secretMetadata, : existingUpdate.secretMetadata,
existingSecret: existingUpdate.existingSecret,
timestamp: Date.now() timestamp: Date.now()
}; };
@@ -566,18 +545,40 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
newChanges.folders = folderChanges; newChanges.folders = folderChanges;
} }
savePendingChangesToStorage( const updatedContextMap = new Map(state.pendingChangesByContext);
newChanges, updatedContextMap.set(contextKey, newChanges);
context.workspaceId,
context.environment, const currentChanges =
context.secretPath contextKey ===
); generateContextKey(
return { pendingChanges: newChanges }; state.currentContext?.workspaceId || context.workspaceId,
state.currentContext?.environment || context.environment,
state.currentContext?.secretPath || context.secretPath
)
? newChanges
: state.pendingChanges;
return {
pendingChangesByContext: updatedContextMap,
pendingChanges: currentChanges,
currentContext: context
};
}), }),
removePendingChange: (changeId: string, resourceType: string, context: BatchContext) => removePendingChange: (changeId: string, resourceType: string, context: BatchContext) =>
set((state) => { set((state) => {
const newChanges = { ...state.pendingChanges }; const contextKey = generateContextKey(
context.workspaceId,
context.environment,
context.secretPath
);
// Get existing changes for this context
const existingChanges = state.pendingChangesByContext.get(contextKey) || {
secrets: [],
folders: []
};
const newChanges = { ...existingChanges };
if (resourceType === "secret") { if (resourceType === "secret") {
newChanges.secrets = newChanges.secrets.filter((c) => c.id !== changeId); newChanges.secrets = newChanges.secrets.filter((c) => c.id !== changeId);
@@ -585,28 +586,70 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
newChanges.folders = newChanges.folders.filter((c) => c.id !== changeId); newChanges.folders = newChanges.folders.filter((c) => c.id !== changeId);
} }
savePendingChangesToStorage( const updatedContextMap = new Map(state.pendingChangesByContext);
newChanges, updatedContextMap.set(contextKey, newChanges);
context.workspaceId,
context.environment, const isCurrentContext =
context.secretPath state.currentContext &&
contextKey ===
generateContextKey(
state.currentContext.workspaceId,
state.currentContext.environment,
state.currentContext.secretPath
); );
return { pendingChanges: newChanges };
return {
pendingChangesByContext: updatedContextMap,
pendingChanges: isCurrentContext ? newChanges : state.pendingChanges
};
}), }),
loadPendingChanges: (context) => { loadPendingChanges: (context) => {
const changes = loadPendingChangesFromStorage( const contextKey = generateContextKey(
context.workspaceId, context.workspaceId,
context.environment, context.environment,
context.secretPath context.secretPath
); );
set({ pendingChanges: changes });
set((state) => {
const contextChanges = state.pendingChangesByContext.get(contextKey) || {
secrets: [],
folders: []
};
return {
pendingChanges: contextChanges,
currentContext: context
};
});
}, },
clearAllPendingChanges: (context) => { clearAllPendingChanges: (context) => {
clearPendingChangesFromStorage(context.workspaceId, context.environment, context.secretPath); const contextKey = generateContextKey(
set({ context.workspaceId,
pendingChanges: { secrets: [], folders: [] } context.environment,
context.secretPath
);
set((state) => {
// Clear changes for this specific context
const updatedContextMap = new Map(state.pendingChangesByContext);
updatedContextMap.delete(contextKey);
// If this is the current context, also clear the active pending changes
const isCurrentContext =
state.currentContext &&
contextKey ===
generateContextKey(
state.currentContext.workspaceId,
state.currentContext.environment,
state.currentContext.secretPath
);
return {
pendingChangesByContext: updatedContextMap,
pendingChanges: isCurrentContext ? { secrets: [], folders: [] } : state.pendingChanges
};
}); });
}, },
@@ -191,7 +191,7 @@ const ChangeTable: React.FC<ChangeTableProps> = ({
{hasKeyChange && ( {hasKeyChange && (
<ComparisonTableRow <ComparisonTableRow
label="Key" label="Key"
previousValue={<span className="font-mono">{change.secretKey}</span>} previousValue={<span className="font-mono">{change.existingSecret.key}</span>}
newValue={<span className="font-mono">{change.newSecretName}</span>} newValue={<span className="font-mono">{change.newSecretName}</span>}
/> />
)} )}
@@ -200,7 +200,7 @@ const ChangeTable: React.FC<ChangeTableProps> = ({
label="Value" label="Value"
previousValue={ previousValue={
<div className="max-w-md break-all rounded"> <div className="max-w-md break-all rounded">
{change.originalValue || <span className="italic">(empty)</span>} {change.existingSecret.value || <span className="italic">(empty)</span>}
</div> </div>
} }
newValue={ newValue={
@@ -213,28 +213,32 @@ const ChangeTable: React.FC<ChangeTableProps> = ({
{hasCommentChange && ( {hasCommentChange && (
<ComparisonTableRow <ComparisonTableRow
label="Comment" label="Comment"
previousValue={change.originalComment || <span className="italic">(empty)</span>} previousValue={
change.existingSecret.comment || <span className="italic">(empty)</span>
}
newValue={change.secretComment || <span className="italic">(empty)</span>} newValue={change.secretComment || <span className="italic">(empty)</span>}
/> />
)} )}
{hasMultilineChange && ( {hasMultilineChange && (
<ComparisonTableRow <ComparisonTableRow
label="Multiline" label="Multi-line Encoding"
previousValue={change.originalSkipMultilineEncoding ? "Enabled" : "Disabled"} previousValue={
change.existingSecret.skipMultilineEncoding ? "Enabled" : "Disabled"
}
newValue={change.skipMultilineEncoding ? "Enabled" : "Disabled"} newValue={change.skipMultilineEncoding ? "Enabled" : "Disabled"}
/> />
)} )}
{hasTagsChange && ( {hasTagsChange && (
<ComparisonTableRow <ComparisonTableRow
label="Tags" label="Tags"
previousValue={<TagsList tags={change.originalTags} />} previousValue={<TagsList tags={change.existingSecret.tags} />}
newValue={<TagsList tags={change.tags} />} newValue={<TagsList tags={change.tags} />}
/> />
)} )}
{hasMetadataChange && ( {hasMetadataChange && (
<ComparisonTableRow <ComparisonTableRow
label="Metadata" label="Metadata"
previousValue={<MetadataList metadata={change.originalSecretMetadata} />} previousValue={<MetadataList metadata={change.existingSecret.secretMetadata} />}
newValue={<MetadataList metadata={change.secretMetadata} />} newValue={<MetadataList metadata={change.secretMetadata} />}
/> />
)} )}
@@ -220,7 +220,7 @@ export const SecretItem = memo(
} }
if (isDirty && !isSubmitting && !isAutoSavingRef.current) { if (isDirty && !isSubmitting && !isAutoSavingRef.current) {
const debounceTime = isPending ? 500 : 1500; const debounceTime = 600;
autoSaveTimeoutRef.current = setTimeout(() => { autoSaveTimeoutRef.current = setTimeout(() => {
autoSaveChanges(formValues); autoSaveChanges(formValues);
@@ -86,17 +86,16 @@ 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( useNavigationBlocker({
{
shouldBlock: pendingChanges.secrets.length > 0 || pendingChanges.folders.length > 0, shouldBlock: pendingChanges.secrets.length > 0 || pendingChanges.folders.length > 0,
message: "You have unsaved changes. If you leave now, your work will be lost. Do you want to continue?", message:
"You have unsaved changes. If you leave now, your work will be lost. Do you want to continue?",
context: { context: {
workspaceId, workspaceId,
environment, environment,
secretPath secretPath
} }
} });
);
const { addPendingChange } = useBatchModeActions(); const { addPendingChange } = useBatchModeActions();
const handleSecretOperation = async ( const handleSecretOperation = async (
@@ -368,7 +367,8 @@ export const SecretListView = ({
}), }),
timestamp: Date.now(), timestamp: Date.now(),
resourceType: "secret" resourceType: "secret",
existingSecret: orgSecret
}; };
addPendingChange(updateChange, { addPendingChange(updateChange, {