mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 08:26:00 +00:00
Merge pull request #329 from fervillarrealm/feature/52-save-changes-user-leaving-dashboard
feat(ui): save changes when user leaving dashboard
This commit is contained in:
@@ -44,3 +44,5 @@ const plansProd: Mapping = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const plans = plansProd || plansDev;
|
export const plans = plansProd || plansDev;
|
||||||
|
|
||||||
|
export const leaveConfirmDefaultMessage = 'Do you want to save your results before leaving this page?';
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export { usePopUp } from './usePopUp';
|
export { usePopUp } from './usePopUp';
|
||||||
export { useToggle } from './useToggle';
|
export { useToggle } from './useToggle';
|
||||||
|
export { useLeaveConfirm } from './useLeaveConfirm';
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
import { leaveConfirmDefaultMessage } from '@app/const';
|
||||||
|
|
||||||
|
type LeaveConfirmProps = {
|
||||||
|
initialValue: boolean,
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LeaveConfirmReturn {
|
||||||
|
hasUnsavedChanges: boolean,
|
||||||
|
setHasUnsavedChanges: Dispatch<SetStateAction<boolean>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLeaveConfirm({
|
||||||
|
initialValue,
|
||||||
|
message = leaveConfirmDefaultMessage,
|
||||||
|
}: LeaveConfirmProps): LeaveConfirmReturn {
|
||||||
|
const router = useRouter()
|
||||||
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(initialValue);
|
||||||
|
|
||||||
|
const onRouteChangeStart = useCallback(() => {
|
||||||
|
if (hasUnsavedChanges) {
|
||||||
|
if (window.confirm(message)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
throw new Error("Abort route change by user's confirmation.")
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [hasUnsavedChanges])
|
||||||
|
|
||||||
|
const handleWindowClose = useCallback((e: any) => {
|
||||||
|
if (!hasUnsavedChanges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = message;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
router.events.on("routeChangeStart", onRouteChangeStart);
|
||||||
|
window.addEventListener('beforeunload', handleWindowClose);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
router.events.off("routeChangeStart", onRouteChangeStart);
|
||||||
|
window.removeEventListener('beforeunload', handleWindowClose);
|
||||||
|
}
|
||||||
|
}, [onRouteChangeStart, handleWindowClose]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasUnsavedChanges,
|
||||||
|
setHasUnsavedChanges,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -35,9 +35,11 @@ import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
|
|||||||
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
|
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
|
||||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||||
import { IconButton } from '@app/components/v2';
|
import { IconButton } from '@app/components/v2';
|
||||||
|
import { leaveConfirmDefaultMessage } from '@app/const';
|
||||||
import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount';
|
import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount';
|
||||||
import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
|
import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
|
||||||
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
|
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
|
||||||
|
import { useLeaveConfirm } from '@app/hooks';
|
||||||
|
|
||||||
import addSecrets from '../api/files/AddSecrets';
|
import addSecrets from '../api/files/AddSecrets';
|
||||||
import deleteSecrets from '../api/files/DeleteSecrets';
|
import deleteSecrets from '../api/files/DeleteSecrets';
|
||||||
@@ -116,7 +118,6 @@ function findDuplicates(arr: any[]) {
|
|||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [data, setData] = useState<SecretDataProps[] | null>();
|
const [data, setData] = useState<SecretDataProps[] | null>();
|
||||||
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
|
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
|
||||||
const [buttonReady, setButtonReady] = useState(false);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [blurred, setBlurred] = useState(true);
|
const [blurred, setBlurred] = useState(true);
|
||||||
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
||||||
@@ -137,6 +138,7 @@ export default function Dashboard() {
|
|||||||
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
|
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
|
||||||
const [projectTags, setProjectTags] = useState<Tag[]>([]);
|
const [projectTags, setProjectTags] = useState<Tag[]>([]);
|
||||||
|
|
||||||
|
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({initialValue: false});
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
@@ -153,36 +155,6 @@ export default function Dashboard() {
|
|||||||
setAtSecretsAreaTop(false);
|
setAtSecretsAreaTop(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// #TODO: fix save message for changing reroutes
|
|
||||||
// const beforeRouteHandler = (url) => {
|
|
||||||
// const warningText =
|
|
||||||
// "Do you want to save your results bfore leaving this page?";
|
|
||||||
// if (!buttonReady) return;
|
|
||||||
// if (router.asPath !== url && !confirm(warningText)) {
|
|
||||||
// // router.events.emit('routeChangeError');
|
|
||||||
// // setData(data)
|
|
||||||
// savePush();
|
|
||||||
// throw `Route change to "${url}" was aborted (this error can be safely ignored).`;
|
|
||||||
// } else {
|
|
||||||
// setButtonReady(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// prompt the user if they try and leave with unsaved changes
|
|
||||||
useEffect(() => {
|
|
||||||
const warningText = 'Do you want to save your results before leaving this page?';
|
|
||||||
const handleWindowClose = (e: any) => {
|
|
||||||
if (!buttonReady) return;
|
|
||||||
e.preventDefault();
|
|
||||||
e.returnValue = warningText;
|
|
||||||
};
|
|
||||||
window.addEventListener('beforeunload', handleWindowClose);
|
|
||||||
// router.events.on('routeChangeStart', beforeRouteHandler);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('beforeunload', handleWindowClose);
|
|
||||||
// router.events.off('routeChangeStart', beforeRouteHandler);
|
|
||||||
};
|
|
||||||
}, [buttonReady]);
|
|
||||||
|
|
||||||
// TODO(akhilmhdh): change to FP
|
// TODO(akhilmhdh): change to FP
|
||||||
const sortValuesHandler = (
|
const sortValuesHandler = (
|
||||||
@@ -318,7 +290,7 @@ export default function Dashboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => {
|
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => {
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
toggleSidebar('None');
|
toggleSidebar('None');
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `${secretName || 'Secret'} has been deleted. Remember to save changes.`,
|
text: `${secretName || 'Secret'} has been deleted. Remember to save changes.`,
|
||||||
@@ -332,27 +304,27 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
const modifyValue = (value: string, pos: number) => {
|
const modifyValue = (value: string, pos: number) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e)));
|
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e)));
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyValueOverride = (valueOverride: string | undefined, pos: number) => {
|
const modifyValueOverride = (valueOverride: string | undefined, pos: number) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e)));
|
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e)));
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyKey = (key: string, pos: number) => {
|
const modifyKey = (key: string, pos: number) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e)));
|
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e)));
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyComment = (comment: string, pos: number) => {
|
const modifyComment = (comment: string, pos: number) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e)));
|
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e)));
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyTags = (tags: Tag[], pos: number) => {
|
const modifyTags = (tags: Tag[], pos: number) => {
|
||||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
|
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// For speed purposes and better perforamance, we are using useCallback
|
// For speed purposes and better perforamance, we are using useCallback
|
||||||
@@ -422,7 +394,7 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Once "Save changes" is clicked, disable that button
|
// Once "Save changes" is clicked, disable that button
|
||||||
setButtonReady(false);
|
setHasUnsavedChanges(false);
|
||||||
|
|
||||||
const secretsToBeDeleted = initialData!
|
const secretsToBeDeleted = initialData!
|
||||||
.filter(
|
.filter(
|
||||||
@@ -566,7 +538,7 @@ export default function Dashboard() {
|
|||||||
);
|
);
|
||||||
return filteredOldData.concat(filteredNewData);
|
return filteredOldData.concat(filteredNewData);
|
||||||
});
|
});
|
||||||
setButtonReady(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addData = (newData: SecretDataProps[]) => {
|
const addData = (newData: SecretDataProps[]) => {
|
||||||
@@ -587,6 +559,26 @@ export default function Dashboard() {
|
|||||||
deleteRow({ ids, secretName });
|
deleteRow({ ids, secretName });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnEnvironmentChange = (envName: string) => {
|
||||||
|
if(hasUnsavedChanges) {
|
||||||
|
if (!window.confirm(leaveConfirmDefaultMessage)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedWorkspaceEnv = workspaceEnvs.find(({ name }: { name: string }) => envName === name) || {
|
||||||
|
name: 'unknown',
|
||||||
|
slug: 'unknown',
|
||||||
|
isWriteDenied: false,
|
||||||
|
isReadDenied: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selectedWorkspaceEnv) {
|
||||||
|
if (snapshotData) setSelectedSnapshotEnv(selectedWorkspaceEnv);
|
||||||
|
else setSelectedEnv(selectedWorkspaceEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
|
};
|
||||||
|
|
||||||
return data ? (
|
return data ? (
|
||||||
<div className="bg-bunker-800 max-h-screen h-full relative flex flex-col justify-between text-white dark">
|
<div className="bg-bunker-800 max-h-screen h-full relative flex flex-col justify-between text-white dark">
|
||||||
<Head>
|
<Head>
|
||||||
@@ -655,16 +647,7 @@ export default function Dashboard() {
|
|||||||
<ListBox
|
<ListBox
|
||||||
isSelected={selectedEnv.name}
|
isSelected={selectedEnv.name}
|
||||||
data={workspaceEnvs.map(({ name }) => name)}
|
data={workspaceEnvs.map(({ name }) => name)}
|
||||||
onChange={(envName) =>
|
onChange={handleOnEnvironmentChange}
|
||||||
setSelectedEnv(
|
|
||||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
|
||||||
name: 'unknown',
|
|
||||||
slug: 'unknown',
|
|
||||||
isWriteDenied: false,
|
|
||||||
isReadDenied: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -681,14 +664,14 @@ export default function Dashboard() {
|
|||||||
icon={faClockRotateLeft}
|
icon={faClockRotateLeft}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
{(data?.length !== 0 || buttonReady) && !snapshotData && (
|
{(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && (
|
||||||
<div className="flex justify-start max-w-sm mt-1">
|
<div className="flex justify-start max-w-sm mt-1">
|
||||||
<Button
|
<Button
|
||||||
text={String(t('common:save-changes'))}
|
text={String(t('common:save-changes'))}
|
||||||
onButtonPressed={savePush}
|
onButtonPressed={savePush}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="md"
|
size="md"
|
||||||
active={buttonReady}
|
active={hasUnsavedChanges}
|
||||||
iconDisabled={faCheck}
|
iconDisabled={faCheck}
|
||||||
textDisabled={String(t('common:saved'))}
|
textDisabled={String(t('common:saved'))}
|
||||||
loading={saveLoading}
|
loading={saveLoading}
|
||||||
@@ -723,11 +706,11 @@ export default function Dashboard() {
|
|||||||
text: `Rollback has been performed successfully.`,
|
text: `Rollback has been performed successfully.`,
|
||||||
type: 'success'
|
type: 'success'
|
||||||
});
|
});
|
||||||
setButtonReady(false);
|
setHasUnsavedChanges(false);
|
||||||
}}
|
}}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="md"
|
size="md"
|
||||||
active={buttonReady}
|
active={hasUnsavedChanges}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -742,31 +725,13 @@ export default function Dashboard() {
|
|||||||
<ListBox
|
<ListBox
|
||||||
isSelected={selectedEnv.name}
|
isSelected={selectedEnv.name}
|
||||||
data={workspaceEnvs.map(({ name }) => name)}
|
data={workspaceEnvs.map(({ name }) => name)}
|
||||||
onChange={(envName) =>
|
onChange={handleOnEnvironmentChange}
|
||||||
setSelectedEnv(
|
|
||||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
|
||||||
name: 'unknown',
|
|
||||||
slug: 'unknown',
|
|
||||||
isWriteDenied: false,
|
|
||||||
isReadDenied: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ListBox
|
<ListBox
|
||||||
isSelected={selectedSnapshotEnv?.name || ''}
|
isSelected={selectedSnapshotEnv?.name || ''}
|
||||||
data={workspaceEnvs.map(({ name }) => name)}
|
data={workspaceEnvs.map(({ name }) => name)}
|
||||||
onChange={(envName) =>
|
onChange={handleOnEnvironmentChange}
|
||||||
setSelectedSnapshotEnv(
|
|
||||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
|
||||||
name: 'unknown',
|
|
||||||
slug: 'unknown',
|
|
||||||
isWriteDenied: false,
|
|
||||||
isReadDenied: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="h-10 w-full bg-mineshaft-700 hover:bg-white/10 ml-2 rounded-md flex flex-row items-center">
|
<div className="h-10 w-full bg-mineshaft-700 hover:bg-white/10 ml-2 rounded-md flex flex-row items-center">
|
||||||
@@ -970,7 +935,7 @@ export default function Dashboard() {
|
|||||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||||
createNewFile={addRow}
|
createNewFile={addRow}
|
||||||
errorDragAndDrop={errorDragAndDrop}
|
errorDragAndDrop={errorDragAndDrop}
|
||||||
setButtonReady={setButtonReady}
|
setButtonReady={setHasUnsavedChanges}
|
||||||
keysExist
|
keysExist
|
||||||
numCurrentRows={data.length}
|
numCurrentRows={data.length}
|
||||||
/>
|
/>
|
||||||
@@ -986,7 +951,7 @@ export default function Dashboard() {
|
|||||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||||
createNewFile={addRow}
|
createNewFile={addRow}
|
||||||
errorDragAndDrop={errorDragAndDrop}
|
errorDragAndDrop={errorDragAndDrop}
|
||||||
setButtonReady={setButtonReady}
|
setButtonReady={setHasUnsavedChanges}
|
||||||
numCurrentRows={data.length}
|
numCurrentRows={data.length}
|
||||||
keysExist={false}
|
keysExist={false}
|
||||||
/>
|
/>
|
||||||
@@ -1013,7 +978,7 @@ export default function Dashboard() {
|
|||||||
modifyValue={listenChangeValue}
|
modifyValue={listenChangeValue}
|
||||||
modifyValueOverride={listenChangeValueOverride}
|
modifyValueOverride={listenChangeValueOverride}
|
||||||
modifyComment={listenChangeComment}
|
modifyComment={listenChangeComment}
|
||||||
buttonReady={buttonReady}
|
buttonReady={hasUnsavedChanges}
|
||||||
workspaceEnvs={workspaceEnvs}
|
workspaceEnvs={workspaceEnvs}
|
||||||
selectedEnv={selectedEnv!}
|
selectedEnv={selectedEnv!}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
|
|||||||
Reference in New Issue
Block a user