fleet/frontend/services/entities/queries.ts
jacobshandling f0d3809b22
UI: Allow editing the name and team of a "Save as new" query (#30544)
## #14801 
### [Demo
video](https://drive.google.com/file/d/1Lovk7iwvgUv1NpfsqSt-Is0yTBt0SZ5O/view?usp=sharing)
<img width="1624" alt="Screenshot 2025-07-02 at 4 58 33 PM"
src="https://github.com/user-attachments/assets/86c7b214-e8e4-4e58-9969-b1373ed97691"
/>


* **New Features**
* Added the ability to select a team and update the name when saving a
query as a new copy, using a dedicated modal dialog.

* **Improvements**
* Enhanced the team selection dropdown with new styling options and
clarified prop names.
* Updated query editing workflow to use a modal for "Save as new"
actions.
* Improved type safety and clarity in several interfaces and utility
functions.

* **Bug Fixes**
  * Fixed inconsistencies in prop naming for team dropdown components.
* Ensured "Discard data" setting is maintained when "Save as new"ing a
query - it was previously not maintained correctly

* **Tests**
* Updated and removed tests to align with the new "Save as new" query
workflow and prop changes.
  * Added utilities for creating mock location objects in tests.

* **Style**
  * Added a new light grey color to the UI color palette.

- [x] Changes file added for user-visible changes in `changes/`
- [x] Added/updated automated tests
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2025-07-03 13:11:06 -07:00

142 lines
3.6 KiB
TypeScript

/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import sendRequest from "services";
import endpoints from "utilities/endpoints";
import { getErrorReason } from "interfaces/errors";
import { ISelectedTargetsForApi } from "interfaces/target";
import {
ICreateQueryRequestBody,
IModifyQueryRequestBody,
IQueryKeyQueriesLoadAll,
ISchedulableQuery,
} from "interfaces/schedulable_query";
import {
buildQueryStringFromParams,
convertParamsToSnakeCase,
} from "utilities/url";
import { SelectedPlatform } from "interfaces/platform";
export interface ILoadQueriesParams {
teamId?: number;
page?: number;
perPage?: number;
query?: string;
orderDirection?: "asc" | "desc";
orderKey?: string;
mergeInherited?: boolean;
targetedPlatform?: SelectedPlatform;
}
export interface IQueryKeyLoadQueries extends ILoadQueriesParams {
scope: "queries";
}
interface ICreateQueryResponse {
query: ISchedulableQuery;
}
export interface IQueriesResponse {
queries: ISchedulableQuery[];
count: number;
meta: {
has_next_results: boolean;
has_previous_results: boolean;
};
}
export default {
create: (
createQueryRequestBody: ICreateQueryRequestBody
): Promise<ICreateQueryResponse> => {
const { QUERIES } = endpoints;
if (createQueryRequestBody.name) {
createQueryRequestBody.name = createQueryRequestBody.name.trim();
}
return sendRequest("POST", QUERIES, createQueryRequestBody);
},
destroy: (id: string | number) => {
const { QUERIES } = endpoints;
const path = `${QUERIES}/id/${id}`;
return sendRequest("DELETE", path);
},
bulkDestroy: (ids: number[]) => {
const { QUERIES } = endpoints;
const path = `${QUERIES}/delete`;
return sendRequest("POST", path, { ids });
},
load: (id: number) => {
const { QUERIES } = endpoints;
const path = `${QUERIES}/${id}`;
return sendRequest("GET", path);
},
loadAll: ({
teamId,
page,
perPage,
query,
orderDirection,
orderKey,
mergeInherited,
// FE logic uses less ambiguous `targetedPlatform`, while API expects `platform` for alignment
// with other API conventions and database `queries.platform` column
targetedPlatform: platform,
}: IQueryKeyQueriesLoadAll): Promise<IQueriesResponse> => {
const { QUERIES } = endpoints;
const snakeCaseParams = convertParamsToSnakeCase({
teamId,
page,
perPage,
query,
orderDirection,
orderKey,
mergeInherited,
platform,
});
// API expects "macos" instead of "darwin"
if (snakeCaseParams.platform === "darwin") {
snakeCaseParams.platform = "macos";
}
const queryString = buildQueryStringFromParams(snakeCaseParams);
return sendRequest(
"GET",
queryString ? QUERIES.concat(`?${queryString}`) : QUERIES
);
},
run: async ({
query,
queryId,
selected,
}: {
query: string;
queryId: number | null;
selected: ISelectedTargetsForApi;
}) => {
const { LIVE_QUERY } = endpoints;
try {
const { campaign } = await sendRequest("POST", LIVE_QUERY, {
query,
query_id: queryId,
selected,
});
return campaign;
} catch (e) {
throw new Error(
getErrorReason(e) || `run query: parse server error ${e}`
);
}
},
update: (id: number, updateParams: IModifyQueryRequestBody) => {
const { QUERIES } = endpoints;
const path = `${QUERIES}/${id}`;
if (updateParams.name) {
updateParams.name = updateParams.name.trim();
}
return sendRequest("PATCH", path, updateParams);
},
};