fleet/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tsx
Scott Gress 2686907dba
Update API calls in front-end to use new, non-deprecated URLs and params (#41515)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #41391

# Details

This PR updates front-end API calls to use new URLs and API params, so
that the front end doesn't cause deprecation warnings to appear on the
server.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
n/a, should not be user-visible

## Testing

- [X] Added/updated automated tests
- [ ] QA'd all new/changed functionality manually
The biggest risk here is not that we missed a spot that still causes a
deprecation warning, but that we might inadvertently make a change that
breaks the front end, for instance by sending `fleet_id` to a function
that drops it silently and thus sends no ID to the server. Fortunately
we use TypeScript in virtually every place affected by these changes, so
the code would not compile if there were mismatches between the API
expectation and what we're sending. Still, spot checking as many places
as possible both for deprecation-warning leaks and loss of functionality
is important.

## Summary by CodeRabbit

* **Refactor**
* Updated API nomenclature across the application to use "fleets"
instead of "teams" and "reports" instead of "queries" in endpoint paths
and request/response payloads.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-12 22:26:48 -05:00

239 lines
6.7 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 { 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 (
<div className={`${baseClass}__empty`}>
<b>No batch scripts {status} for this fleet</b>
<p>{EMPTY_STATE_DETAILS[status]}</p>
</div>
);
};
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;