fleet/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tsx
2026-04-27 17:54:34 -05:00

241 lines
6.8 KiB
TypeScript

import React, { useCallback, useEffect, useRef, useState } from "react";
import { useQuery } from "react-query";
import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
import PATHS from "router/paths";
import scriptsAPI, {
IScriptBatchSummaryV2,
IScriptBatchSummariesResponse,
} from "services/entities/scripts";
import { isValidScriptBatchStatus, ScriptBatchStatus } from "interfaces/script";
import { COLORS } from "styles/var/colors";
import Spinner from "components/Spinner";
import ProgressBar from "components/ProgressBar";
import SectionHeader from "components/SectionHeader";
import TabNav from "components/TabNav";
import TabText from "components/TabText";
import PaginatedList, { IPaginatedListHandle } from "components/PaginatedList";
import ListItem from "components/ListItem";
import Icon from "components/Icon/Icon";
import EmptyState from "components/EmptyState";
import { IScriptsCommonProps } from "../../ScriptsNavItems";
import getWhen from "../../helpers";
const baseClass = "script-batch-progress";
const STATUS_BY_INDEX: ScriptBatchStatus[] = [
"started",
"scheduled",
"finished",
];
export const EMPTY_STATE_DETAILS: Record<ScriptBatchStatus, string> = {
started: "When a script is run on multiple hosts, progress will appear here.",
scheduled:
"When a script is scheduled to run in the future, it will appear here.",
finished:
"When a batch script is completed or canceled, historical results will appear here.",
};
const getEmptyState = (status: ScriptBatchStatus) => {
return (
<EmptyState
variant="list"
header={`No batch scripts ${status} for this fleet`}
info={EMPTY_STATE_DETAILS[status]}
/>
);
};
export type IScriptBatchProgressProps = IScriptsCommonProps;
const ScriptBatchProgress = ({
location,
router,
teamId,
}: IScriptBatchProgressProps) => {
const [pageNumber, setPageNumber] = useState(0);
const paginatedListRef = useRef<IPaginatedListHandle<IScriptBatchSummaryV2>>(
null
);
const statusParam = location?.query.status;
const selectedStatus = statusParam as ScriptBatchStatus;
const DEFAULT_PAGE_SIZE = 10;
const queryKey = {
fleet_id: teamId,
status: selectedStatus,
page: pageNumber,
per_page: DEFAULT_PAGE_SIZE,
};
const { data, isFetching: updating } = useQuery<
IScriptBatchSummariesResponse,
Error,
IScriptBatchSummariesResponse
>([queryKey], () => scriptsAPI.getRunScriptBatchSummaries(queryKey), {
keepPreviousData: true,
});
const handleTabChange = useCallback(
(index: number) => {
const newStatus = STATUS_BY_INDEX[index];
const newParams = new URLSearchParams(location?.search);
newParams.set("status", newStatus);
const newQuery = newParams.toString();
router.push(
PATHS.CONTROLS_SCRIPTS_BATCH_PROGRESS.concat(
newQuery ? `?${newQuery}` : ""
)
);
setPageNumber(0);
},
[location?.search, router]
);
const onClickRow = (r: IScriptBatchSummaryV2) => {
// explicitly including the status param here avoids triggering the script details page's effect
// which would add it automatically, muddying browser history and preventing smooth forward/back navigation
router.push(
PATHS.CONTROLS_SCRIPTS_BATCH_DETAILS(r.batch_execution_id).concat(
"?status=ran"
)
);
return r;
};
const renderRow = (summary: IScriptBatchSummaryV2) => {
const {
script_name,
targeted_host_count,
ran_host_count,
errored_host_count,
} = summary;
const when = getWhen(summary);
return (
<>
<ListItem title={script_name} details={when} />
{summary.status !== "scheduled" && (
<div className={`${baseClass}__row-right`}>
<div className={`${baseClass}__status-count`}>
{ran_host_count + errored_host_count} / {targeted_host_count}{" "}
hosts
</div>
<ProgressBar
sections={[
{
// results
color: COLORS["status-success"],
portion: ran_host_count / targeted_host_count,
},
{
// errors
color: COLORS["status-error"],
portion: errored_host_count / targeted_host_count,
},
]}
/>
<div className={`${baseClass}__row-errors`}>
<Icon
name="error-outline"
color="ui-fleet-black-50"
size="small"
/>{" "}
<div>{errored_host_count}</div>
</div>
</div>
)}
</>
);
};
// Reset to first tab if status is invalid.
useEffect(() => {
if (!isValidScriptBatchStatus(statusParam)) {
handleTabChange(0);
}
}, [statusParam, handleTabChange]);
const renderTabContent = (status: ScriptBatchStatus) => {
// If we're switching to a new tab, show the loading spinner
// while we get the first page and # of results.
if (updating) {
return (
<div className={`${baseClass}__loading`}>
<Spinner />
</div>
);
}
const count = data?.count || 0;
const rows = data?.batch_executions || [];
if (count === 0) {
return getEmptyState(status);
}
return (
<div className={`${baseClass}__tab-content`}>
{!updating && count && (
<div className={`${baseClass}__status-count`}>
{count} batch script{count > 1 ? "s" : ""}
</div>
)}
<PaginatedList<IScriptBatchSummaryV2>
ref={paginatedListRef}
count={count}
data={rows}
pageSize={DEFAULT_PAGE_SIZE}
currentPage={pageNumber}
onChangePage={setPageNumber}
isLoading={updating}
onClickRow={onClickRow}
renderItemRow={renderRow}
useCheckBoxes={false}
/>
</div>
);
};
return (
<>
<div className={baseClass}>
<SectionHeader title="Batch progress" alignLeftHeaderVertically />
<TabNav secondary>
<Tabs
selectedIndex={STATUS_BY_INDEX.indexOf(selectedStatus)}
onSelect={handleTabChange}
>
<TabList>
<Tab>
<TabText>Started</TabText>
</Tab>
<Tab>
<TabText>Scheduled</TabText>
</Tab>
<Tab>
<TabText>Finished</TabText>
</Tab>
</TabList>
<TabPanel>{renderTabContent(STATUS_BY_INDEX[0])}</TabPanel>
<TabPanel>{renderTabContent(STATUS_BY_INDEX[1])}</TabPanel>
<TabPanel>{renderTabContent(STATUS_BY_INDEX[2])}</TabPanel>
</Tabs>
</TabNav>
</div>
</>
);
};
export default ScriptBatchProgress;