fleet/frontend/pages/hosts/ManageHostsPage/components/RunScriptBatchModal/RunScriptBatchModal.tsx

202 lines
5.7 KiB
TypeScript
Raw Normal View History

import React, { useCallback, useContext, useState } from "react";
import { useQuery } from "react-query";
import classnames from "classnames";
import { NotificationContext } from "context/notification";
import { IScript } from "interfaces/script";
import { getErrorReason } from "interfaces/errors";
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
import Modal from "components/Modal";
import scriptsAPI, {
IListScriptsQueryKey,
IScriptBatchSupportedFilters,
IScriptsResponse,
} from "services/entities/scripts";
import ScriptDetailsModal from "pages/hosts/components/ScriptDetailsModal";
import Spinner from "components/Spinner";
import EmptyTable from "components/EmptyTable";
import Button from "components/buttons/Button";
import RunScriptBatchPaginatedList from "../RunScriptBatchPaginatedList";
import { IPaginatedListScript } from "../RunScriptBatchPaginatedList/RunScriptBatchPaginatedList";
const baseClass = "run-script-batch-modal";
interface IRunScriptBatchModal {
runByFilters: boolean; // otherwise, by selectedHostIds
// since teamId has multiple uses in this component, it's passed in as its own prop and added to
// `filters` as needed
filters: Omit<IScriptBatchSupportedFilters, "team_id">;
teamId: number;
totalFilteredHostsCount: number;
selectedHostIds: number[];
onCancel: () => void;
}
const RunScriptBatchModal = ({
runByFilters = false,
filters,
totalFilteredHostsCount,
selectedHostIds,
teamId,
onCancel,
}: IRunScriptBatchModal) => {
const { renderFlash } = useContext(NotificationContext);
const [isUpdating, setIsUpdating] = useState(false);
const [scriptForDetails, setScriptForDetails] = useState<
IPaginatedListScript | undefined
>(undefined);
// just used to get total number of scripts, could be optimized by implementing a dedicated scriptsCount endpoint
const { data: scripts } = useQuery<
IScriptsResponse,
Error,
IScript[],
IListScriptsQueryKey[]
>(
[
{
scope: "scripts",
team_id: teamId,
},
],
({ queryKey }) => {
return scriptsAPI.getScripts(queryKey[0]);
},
{
...DEFAULT_USE_QUERY_OPTIONS,
keepPreviousData: true,
select: (data) => data.scripts || [],
}
);
const onRunScriptBatch = useCallback(
async (script: IScript) => {
setIsUpdating(true);
const body = runByFilters
? // satisfy IScriptBatchSupportedFilters
{ script_id: script.id, filters: { ...filters, team_id: teamId } }
: { script_id: script.id, host_ids: selectedHostIds };
try {
await scriptsAPI.runScriptBatch(body);
renderFlash(
"success",
`Script is running on ${
runByFilters
? totalFilteredHostsCount.toLocaleString()
: selectedHostIds.length.toLocaleString()
} hosts, or will run as each host comes online. See host details for individual results.`
);
} catch (error) {
let errorMessage = "Could not run script.";
if (getErrorReason(error).includes("too many hosts")) {
errorMessage =
"Could not run script: too many hosts targeted. Please try again with fewer hosts.";
}
renderFlash("error", errorMessage);
// can determine more specific error case with additional call to upcoming summary endpoint
} finally {
setIsUpdating(false);
}
},
[renderFlash, selectedHostIds]
);
const renderModalContent = () => {
if (scripts === undefined) {
return <Spinner />;
}
if (!scripts.length) {
return (
<EmptyTable
header="No scripts available for this team"
info={
<>
You can add saved scripts{" "}
<a href={`/controls/scripts?team_id=${teamId}`}>here</a>.
</>
}
/>
);
}
const targetCount = runByFilters
? totalFilteredHostsCount
: selectedHostIds.length;
return (
<>
<p>
Will run on{" "}
<b>
{targetCount.toLocaleString()} host{targetCount > 1 ? "s" : ""}
</b>
. You can see individual script results on the host details page.
</p>
<RunScriptBatchPaginatedList
onRunScript={onRunScriptBatch}
isUpdating={isUpdating}
teamId={teamId}
scriptCount={scripts.length}
setScriptForDetails={setScriptForDetails}
/>
</>
);
};
const classes = classnames(baseClass, {
[`${baseClass}__hide-main`]: !!scriptForDetails,
});
return (
<>
<Modal
title="Run script"
onExit={onCancel}
onEnter={onCancel}
className={classes}
disableClosingModal={isUpdating}
>
<>
{renderModalContent()}
<div className="modal-cta-wrap">
<Button disabled={isUpdating} onClick={onCancel}>
Done
</Button>
</div>
</>
</Modal>
{!!scriptForDetails && (
<ScriptDetailsModal
onCancel={() => setScriptForDetails(undefined)}
selectedScriptDetails={scriptForDetails}
suppressSecondaryActions
customPrimaryButtons={
<div className="modal-cta-wrap">
<Button
onClick={() => {
onRunScriptBatch(scriptForDetails);
}}
isLoading={isUpdating}
>
Run
</Button>
<Button
onClick={() => setScriptForDetails(undefined)}
variant="inverse"
>
Go back
</Button>
</div>
}
/>
)}
</>
);
};
export default RunScriptBatchModal;