import React, { useCallback, useContext, useEffect, useState } from "react"; import { useQuery } from "react-query"; import PATHS from "router/paths"; import classnames from "classnames"; import { Link } from "react-router"; import Radio from "components/forms/fields/Radio"; // @ts-ignore import InputField from "components/forms/fields/InputField"; import TooltipWrapper from "components/TooltipWrapper"; import { NotificationContext } from "context/notification"; import { addTeamIdCriteria, 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, IRunScriptBatchRequest, } 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"; import { validateFormData, IRunScriptBatchModalFormValidation, } from "./helpers"; const baseClass = "run-script-batch-modal"; export interface IRunScriptBatchModalScheduleFormData { date: string; time: string; } 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; teamId: number; // If we are on the free tier, we don't want to apply any kind of team filters (since the feature is Premium only). isFreeTier?: boolean; totalFilteredHostsCount: number; selectedHostIds: number[]; onCancel: () => void; } const RunScriptBatchModal = ({ runByFilters = false, filters, totalFilteredHostsCount, selectedHostIds, teamId, isFreeTier, onCancel, }: IRunScriptBatchModal) => { const { renderFlash } = useContext(NotificationContext); const [currentTimeUTC, setCurrentTimeUTC] = useState(""); useEffect(() => { const intervalId = setInterval(() => { const now = new Date(); const hours = now.getUTCHours().toString().padStart(2, "0"); const minutes = now.getUTCMinutes().toString().padStart(2, "0"); setCurrentTimeUTC(`The current time in UTC is ${hours}:${minutes}`); }, 1000); // Cleanup function to clear the interval return () => clearInterval(intervalId); }, []); const [batchRunDate, setBatchRunDate] = useState(""); const [batchRunTime, setBatchRunTime] = useState(""); const [ formValidation, setFormValidation, ] = useState(() => validateFormData({ date: batchRunDate, time: batchRunTime }) ); const [runMode, setRunMode] = useState<"run_now" | "schedule">("run_now"); const [selectedScript, setSelectedScript] = useState( undefined ); const [isUpdating, setIsUpdating] = useState(false); const [scriptForDetails, setScriptForDetails] = useState< IPaginatedListScript | undefined >(undefined); // just used to get the total number of scripts, could be optimized by implementing a dedicated scriptsCount endpoint const { data: scripts } = useQuery< IScriptsResponse, Error, IScript[], IListScriptsQueryKey[] >( [addTeamIdCriteria({ scope: "scripts" }, teamId, isFreeTier)], ({ queryKey }) => { return scriptsAPI.getScripts(queryKey[0]); }, { ...DEFAULT_USE_QUERY_OPTIONS, keepPreviousData: true, select: (data) => { return data.scripts || []; }, } ); // Handle switching between "run now" and "schedule" modes. const onChangeRunMode = (mode: "run_now" | "schedule") => { setRunMode(mode); setFormValidation( validateFormData({ date: batchRunDate, time: batchRunTime }, mode) ); }; // Handle changes to the date and time inputs. const onInputChange = (update: { name: string; value: string }) => { if (update.name === "date") { setBatchRunDate(update.value); } else if (update.name === "time") { setBatchRunTime(update.value); } setFormValidation( validateFormData( { date: batchRunDate, time: batchRunTime, [update.name]: update.value, }, runMode ) ); }; const onRunScriptBatch = useCallback( async (script: IScript) => { setIsUpdating(true); // Create the base request. let body: IRunScriptBatchRequest; if (runByFilters) { body = { script_id: script.id, filters: addTeamIdCriteria(filters, teamId, isFreeTier), }; } else { body = { script_id: script.id, host_ids: selectedHostIds, }; } // Add not_before if scheduling if (runMode === "schedule") { body.not_before = `${batchRunDate}T${batchRunTime}:00.000Z`; } try { await scriptsAPI.runScriptBatch(body); if (runMode === "schedule") { renderFlash( "success", Successfully scheduled script. Show schedule ); } else { renderFlash( "success", Successfully ran script. Show script activity ); } onCancel(); // TODO -- redirect to the batch scripts page. } 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, runMode, batchRunDate, batchRunTime] ); const renderModalContent = () => { if (scripts === undefined) { return ; } if (!scripts.length) { return ( You can add saved scripts{" "} here . } /> ); } if (!selectedScript) { const targetCount = runByFilters ? totalFilteredHostsCount : selectedHostIds.length; return ( <>

Run a script on{" "} {targetCount.toLocaleString()} host{targetCount > 1 ? "s" : ""} , or schedule a script to run on targeted hosts in the future.

setSelectedScript(script)} isUpdating={isUpdating} teamId={teamId} isFreeTier={isFreeTier} scriptCount={scripts.length} setScriptForDetails={setScriptForDetails} /> ); } const platforms = selectedScript.name.indexOf(".ps1") > 0 ? "Windows" : "macOS and Linux"; return (

{selectedScript.name} will run on compatible hosts ({platforms} ).

Schedule
onChangeRunMode("run_now")} /> onChangeRunMode("schedule")} />
{runMode === "schedule" && (
)}
); }; const classes = classnames(baseClass, { [`${baseClass}__hide-main`]: !!scriptForDetails, }); return ( <> <> {renderModalContent()} {!selectedScript && !scriptForDetails && (
)} {selectedScript && (
)}
{!!scriptForDetails && !selectedScript && ( setScriptForDetails(undefined)} selectedScriptDetails={scriptForDetails} suppressSecondaryActions customPrimaryButtons={
} /> )} ); }; export default RunScriptBatchModal;