Files

298 lines
8.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, {
useCallback,
useContext,
useRef,
useState,
useEffect,
} from "react";
import { format } from "date-fns";
import { useQuery } from "react-query";
import FileSaver from "file-saver";
import { AppContext } from "context/app";
import scriptAPI from "services/entities/scripts";
import { IHostScript } from "interfaces/script";
import { notify } from "components/ToastNotification";
import Modal from "components/Modal";
import ModalFooter from "components/ModalFooter";
import Button from "components/buttons/Button";
import Spinner from "components/Spinner";
import Textarea from "components/Textarea";
import DataError from "components/DataError";
import ActionsDropdown from "components/ActionsDropdown";
import { generateActionDropdownOptions } from "pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import { IPaginatedListScript } from "pages/hosts/ManageHostsPage/components/RunScriptBatchPaginatedList/RunScriptBatchPaginatedList";
import RunScriptHelpText from "./RunScriptHelpText";
const baseClass = "script-details-modal";
type PartialOrFullHostScript =
| Pick<IHostScript, "script_id" | "name"> // Use on Scripts page does not include last_execution
| IHostScript;
interface IScriptDetailsModalProps {
onCancel: () => void;
/** optional onClose to allow both "go back" behavior and "close" behavior depending on context */
onClose?: () => void;
onDelete?: () => void;
runScriptHelpText?: boolean;
showHostScriptActions?: boolean;
onClickRun?: (script: IHostScript) => void;
hostTeamId?: number | null;
selectedScriptId?: number;
selectedScriptDetails?: PartialOrFullHostScript | IPaginatedListScript | null;
selectedScriptContent?: string;
isLoadingScriptContent?: boolean;
isScriptContentError?: Error | null;
isHidden?: boolean;
onClickRunDetails?: (scriptExecutionId: string) => void;
teamIdForApi?: number;
suppressSecondaryActions?: boolean;
customPrimaryButtons?: React.ReactNode;
}
const ScriptDetailsModal = ({
onCancel,
onClose,
onDelete,
onClickRun,
runScriptHelpText = false,
showHostScriptActions = false,
hostTeamId,
selectedScriptId,
selectedScriptDetails,
selectedScriptContent,
isLoadingScriptContent,
isScriptContentError,
isHidden = false,
onClickRunDetails,
teamIdForApi,
suppressSecondaryActions = false,
customPrimaryButtons,
}: IScriptDetailsModalProps) => {
// For scrollable modal
const [isTopScrolling, setIsTopScrolling] = useState(false);
const topDivRef = useRef<HTMLDivElement>(null);
const checkScroll = () => {
if (topDivRef.current) {
const isScrolling =
topDivRef.current.scrollHeight > topDivRef.current.clientHeight;
setIsTopScrolling(isScrolling);
}
};
const {
currentUser,
isGlobalAdmin,
isAnyTeamAdmin,
isGlobalMaintainer,
isAnyTeamMaintainer,
isTeamTechnician,
isGlobalTechnician,
} = useContext(AppContext);
const isTechnician = !!isTeamTechnician || !!isGlobalTechnician;
const canRunScripts = !!(
isGlobalAdmin ||
isAnyTeamAdmin ||
isGlobalMaintainer ||
isAnyTeamMaintainer
);
// handle multiple possibilities for `selectedScriptDetails`
let scriptId: number | null = null;
if (selectedScriptId) {
scriptId = selectedScriptId;
} else if (selectedScriptDetails) {
if ("script_id" in selectedScriptDetails) {
scriptId = selectedScriptDetails.script_id;
} else if ("id" in selectedScriptDetails) {
scriptId = selectedScriptDetails.id;
}
}
const {
data: scriptContent,
error: isSelectedScriptContentError,
isLoading: isLoadingSelectedScriptContent,
} = useQuery<string, Error>(
["scriptContent", scriptId],
() =>
scriptId
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
scriptAPI.downloadScript(scriptId)
: Promise.resolve(null),
{
refetchOnWindowFocus: false,
enabled: !selectedScriptContent && !!scriptId,
}
);
// For scrollable modal
useEffect(() => {
checkScroll();
window.addEventListener("resize", checkScroll);
return () => window.removeEventListener("resize", checkScroll);
}, [scriptContent]); // Re-run when data changes
const getScriptContent = async () => {
try {
const content = selectedScriptContent || scriptContent || "";
const formatDate = format(new Date(), "yyyy-MM-dd");
const filename = `${formatDate} ${
selectedScriptDetails?.name || "Script details"
}`;
const file = new File([content], filename);
FileSaver.saveAs(file);
} catch {
notify.error("Couldnt download. Please try again.");
}
};
const onClickDownload = () => {
if (selectedScriptContent) {
const formatDate = format(new Date(), "yyyy-MM-dd");
const filename = `${formatDate} ${selectedScriptDetails?.name}`;
const file = new File([selectedScriptContent], filename);
FileSaver.saveAs(file);
} else {
getScriptContent();
}
};
const onSelectMoreActions = useCallback(
async (action: string, script: IHostScript) => {
switch (action) {
case "showRunDetails": {
if (script.last_execution?.execution_id) {
onClickRunDetails &&
onClickRunDetails(script.last_execution?.execution_id);
}
break;
}
case "run": {
// should always be present if these actions are visible
onClickRun && onClickRun(script);
break;
}
default: // do nothing
}
},
[onClickRunDetails, onClickRun]
);
const shouldShowFooter =
!isLoadingScriptContent && selectedScriptDetails !== undefined;
const renderFooter = () => {
return (
<ModalFooter
isTopScrolling={isTopScrolling}
secondaryButtons={
suppressSecondaryActions ? undefined : (
<>
<Button
className={`${baseClass}__action-button`}
variant="subdued"
onClick={() => onClickDownload()}
icon="download"
ariaLabel="Download script"
/>
<GitOpsModeTooltipWrapper
position="bottom"
renderChildren={(disableChildren) => (
<Button
disabled={disableChildren}
className={`${baseClass}__action-button`}
variant="subdued"
onClick={onDelete}
icon="trash"
ariaLabel="Delete script"
/>
)}
/>
</>
)
}
primaryButtons={
customPrimaryButtons || (
<>
{showHostScriptActions && selectedScriptDetails && (
<div className={`${baseClass}__manage-automations-wrapper`}>
<ActionsDropdown
className={`${baseClass}__manage-automations-dropdown`}
onChange={(value) =>
onSelectMoreActions(
value,
selectedScriptDetails as IHostScript
)
}
placeholder="More actions"
isSearchable={false}
options={generateActionDropdownOptions(
currentUser,
hostTeamId || null,
selectedScriptDetails as IHostScript
)}
menuPlacement="top"
variant="subdued"
/>
</div>
)}
<Button onClick={onCancel}>Close</Button>
</>
)
}
/>
);
};
const renderContent = () => {
if (isLoadingScriptContent || isLoadingSelectedScriptContent) {
return <Spinner />;
}
if (isScriptContentError || isSelectedScriptContentError) {
return <DataError description="Close this modal and try again." />;
}
return (
<div
className={`${baseClass}__script-content modal-scrollable-content`}
ref={topDivRef}
>
<Textarea label="Script content:" variant="code">
{scriptContent}
</Textarea>
{runScriptHelpText && (
<RunScriptHelpText
className="form-field__help-text"
isTechnician={isTechnician}
canRunScripts={canRunScripts}
teamId={teamIdForApi}
/>
)}
</div>
);
};
return (
<Modal
className={baseClass}
title={selectedScriptDetails?.name || "Script details"}
width="large"
onExit={onClose ?? onCancel}
isHidden={isHidden}
>
{renderContent()}
{shouldShowFooter && renderFooter()}
</Modal>
);
};
export default ScriptDetailsModal;