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,15 +606,16 @@ 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,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
__v: 0 __v: 0
})) || [] })) || []
: mergedSecrets[updateIndex].tags
}; };
} }
break; break;
@@ -935,25 +938,34 @@ const Page = () => {
folders?.length === 0 && <PermissionDeniedBanner />} folders?.length === 0 && <PermissionDeniedBanner />}
</div> </div>
</div> </div>
{!isDetailsLoading && totalCount > 0 && ( {!isDetailsLoading &&
<Pagination (totalCount > 0 ||
startAdornment={ pendingChanges.secrets.length > 0 ||
<SecretTableResourceCount pendingChanges.folders.length > 0) && (
dynamicSecretCount={totalDynamicSecretCount} <Pagination
importCount={totalImportCount} startAdornment={
secretCount={totalSecretCount} <SecretTableResourceCount
folderCount={totalFolderCount} dynamicSecretCount={totalDynamicSecretCount}
secretRotationCount={totalSecretRotationCount} importCount={totalImportCount}
/> secretCount={
} totalSecretCount +
className="rounded-b-md border-t border-solid border-t-mineshaft-600" pendingChanges.secrets.filter((s) => s.type === PendingAction.Create).length
count={totalCount} }
page={page} folderCount={
perPage={perPage} totalFolderCount +
onChangePage={(newPage) => setPage(newPage)} pendingChanges.folders.filter((f) => f.type === PendingAction.Create).length
onChangePerPage={handlePerPageChange} }
/> secretRotationCount={totalSecretRotationCount}
)} />
}
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
count={totalCount + pendingChanges.secrets.length + pendingChanges.folders.length}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
<Modal <Modal
isOpen={createSecretPopUp.isOpen} isOpen={createSecretPopUp.isOpen}
onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)} onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)}
@@ -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 ===
return { pendingChanges: newChanges }; generateContextKey(
state.currentContext.workspaceId,
state.currentContext.environment,
state.currentContext.secretPath
);
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:
message: "You have unsaved changes. If you leave now, your work will be lost. Do you want to continue?", "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, {