fleet/frontend/pages/ManageControlsPage/Scripts/components/EditScriptModal/EditScriptModal.tsx
jacobshandling 0db86ef2f1
UI housekeeping: Update Modal.children from JSX.Element to React.ReactNode, remove empty fragment wrappers (#41394)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Simplified modal structures across multiple dialog components for
improved code maintainability.
* Enhanced modal component's flexibility to support broader content
types.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-10 15:30:55 -07:00

247 lines
6.2 KiB
TypeScript

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 RunScriptHelpText from "pages/hosts/components/ScriptDetailsModal/RunScriptHelpText";
import scriptAPI from "services/entities/scripts";
import Button from "components/buttons/Button";
import DataError from "components/DataError";
import Editor from "components/Editor";
import Modal from "components/Modal";
import ModalFooter from "components/ModalFooter";
import Spinner from "components/Spinner";
import { ScriptContent } from "interfaces/script";
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
import { getErrorMessage } from "../ScriptUploadModal/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;
scriptName: string;
}
const validate = (scriptContent: string) => {
if (scriptContent.trim() === "") {
return "Script cannot be empty";
}
return null;
};
const EditScriptModal = ({
scriptId,
scriptName,
onExit,
}: IEditScriptModal) => {
const { renderFlash } = useContext(NotificationContext);
const {
currentTeam,
isGlobalAdmin,
isAnyTeamAdmin,
isGlobalMaintainer,
isAnyTeamMaintainer,
isTeamTechnician,
isGlobalTechnician,
} = useContext(AppContext);
const isTechnician = !!isTeamTechnician || !!isGlobalTechnician;
const canRunScripts = !!(
isGlobalAdmin ||
isAnyTeamAdmin ||
isGlobalMaintainer ||
isAnyTeamMaintainer
);
// Editable script content
const [scriptFormData, setScriptFormData] = useState("");
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>(
[scriptId],
() => scriptAPI.downloadScript(scriptId),
{
...DEFAULT_USE_QUERY_OPTIONS,
onSuccess: (curScriptContent_) => {
setScriptFormData(curScriptContent_);
},
}
);
const onChange = (value: string) => {
setScriptFormData(value);
const err = validate(value);
if (!err && !!formError) {
setFormError(validate(value));
}
};
const onBlur = () => {
setFormError(validate(scriptFormData));
};
const onSave = async () => {
const err = validate(scriptFormData);
setFormError(err);
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);
renderFlash("success", "Successfully saved script.");
onExit();
} catch (e) {
renderFlash("error", getErrorMessage(e));
} finally {
setIsSubmitting(false);
setShowConfirmChanges(false);
}
};
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSave();
};
const renderContent = () => {
if (isLoadingSelectedScriptContent) {
return <Spinner />;
}
if (isSelectedScriptContentError) {
return <DataError description="Close this modal and try again." />;
}
// Set editing mode based on the file extension.
const mode = scriptName.match(/\.sh$/) ? "sh" : "powershell";
return (
<>
<form onSubmit={onSubmit}>
<Editor
mode={mode}
error={formError}
label="Script"
onBlur={onBlur}
onChange={onChange}
value={scriptFormData}
/>
<RunScriptHelpText
className="form-field__help-text"
isTechnician={isTechnician}
canRunScripts={canRunScripts}
teamId={currentTeam?.id}
/>
</form>
{canRunScripts && (
<ModalFooter
primaryButtons={
<>
<Button onClick={onExit} variant="inverse">
Cancel
</Button>
<Button
onClick={onSave}
isLoading={isSubmitting}
disabled={!!formError}
>
Save
</Button>
</>
}
/>
)}
</>
);
};
const classes = classnames(baseClass, {
[`${baseClass}__hide-main`]: !!showConfirmChanges,
});
return (
<>
<Modal
className={classes}
title={scriptName}
width="large"
onExit={onExit}
>
{renderContent()}
</Modal>
{!!showConfirmChanges && (
<WarningModal
onExit={() => setShowConfirmChanges(false)}
onSave={onSave}
scriptName={scriptName}
isSubmitting={isSubmitting}
/>
)}
</>
);
};
export default EditScriptModal;