2024-11-08 14:22:49 +00:00
|
|
|
|
import React, { useCallback, useContext, ReactNode } from "react";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
import { format } from "date-fns";
|
2024-11-08 14:22:49 +00:00
|
|
|
|
import {
|
|
|
|
|
|
useQuery,
|
|
|
|
|
|
RefetchOptions,
|
|
|
|
|
|
RefetchQueryFilters,
|
|
|
|
|
|
QueryObserverResult,
|
|
|
|
|
|
} from "react-query";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
import FileSaver from "file-saver";
|
|
|
|
|
|
|
|
|
|
|
|
import { AppContext } from "context/app";
|
|
|
|
|
|
import { NotificationContext } from "context/notification";
|
2024-11-08 14:22:49 +00:00
|
|
|
|
import scriptAPI, { IHostScriptsResponse } from "services/entities/scripts";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
import { IHostScript } from "interfaces/script";
|
2024-11-08 14:22:49 +00:00
|
|
|
|
import { IApiError, getErrorReason } from "interfaces/errors";
|
2024-11-06 17:48:11 +00:00
|
|
|
|
|
|
|
|
|
|
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;
|
2024-11-08 14:22:49 +00:00
|
|
|
|
/** Help text on manage scripts page's modal but not on host detail's page modal */
|
2024-11-06 17:48:11 +00:00
|
|
|
|
runScriptHelpText?: boolean;
|
2024-11-08 14:22:49 +00:00
|
|
|
|
/** Host actions dropdown on host details page's modal but not on manage scripts page's modal */
|
2024-11-06 17:48:11 +00:00
|
|
|
|
showHostScriptActions?: boolean;
|
|
|
|
|
|
setRunScriptRequested?: (value: boolean) => void;
|
|
|
|
|
|
hostId?: number | null;
|
|
|
|
|
|
hostTeamId?: number | null;
|
2024-11-08 14:22:49 +00:00
|
|
|
|
refetchHostScripts?: <TPageData>(
|
|
|
|
|
|
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined
|
|
|
|
|
|
) => Promise<QueryObserverResult<IHostScriptsResponse, IApiError>>;
|
2024-11-06 17:48:11 +00:00
|
|
|
|
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],
|
|
|
|
|
|
() =>
|
2024-11-08 14:22:49 +00:00
|
|
|
|
selectedScriptDetails?.script_id
|
|
|
|
|
|
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
|
|
scriptAPI.downloadScript(selectedScriptDetails.script_id!)
|
2024-11-06 17:48:11 +00:00
|
|
|
|
: 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", "Couldn’t 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) => {
|
2024-11-08 14:22:49 +00:00
|
|
|
|
if (hostId && !!setRunScriptRequested && !!refetchHostScripts) {
|
2024-11-06 17:48:11 +00:00
|
|
|
|
switch (action) {
|
|
|
|
|
|
case "showRunDetails": {
|
2024-11-08 14:22:49 +00:00
|
|
|
|
if (script.last_execution?.execution_id) {
|
|
|
|
|
|
onClickRunDetails &&
|
|
|
|
|
|
onClickRunDetails(script.last_execution?.execution_id);
|
|
|
|
|
|
}
|
2024-11-06 17:48:11 +00:00
|
|
|
|
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();
|
2024-11-08 14:22:49 +00:00
|
|
|
|
|
|
|
|
|
|
onCancel(); // Running a script returns to run script modal
|
2024-11-06 17:48:11 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
renderFlash("error", getErrorReason(e));
|
|
|
|
|
|
setRunScriptRequested(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
default: // do nothing
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[
|
|
|
|
|
|
hostId,
|
|
|
|
|
|
onClickRunDetails,
|
|
|
|
|
|
setRunScriptRequested,
|
|
|
|
|
|
refetchHostScripts,
|
|
|
|
|
|
renderFlash,
|
2024-11-08 14:22:49 +00:00
|
|
|
|
onCancel,
|
2024-11-06 17:48:11 +00:00
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
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;
|