fleet/frontend/services/entities/scripts.ts
jacobshandling 166e5ed663
UI: Batch script run detail page (#32333)
## For #31226 

New features:
- Dynamic header for each possible state of a batch script run: Started,
Scheduled, and Finished (corresponds to tabs at
`/controls/scripts/progress`
- Unique tabs for each possible status of hosts targeted by a batch
script run: Ran, Errored, Pending, Incompatible, Canceled.
- Within each tab, sortable, paginated host results with output preview
and execution time.
- View script/run details, cancel a batch, view manage hosts page
filtered for the script batch run and a status.
- Global script batch runs activities and and Scripts progress rows now
navigate to this details page.

Cleanups and improvements:
- Expand tab count badge options using “alert”/“pending” variants across
hosts, policies, and query results.
- Misc cleanups and improvements


![ezgif-1438d4041f694f](https://github.com/user-attachments/assets/2d93127b-dea4-4ca6-abcc-7c888b2e0b93)


- [x] Changes file added for user-visible changes in `changes/`,


- [x] Updated automated tests - new tests tracked for follow-up work
- [x] QA'd all new/changed functionality manually

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2025-08-29 09:37:05 -06:00

355 lines
9.9 KiB
TypeScript

import {
IHostScript,
IScript,
ScriptBatchHostStatus,
ScriptBatchStatus,
} from "interfaces/script";
import sendRequest from "services";
import {
createMockBatchScriptSummary,
createMockScriptBatchHostResults,
} from "__mocks__/scriptMock";
import endpoints from "utilities/endpoints";
import { buildQueryStringFromParams } from "utilities/url";
import {
ListEntitiesResponseCommon,
OrderDirection,
PaginationParams,
} from "./common";
/** Single script response from GET /script/:id */
export type IScriptResponse = IScript;
/** All scripts response from GET /scripts */
export interface IScriptsResponse {
scripts: IScript[] | null;
meta: {
has_next_results: boolean;
has_previous_results: boolean;
};
}
export interface IListScriptsApiParams {
page?: number;
per_page?: number;
team_id?: number;
}
export interface IListScriptsQueryKey extends IListScriptsApiParams {
scope: "scripts";
}
/**
* Script Result response from GET /scripts/results/:id
*/
export interface IScriptResultResponse {
hostname: string;
host_id: number;
execution_id: string;
script_contents: string;
script_id: number | null; // null for ad-hoc script run via API
exit_code: number | null;
output: string;
message: string;
runtime: number;
host_timeout: boolean;
created_at: string;
}
/**
* Request params for for GET /hosts/:id/scripts
*/
export interface IHostScriptsRequestParams {
host_id: number;
page?: number;
per_page?: number;
}
export interface IHostScriptsQueryKey extends IHostScriptsRequestParams {
scope: "host_scripts";
}
/**
* Script response from GET /hosts/:id/scripts
*/
export interface IHostScriptsResponse {
scripts: IHostScript[];
meta: {
has_next_results: boolean;
has_previous_results: boolean;
};
}
/**
* Request body for POST /scripts/run
*
* https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/api-for-contributors.md#run-script-asynchronously
*/
export interface IScriptRunRequest {
host_id: number;
script_id: number; // script_id is not required by the API currently, but we require it here to ensure it is always provided
// script_contents: string; // script_contents is only supported for the CLI currently
}
/**
* Response body for POST /scripts/run
*
* https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/api-for-contributors.md#run-script-asynchronously
*/
export interface IScriptRunResponse {
host_id: number;
execution_id: string;
}
export interface IScriptBatchSupportedFilters {
// a search string, not a Fleet.Query
query?: string;
label_id?: number;
team_id?: number;
status: any; // TODO - improve upstream typing
}
interface IRunScriptBatchRequestBase {
script_id: number;
not_before?: string; // ISO 8601 date-time string
}
interface IByFilters extends IRunScriptBatchRequestBase {
host_ids?: never;
filters: IScriptBatchSupportedFilters;
}
interface IByHostIds extends IRunScriptBatchRequestBase {
host_ids: number[];
filters?: never;
}
/** Request body for POST /scripts/run/batch */
export type IRunScriptBatchRequest = IByFilters | IByHostIds;
/** 202 successful response body for POST /scripts/run/batch */
export interface IRunScriptBatchResponse {
batch_execution_id: string;
}
export interface IScriptBatchSummaryParams {
batch_execution_id: string;
}
export interface IScriptBatchSummaryQueryKey extends IScriptBatchSummaryParams {
scope: "script_batch_summary";
}
export interface IScriptBatchHostCountsV1 {
ran: number;
pending: number;
errored: number;
canceled: number;
}
export type ScriptBatchHostCountV1 = keyof IScriptBatchHostCountsV1;
// 200 successful response
export interface IScriptBatchSummaryV1 extends IScriptBatchHostCountsV1 {
team_id: number;
script_name: string;
created_at: string;
// below fields not yet used by the UI
targeted: number;
script_id: number;
}
export interface IScriptBatchHostCountsV2 {
targeted_host_count: number;
ran_host_count: number;
errored_host_count: number;
pending_host_count: number;
incompatible_host_count: number;
canceled_host_count: number;
}
export interface IScriptBatchSummaryV2 extends IScriptBatchHostCountsV2 {
batch_execution_id: string;
/** ISO 8601 date-time string. When the script batch run was created (NOT when it is/was scheduled
* to run by, which is represented by `not_before`. */
created_at: string;
script_id: number;
script_name: string;
team_id: number;
status: ScriptBatchStatus;
canceled: boolean;
/** ISO 8601 date-time string. Always present as of Fleet 4.73.0 - `null`able for backwards compatibility with older batch runs. */
not_before: string | null;
// /** ISO 8601 date-time string. If present, this script batch run has started. */
started_at: string | null;
/** ISO 8601 date-time string. If present, this script has completed running. */
finished_at: string | null;
}
export interface IScriptBatchSummariesParams {
team_id: number;
status: ScriptBatchStatus;
page: number;
per_page: number;
}
export interface IScriptBatchSummariesQueryKey
extends IScriptBatchSummariesParams {
scope: "script_batch_summaries";
}
export interface IScriptBatchSummariesResponse
extends ListEntitiesResponseCommon {
batch_executions: IScriptBatchSummaryV2[] | null; // should not return `null`, but API currently does sometimes. Remove this option when it's fixed.
}
export type ScriptBatchHostsOrderKey = "display_name" | "script_executed_at";
export interface IScriptBatchHostResultsParams extends PaginationParams {
batch_execution_id: string;
status: ScriptBatchHostStatus;
order_key: ScriptBatchHostsOrderKey;
order_direction: OrderDirection;
}
export interface IScriptBatchHostResultsQueryKey
extends IScriptBatchHostResultsParams {
scope: "script_batch_host_results";
}
export interface IScriptBatchHostResult {
id: number;
display_name: string;
script_status: ScriptBatchHostStatus;
script_execution_id: string | null; // if status === pending, this may be `null` or contain a value dependending on whether the script exectution is this hosts next scheduled activity on the server
// /** ISO 8601 date-time string. `null` if pending, cancelled, or incompatible. */
script_executed_at: string | null;
/** `null` if pending, cancelled, or incompatible. */
script_output_preview: string | null;
}
export interface IScriptBatchHostResultsResponse
extends ListEntitiesResponseCommon {
hosts: IScriptBatchHostResult[];
}
export default {
getHostScripts({ host_id, page, per_page }: IHostScriptsRequestParams) {
const { HOST_SCRIPTS } = endpoints;
const path = `${HOST_SCRIPTS(host_id)}?${buildQueryStringFromParams({
page,
per_page,
})}`;
return sendRequest("GET", path);
},
getScripts(params: IListScriptsApiParams): Promise<IScriptsResponse> {
const { SCRIPTS } = endpoints;
const path = `${SCRIPTS}?${buildQueryStringFromParams({ ...params })}`;
return sendRequest("GET", path);
},
getScript(id: number) {
const { SCRIPT } = endpoints;
return sendRequest("GET", SCRIPT(id));
},
uploadScript(file: File, teamId?: number) {
const { SCRIPTS } = endpoints;
const formData = new FormData();
formData.append("script", file);
if (teamId) {
formData.append("team_id", teamId.toString());
}
return sendRequest("POST", SCRIPTS, formData);
},
downloadScript(id: number) {
const { SCRIPT } = endpoints;
const path = `${SCRIPT(id)}?${buildQueryStringFromParams({
alt: "media",
})}`;
return sendRequest("GET", path);
},
updateScript(id: number, contents: string, name: string) {
const { SCRIPT } = endpoints;
const path = `${SCRIPT(id)}`;
const file = new File([contents], name);
const formData = new FormData();
formData.append("script", file);
return sendRequest("PATCH", path, formData);
},
deleteScript(id: number) {
const { SCRIPT } = endpoints;
return sendRequest("DELETE", SCRIPT(id));
},
getScriptResult(executionId: string) {
const { SCRIPT_RESULT } = endpoints;
return sendRequest("GET", SCRIPT_RESULT(executionId));
},
runScript(request: IScriptRunRequest): Promise<IScriptRunResponse> {
const { SCRIPT_RUN } = endpoints;
return sendRequest("POST", SCRIPT_RUN, request);
},
runScriptBatch(
request: IRunScriptBatchRequest
): Promise<IRunScriptBatchResponse> {
const { SCRIPT_RUN_BATCH } = endpoints;
return sendRequest("POST", SCRIPT_RUN_BATCH, request);
},
cancelScriptBatch(batchExecutionId: string) {
const { SCRIPT_CANCEL_BATCH } = endpoints;
return sendRequest("POST", SCRIPT_CANCEL_BATCH(batchExecutionId));
},
/** calls the deprecated endpoint */
getRunScriptBatchSummaryV1({
batch_execution_id,
}: IScriptBatchSummaryParams): Promise<IScriptBatchSummaryV1> {
return sendRequest(
"GET",
`${endpoints.SCRIPT_RUN_BATCH_SUMMARY_V1(batch_execution_id)}`
);
},
getRunScriptBatchSummaryV2({
batch_execution_id,
}: IScriptBatchSummaryParams): Promise<IScriptBatchSummaryV2> {
return sendRequest(
"GET",
`${endpoints.SCRIPT_RUN_BATCH_SUMMARY_V2(batch_execution_id)}`
);
},
getRunScriptBatchSummaries(
params: IScriptBatchSummariesParams
): Promise<IScriptBatchSummariesResponse> {
const path = `${
endpoints.SCRIPT_RUN_BATCH_SUMMARIES
}?${buildQueryStringFromParams({ ...params })}`;
return sendRequest("GET", path);
},
getScriptBatchHostResults(
params: IScriptBatchHostResultsParams
): Promise<IScriptBatchHostResultsResponse> {
const {
batch_execution_id,
status,
page,
per_page,
order_key,
order_direction,
} = params;
const path = `${endpoints.SCRIPT_BATCH_HOST_RESULTS(
batch_execution_id
)}?${buildQueryStringFromParams({
status,
page,
per_page,
order_key: order_key === "script_executed_at" ? "updated_at" : order_key, // map to server field name
order_direction,
})}`;
return sendRequest("GET", path);
},
};