fleet/frontend/pages/ManageControlsPage/Scripts/components/ScriptDetailsModal/ScriptDetailsModal.tsx

261 lines
8.1 KiB
TypeScript
Raw Normal View History

import React, { useCallback, useContext, ReactNode } from "react";
import { format } from "date-fns";
import {
useQuery,
RefetchOptions,
RefetchQueryFilters,
QueryObserverResult,
} from "react-query";
import FileSaver from "file-saver";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import scriptAPI, { IHostScriptsResponse } from "services/entities/scripts";
import { IHostScript } from "interfaces/script";
import { IApiError, getErrorReason } from "interfaces/errors";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import Spinner from "components/Spinner";
import Icon from "components/Icon";
import Textarea from "components/Textarea";
import CustomLink from "components/CustomLink";
import DataError from "components/DataError";
import paths from "router/paths";
import ActionsDropdown from "components/ActionsDropdown";
import { generateActionDropdownOptions } from "pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig";
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;
onDelete: () => void;
/** Help text on manage scripts page's modal but not on host detail's page modal */
runScriptHelpText?: boolean;
/** Host actions dropdown on host details page's modal but not on manage scripts page's modal */
showHostScriptActions?: boolean;
setRunScriptRequested?: (value: boolean) => void;
hostId?: number | null;
hostTeamId?: number | null;
refetchHostScripts?: <TPageData>(
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
) => Promise<QueryObserverResult<IHostScriptsResponse, IApiError>>;
selectedScriptDetails?: PartialOrFullHostScript;
selectedScriptContent?: string;
isLoadingScriptContent?: boolean;
isScriptContentError?: Error | null;
isHidden?: boolean;
onClickRunDetails?: (scriptExecutionId: string) => void;
}
const ScriptDetailsModal = ({
onCancel,
onDelete,
runScriptHelpText = false,
showHostScriptActions = false,
setRunScriptRequested,
hostId,
hostTeamId,
refetchHostScripts,
selectedScriptDetails,
selectedScriptContent,
isLoadingScriptContent,
isScriptContentError,
isHidden = false,
onClickRunDetails,
}: IScriptDetailsModalProps) => {
const { currentUser } = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext);
const {
data: scriptContent,
error: isSelectedScriptContentError,
isLoading: isLoadingSelectedScriptContent,
} = useQuery<any, Error>(
["scriptContent", selectedScriptDetails?.script_id],
() =>
selectedScriptDetails?.script_id
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
scriptAPI.downloadScript(selectedScriptDetails.script_id!)
: Promise.resolve(null),
{
refetchOnWindowFocus: false,
enabled: !selectedScriptContent && !!selectedScriptDetails?.script_id,
}
);
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 {
renderFlash("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) => {
if (hostId && !!setRunScriptRequested && !!refetchHostScripts) {
switch (action) {
case "showRunDetails": {
if (script.last_execution?.execution_id) {
onClickRunDetails &&
onClickRunDetails(script.last_execution?.execution_id);
}
break;
}
case "run": {
try {
setRunScriptRequested && setRunScriptRequested(true);
await scriptAPI.runScript({
host_id: hostId,
script_id: script.script_id,
});
renderFlash(
"success",
"Script is running or will run when the host comes online."
);
refetchHostScripts();
onCancel(); // Running a script returns to run script modal
} catch (e) {
renderFlash("error", getErrorReason(e));
setRunScriptRequested(false);
}
break;
}
default: // do nothing
}
}
},
[
hostId,
onClickRunDetails,
setRunScriptRequested,
refetchHostScripts,
renderFlash,
onCancel,
]
);
const shouldShowFooter = () => {
return !isLoadingScriptContent && selectedScriptDetails !== undefined;
};
const renderFooter = () => {
if (!shouldShowFooter) {
return <></>;
}
return (
<>
<div className={`secondary-actions ${baseClass}__script-actions`}>
<Button
className={`${baseClass}__action-button`}
variant="icon"
onClick={() => onClickDownload()}
>
<Icon name="download" />
</Button>
<Button
className={`${baseClass}__action-button`}
variant="icon"
onClick={onDelete}
>
<Icon name="trash" color="ui-fleet-black-75" />
</Button>
</div>
<div className={`primary-actions ${baseClass}__host-script-actions`}>
{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"
/>
</div>
)}
<Button onClick={onCancel} variant="brand">
Done
</Button>
</div>
</>
);
};
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`}>
<span>Script content:</span>
<Textarea className={`${baseClass}__script-content-textarea`}>
{scriptContent}
</Textarea>
{runScriptHelpText && (
<div className="form-field__help-text">
To run this script on a host, go to the{" "}
<CustomLink text="Hosts" url={paths.MANAGE_HOSTS} /> page and select
a host.
<br />
To run the script across multiple hosts, add a policy automation on
the <CustomLink text="Policies" url={paths.MANAGE_POLICIES} /> page.
</div>
)}
</div>
);
};
return (
<Modal
className={baseClass}
title={selectedScriptDetails?.name || "Script details"}
width="large"
onExit={onCancel}
actionsFooter={shouldShowFooter() ? renderFooter() : undefined}
isHidden={isHidden}
>
{renderContent()}
</Modal>
);
};
export default ScriptDetailsModal;