fleet/frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptBatchSummaryModal/ScriptBatchSummaryModal.tsx
jacobshandling 2fdee71031
UI: Update script batch success message formatting (#32282)
## For #32218 

<img width="1558" height="944" alt="Screenshot 2025-08-25 at 11 50
16 AM"
src="https://github.com/user-attachments/assets/d22e8771-2bb7-48a3-9597-3fe8c93fd3f1"
/>
<img width="1558" height="944" alt="Screenshot 2025-08-25 at 11 50
41 AM"
src="https://github.com/user-attachments/assets/888b8bad-283b-4d57-ab8f-bdd4b21b687b"
/>
<img width="1558" height="944" alt="Screenshot 2025-08-25 at 11 54
15 AM"
src="https://github.com/user-attachments/assets/264221cd-d20b-4c1d-9ec3-ec75f1824ceb"
/>


- [x] QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:

- [x] Confirmed that the fix is not expected to adversely impact load
test results

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2025-08-25 15:38:37 -07:00

212 lines
5.8 KiB
TypeScript

import React, { useCallback, useContext, useState } from "react";
import { InjectedRouter } from "react-router";
import classnames from "classnames";
import { IActivityDetails } from "interfaces/activity";
import { NotificationContext } from "context/notification";
import Modal from "components/Modal";
import DataSet from "components/DataSet";
import { dateAgo } from "utilities/date_format";
import TooltipWrapper from "components/TooltipWrapper";
import { useQuery } from "react-query";
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
import scriptsAPI, {
IScriptBatchSummaryQueryKey,
IScriptBatchSummaryResponseV1,
} from "services/entities/scripts";
import { AxiosError } from "axios";
import Spinner from "components/Spinner";
import DataError from "components/DataError";
import Button from "components/buttons/Button";
import ScriptBatchStatusTable from "../ScriptBatchStatusTable";
const baseClass = "script-batch-summary-modal";
export type IScriptBatchDetailsForSummary = Pick<
IActivityDetails,
"batch_execution_id" | "created_at" | "script_name" | "host_count"
>;
interface IScriptBatchSummaryModal {
scriptBatchExecutionDetails: IScriptBatchDetailsForSummary;
onCancel: () => void;
router: InjectedRouter;
}
const ScriptBatchSummaryModal = ({
scriptBatchExecutionDetails: details,
onCancel,
router,
}: IScriptBatchSummaryModal) => {
const [showCancelModal, setShowCancelModal] = useState(false);
const [isCanceling, setIsCanceling] = useState(false);
const { data: statusData, isLoading, isError } = useQuery<
IScriptBatchSummaryResponseV1,
AxiosError,
IScriptBatchSummaryResponseV1,
IScriptBatchSummaryQueryKey[]
>(
[
{
scope: "script_batch_summary",
batch_execution_id: details.batch_execution_id || "",
},
],
({ queryKey: [{ batch_execution_id }] }) =>
scriptsAPI.getRunScriptBatchSummary({ batch_execution_id }),
{
enabled: details.batch_execution_id !== undefined,
...DEFAULT_USE_QUERY_OPTIONS,
}
);
const toggleCancelModal = () => {
setShowCancelModal(!showCancelModal);
};
const renderTable = () => {
if (
!details.batch_execution_id ||
isLoading ||
!statusData ||
isCanceling
) {
return <Spinner />;
}
if (isError) {
return <DataError />;
}
return (
<ScriptBatchStatusTable
statusData={statusData}
batchExecutionId={details.batch_execution_id || ""}
onClickCancel={toggleCancelModal}
/>
);
};
let activityCreatedAt: Date | null = null;
if (details?.created_at) {
try {
activityCreatedAt = new Date(details?.created_at || "");
} catch (e) {
// invalid date string
activityCreatedAt = null;
}
}
const targetedTitle = (
<TooltipWrapper
tipContent="The number of hosts originally targeted,
including those where scripts were
incompatible or cancelled."
>
Targeted
</TooltipWrapper>
);
const { renderFlash } = useContext(NotificationContext);
const onConfirmCancel = useCallback(
async (batchExecutionId: string) => {
setIsCanceling(true);
try {
await scriptsAPI.cancelScriptBatch(batchExecutionId);
renderFlash("success", "Successfully canceled script.");
setShowCancelModal(false);
onCancel();
} catch (error) {
renderFlash("error", "Could not cancel script. Please try again.");
} finally {
setIsCanceling(false);
}
},
[renderFlash]
);
const renderCancelModal = () => {
const cancelBaseClass = "script-batch-cancel-modal";
if (!statusData) {
// the conditions for triggering the cancel modal mean this will never be the case. This is
// for the TS compiler
return null;
}
return (
<Modal
title="Cancel script?"
onExit={toggleCancelModal}
onEnter={toggleCancelModal}
className={cancelBaseClass}
>
<>
<div className={`${cancelBaseClass}__content`}>
<p>
This will cancel any pending script runs for{" "}
<b>{details?.script_name || "this script"}</b>.
</p>
<p>
If this script is currently running on a host, it will complete,
but results won&rsquo;t appear in Fleet.
</p>
<p>You cannot undo this action.</p>
<div className="modal-cta-wrap">
<Button
isLoading={isCanceling}
disabled={isCanceling}
onClick={() =>
onConfirmCancel(details.batch_execution_id || "")
}
variant="alert"
>
Cancel script
</Button>
<Button variant="inverse-alert" onClick={toggleCancelModal}>
Back
</Button>
</div>
</div>
</>
</Modal>
);
};
const parentModalClasses = classnames(baseClass, {
[`${baseClass}__hide-main`]: showCancelModal,
});
return (
<>
<Modal
// script_name will always be present at this point
title={details?.script_name || "Script Batch Summary"}
onExit={onCancel}
onEnter={onCancel}
className={parentModalClasses}
>
<div className={`${baseClass}__modal-content`}>
<div className="header">
{activityCreatedAt && (
<DataSet title="Ran" value={dateAgo(activityCreatedAt)} />
)}
<DataSet title={targetedTitle} value={details.host_count} />
</div>
{renderTable()}
<div className="modal-cta-wrap">
<Button onClick={onCancel}>Done</Button>
</div>
</div>
</Modal>
{showCancelModal && renderCancelModal()}
</>
);
};
export default ScriptBatchSummaryModal;