UI: Warn before saving script contents (#29026)

## For #28699 auxiliary feature


![ezgif-28c6ba4048c004](https://github.com/user-attachments/assets/2631286a-b4bd-431a-8ea1-5b962af933dd)

- [x] Changes file added for user-visible changes in `changes/`
- [x] A detailed QA plan exists on the associated ticket (if it isn't
there, work with the product group's QA engineer to add it)
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
jacobshandling 2025-05-12 13:51:38 -07:00 committed by GitHub
parent a7b1e6358b
commit 262832bfff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 93 additions and 10 deletions

View file

@ -0,0 +1 @@
- Warn users of consequences when updating script contents

View file

@ -6,6 +6,7 @@ import Icon from "components/Icon/Icon";
const baseClass = "modal";
type ModalWidth = "medium" | "large" | "xlarge" | "auto";
// 650px 800px 850px auto
export interface IModalProps {
title: string | JSX.Element;

View file

@ -1,6 +1,8 @@
import React, { useContext, useState } from "react";
import { useQuery } from "react-query";
import classnames from "classnames";
import { NotificationContext } from "context/notification";
import { AppContext } from "context/app";
import { getPathWithQueryParams } from "utilities/url";
@ -21,6 +23,52 @@ import { getErrorMessage } from "../ScriptUploader/helpers";
const baseClass = "edit-script-modal";
interface IWarningModal {
onExit: () => void;
onSave: () => void;
scriptName: string;
isSubmitting: boolean;
}
const WarningModal = ({
onExit,
onSave,
scriptName,
isSubmitting,
}: IWarningModal) => {
return (
<Modal
className={`${baseClass}__warning`}
title="Save changes?"
onExit={onExit}
>
<>
<p>
The changes you are making will cancel any pending script runs for{" "}
<b>{scriptName}</b>.<br />
<br />
If this script is currently running on a host, it will complete, but
results won&apos;t appear in Fleet. <br />
<br />
You cannot undo this action.
</p>
<div className="modal-cta-wrap">
<Button
type="button"
onClick={onSave}
className="save-loading"
isLoading={isSubmitting}
>
Save
</Button>
<Button onClick={onExit} variant="inverse">
Cancel
</Button>
</div>
</>
</Modal>
);
};
interface IEditScriptModal {
onExit: () => void;
scriptId: number;
@ -47,7 +95,10 @@ const EditScriptModal = ({
const [isSubmitting, setIsSubmitting] = useState(false);
const [formError, setFormError] = useState<string | null>(null);
const [showConfirmChanges, setShowConfirmChanges] = useState(false);
const {
data: curScriptContent,
error: isSelectedScriptContentError,
isLoading: isLoadingSelectedScriptContent,
} = useQuery<ScriptContent, Error>(
@ -55,8 +106,8 @@ const EditScriptModal = ({
() => scriptAPI.downloadScript(scriptId),
{
...DEFAULT_USE_QUERY_OPTIONS,
onSuccess: (scriptContent) => {
setScriptFormData(scriptContent);
onSuccess: (curScriptContent_) => {
setScriptFormData(curScriptContent_);
},
}
);
@ -79,6 +130,12 @@ const EditScriptModal = ({
if (err || isSubmitting) {
return;
}
// if contents have changed and not already showing the warning modal
if (curScriptContent !== scriptFormData && !showConfirmChanges) {
setShowConfirmChanges(true);
return;
}
// show warning modal and abort this call
try {
setIsSubmitting(true);
await scriptAPI.updateScript(scriptId, scriptFormData, scriptName);
@ -88,6 +145,7 @@ const EditScriptModal = ({
renderFlash("error", getErrorMessage(e));
} finally {
setIsSubmitting(false);
setShowConfirmChanges(false);
}
};
@ -161,15 +219,28 @@ const EditScriptModal = ({
);
};
const classes = classnames(baseClass, {
[`${baseClass}__hide-main`]: !!showConfirmChanges,
});
return (
<Modal
className={baseClass}
title={scriptName}
width="large"
onExit={onExit}
>
{renderContent()}
</Modal>
<>
<Modal
className={classes}
title={scriptName}
width="large"
onExit={onExit}
>
{renderContent()}
</Modal>
{!!showConfirmChanges && (
<WarningModal
onExit={() => setShowConfirmChanges(false)}
onSave={onSave}
scriptName={scriptName}
isSubmitting={isSubmitting}
/>
)}
</>
);
};

View file

@ -0,0 +1,10 @@
.edit-script-modal {
&__hide-main {
visibility: hidden;
}
}
// since this modal shows only while anotheris also showing, suppress background for one of them to
// prevent "double-darkening"
.modal__background:has(.edit-script-modal.edit-script-modal__hide-main) {
visibility: hidden;
}