fleet/frontend/services/entities/software.ts
Gabriel Hernandez a9f4749054
Add count and apps_updated_at data to GET /fleet_maintained_apps and UI (#22778)
relates to #22732

This adds the `counts` and `apps_updated_at` response data on the `GET
/fleet_maintained_apps` endpoint. We then add that to the UI to show the
counts and last time fleet maintained app data has been updated:


![image](https://github.com/user-attachments/assets/bb4d3819-2274-4782-a56b-b1868122cce0)


- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
2024-10-09 15:49:06 +01:00

385 lines
11 KiB
TypeScript

import { AxiosProgressEvent } from "axios";
import sendRequest, { sendRequestWithProgress } from "services";
import endpoints from "utilities/endpoints";
import {
ISoftwareResponse,
ISoftwareCountResponse,
ISoftwareVersion,
ISoftwareTitle,
ISoftwareTitleDetails,
IFleetMaintainedApp,
IFleetMaintainedAppDetails,
} from "interfaces/software";
import {
buildQueryStringFromParams,
convertParamsToSnakeCase,
} from "utilities/url";
import { IPackageFormData } from "pages/SoftwarePage/components/PackageForm/PackageForm";
import { IAddFleetMaintainedData } from "pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage";
export interface ISoftwareApiParams {
page?: number;
perPage?: number;
orderKey?: string;
orderDirection?: "asc" | "desc";
query?: string;
vulnerable?: boolean;
max_cvss_score?: number;
min_cvss_score?: number;
exploit?: boolean;
availableForInstall?: boolean;
packagesOnly?: boolean;
selfService?: boolean;
teamId?: number;
}
export interface ISoftwareTitlesResponse {
counts_updated_at: string | null;
count: number;
software_titles: ISoftwareTitle[];
meta: {
has_next_results: boolean;
has_previous_results: boolean;
};
}
export interface ISoftwareVersionsResponse {
counts_updated_at: string | null;
count: number;
software: ISoftwareVersion[];
meta: {
has_next_results: boolean;
has_previous_results: boolean;
};
}
export interface ISoftwareTitleResponse {
software_title: ISoftwareTitleDetails;
}
export interface ISoftwareVersionResponse {
software: ISoftwareVersion;
}
export interface ISoftwareVersionsQueryKey extends ISoftwareApiParams {
// used to trigger software refetches from sibling pages
addedSoftwareToken: string | null;
scope: "software-versions";
}
export interface ISoftwareTitlesQueryKey extends ISoftwareApiParams {
// used to trigger software refetches from sibling pages
addedSoftwareToken?: string | null;
scope: "software-titles";
}
export interface ISoftwareQueryKey extends ISoftwareApiParams {
scope: "software";
}
export interface ISoftwareCountQueryKey
extends Pick<ISoftwareApiParams, "query" | "vulnerable" | "teamId"> {
scope: "softwareCount";
}
export interface IGetSoftwareTitleQueryParams {
softwareId: number;
teamId?: number;
}
export interface IGetSoftwareTitleQueryKey
extends IGetSoftwareTitleQueryParams {
scope: "softwareById";
}
export interface IGetSoftwareVersionQueryParams {
versionId: number;
teamId?: number;
}
export interface IGetSoftwareVersionQueryKey
extends IGetSoftwareVersionQueryParams {
scope: "softwareVersion";
}
export interface ISoftwareInstallTokenResponse {
token: string;
}
export interface ISoftwareFleetMaintainedAppsQueryParams {
team_id: number;
query?: string;
order_key?: string;
order_direction?: "asc" | "desc";
page?: number;
per_page?: number;
}
export interface ISoftwareFleetMaintainedAppsResponse {
fleet_maintained_apps: IFleetMaintainedApp[];
count: number;
apps_updated_at: string | null;
meta: {
has_next_results: boolean;
has_previous_results: boolean;
};
}
export interface IFleetMaintainedAppResponse {
fleet_maintained_app: IFleetMaintainedAppDetails;
}
interface IAddFleetMaintainedAppPostBody {
team_id: number;
fleet_maintained_app_id: number;
pre_install_query?: string;
install_script?: string;
post_install_script?: string;
uninstall_script?: string;
self_service?: boolean;
}
const ORDER_KEY = "name";
const ORDER_DIRECTION = "asc";
export const MAX_FILE_SIZE_MB = 3000;
export const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
export default {
load: async ({
page,
perPage,
orderKey = ORDER_KEY,
orderDirection: orderDir = ORDER_DIRECTION,
query,
vulnerable,
// availableForInstall, // TODO: Is this supported for the versions endpoint?
teamId,
}: Omit<
ISoftwareApiParams,
"availableForInstall" | "selfService"
>): Promise<ISoftwareResponse> => {
const { SOFTWARE } = endpoints;
const queryParams = {
page,
perPage,
orderKey,
orderDirection: orderDir,
teamId,
query,
vulnerable,
// availableForInstall,
};
const snakeCaseParams = convertParamsToSnakeCase(queryParams);
const queryString = buildQueryStringFromParams(snakeCaseParams);
const path = `${SOFTWARE}?${queryString}`;
try {
return sendRequest("GET", path);
} catch (error) {
throw error;
}
},
getCount: async ({
query,
teamId,
vulnerable,
}: Pick<
ISoftwareApiParams,
"query" | "teamId" | "vulnerable"
>): Promise<ISoftwareCountResponse> => {
const { SOFTWARE } = endpoints;
const path = `${SOFTWARE}/count`;
const queryParams = {
query,
teamId,
vulnerable,
};
const snakeCaseParams = convertParamsToSnakeCase(queryParams);
const queryString = buildQueryStringFromParams(snakeCaseParams);
return sendRequest("GET", path.concat(`?${queryString}`));
},
getSoftwareTitles: (
params: ISoftwareApiParams
): Promise<ISoftwareTitlesResponse> => {
const { SOFTWARE_TITLES } = endpoints;
const snakeCaseParams = convertParamsToSnakeCase(params);
const queryString = buildQueryStringFromParams(snakeCaseParams);
const path = `${SOFTWARE_TITLES}?${queryString}`;
return sendRequest("GET", path);
},
getSoftwareTitle: ({
softwareId,
teamId,
}: IGetSoftwareTitleQueryParams): Promise<ISoftwareTitleResponse> => {
const endpoint = endpoints.SOFTWARE_TITLE(softwareId);
const queryString = buildQueryStringFromParams({ team_id: teamId });
const path =
typeof teamId === "undefined" ? endpoint : `${endpoint}?${queryString}`;
return sendRequest("GET", path);
},
getSoftwareVersions: (params: ISoftwareApiParams) => {
const { SOFTWARE_VERSIONS } = endpoints;
const snakeCaseParams = convertParamsToSnakeCase(params);
const queryString = buildQueryStringFromParams(snakeCaseParams);
const path = `${SOFTWARE_VERSIONS}?${queryString}`;
return sendRequest("GET", path);
},
getSoftwareVersion: ({
versionId,
teamId,
}: IGetSoftwareVersionQueryParams) => {
const endpoint = endpoints.SOFTWARE_VERSION(versionId);
const queryString = buildQueryStringFromParams({ team_id: teamId });
const path =
typeof teamId === "undefined" ? endpoint : `${endpoint}?${queryString}`;
return sendRequest("GET", path);
},
addSoftwarePackage: ({
data,
teamId,
timeout,
onUploadProgress,
signal,
}: {
data: IPackageFormData;
teamId?: number;
timeout?: number;
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
signal?: AbortSignal;
}) => {
const { SOFTWARE_PACKAGE_ADD } = endpoints;
if (!data.software) {
throw new Error("Software package is required");
}
const formData = new FormData();
formData.append("software", data.software);
formData.append("self_service", data.selfService.toString());
data.installScript && formData.append("install_script", data.installScript);
data.uninstallScript &&
formData.append("uninstall_script", data.uninstallScript);
data.preInstallQuery &&
formData.append("pre_install_query", data.preInstallQuery);
data.postInstallScript &&
formData.append("post_install_script", data.postInstallScript);
teamId && formData.append("team_id", teamId.toString());
return sendRequestWithProgress({
method: "POST",
path: SOFTWARE_PACKAGE_ADD,
data: formData,
timeout,
skipParseError: true,
onUploadProgress,
signal,
});
},
editSoftwarePackage: ({
data,
softwareId,
teamId,
timeout,
onUploadProgress,
signal,
}: {
data: IPackageFormData;
softwareId: number;
teamId: number;
timeout?: number;
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
signal?: AbortSignal;
}) => {
const { EDIT_SOFTWARE_PACKAGE } = endpoints;
const formData = new FormData();
formData.append("team_id", teamId.toString());
data.software && formData.append("software", data.software);
formData.append("self_service", data.selfService.toString());
formData.append("install_script", data.installScript);
formData.append("pre_install_query", data.preInstallQuery || "");
formData.append("post_install_script", data.postInstallScript || "");
formData.append("uninstall_script", data.uninstallScript || "");
return sendRequestWithProgress({
method: "PATCH",
path: EDIT_SOFTWARE_PACKAGE(softwareId),
data: formData,
timeout,
skipParseError: true,
onUploadProgress,
signal,
});
},
deleteSoftwarePackage: (softwareId: number, teamId: number) => {
const { SOFTWARE_AVAILABLE_FOR_INSTALL } = endpoints;
const path = `${SOFTWARE_AVAILABLE_FOR_INSTALL(
softwareId
)}?team_id=${teamId}`;
return sendRequest("DELETE", path);
},
getSoftwarePackageToken: (
softwareTitleId: number,
teamId: number
): Promise<ISoftwareInstallTokenResponse> => {
const path = `${endpoints.SOFTWARE_PACKAGE_TOKEN(
softwareTitleId
)}?${buildQueryStringFromParams({ alt: "media", team_id: teamId })}`;
return sendRequest("POST", path);
},
getSoftwareInstallResult: (installUuid: string) => {
const { SOFTWARE_INSTALL_RESULTS } = endpoints;
const path = SOFTWARE_INSTALL_RESULTS(installUuid);
return sendRequest("GET", path);
},
getFleetMaintainedApps: (
params: ISoftwareFleetMaintainedAppsQueryParams
): Promise<ISoftwareFleetMaintainedAppsResponse> => {
const { SOFTWARE_FLEET_MAINTAINED_APPS } = endpoints;
const queryStr = buildQueryStringFromParams(params);
const path = `${SOFTWARE_FLEET_MAINTAINED_APPS}?${queryStr}`;
return sendRequest("GET", path);
},
getFleetMainainedApp: (id: number): Promise<IFleetMaintainedAppResponse> => {
const { SOFTWARE_FLEET_MAINTAINED_APP } = endpoints;
const path = `${SOFTWARE_FLEET_MAINTAINED_APP(id)}`;
return sendRequest("GET", path);
},
addFleetMaintainedApp: (
teamId: number,
formData: IAddFleetMaintainedData
) => {
const { SOFTWARE_FLEET_MAINTAINED_APPS } = endpoints;
const body: IAddFleetMaintainedAppPostBody = {
team_id: teamId,
fleet_maintained_app_id: formData.appId,
pre_install_query: formData.preInstallQuery,
install_script: formData.installScript,
post_install_script: formData.postInstallScript,
uninstall_script: formData.uninstallScript,
self_service: formData.selfService,
};
return sendRequest("POST", SOFTWARE_FLEET_MAINTAINED_APPS, body);
},
};