mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
241 lines
6.8 KiB
TypeScript
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;
|