fleet/frontend/pages/queries/edit/components/SaveAsNewQueryModal/SaveAsNewQueryModal.tsx

234 lines
6.3 KiB
TypeScript
Raw Normal View History

import React, { useCallback, useContext, useState } from "react";
import { InjectedRouter } from "react-router";
import { Location } from "history";
import { AppContext } from "context/app";
import PATHS from "router/paths";
import { getPathWithQueryParams } from "utilities/url";
import { ICreateQueryRequestBody } from "interfaces/schedulable_query";
import queryAPI from "services/entities/queries";
import { NotificationContext } from "context/notification";
import { getErrorReason } from "interfaces/errors";
import {
INVALID_PLATFORMS_FLASH_MESSAGE,
INVALID_PLATFORMS_REASON,
} from "utilities/constants";
import {
API_ALL_TEAMS_ID,
APP_CONTEXT_ALL_TEAMS_ID,
ITeamSummary,
} from "interfaces/team";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import InputField from "components/forms/fields/InputField";
import TeamsDropdown from "components/TeamsDropdown";
import { useTeamIdParam } from "hooks/useTeamIdParam";
const baseClass = "save-as-new-query-modal";
interface ISaveAsNewQueryModal {
router: InjectedRouter;
location: Location;
initialQueryData: ICreateQueryRequestBody;
Surface queries in host details (#37646) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #27322 [Figma](https://www.figma.com/design/v7WjL5zQuFIZerWYaSwy8o/-27322-Surface-custom-host-vitals?node-id=5636-4950&t=LuE3Kp09a5sj24Tt-0) ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually (WIP) ## Screenshots ### Host details <img width="1481" height="1000" alt="Screenshot 2025-12-26 at 2 14 48 PM" src="https://github.com/user-attachments/assets/3d9f02f9-f3a7-4a06-b3e4-414bb7b56e25" /> - `Queries` tab removed. - Shows `Queries` card. #### Queries Card - Added client-side pagination. - Added `Add query` button (screenshots below are with `Admin` role). <img width="710" height="395" alt="Screenshot 2025-12-26 at 2 15 07 PM" src="https://github.com/user-attachments/assets/b4e58269-d1b2-4c87-abfa-2cdfe47b533e" /> <img width="723" height="301" alt="Screenshot 2025-12-26 at 2 15 00 PM" src="https://github.com/user-attachments/assets/2615d5bf-5d75-4e83-bc69-bc884232bf32" /> - As an `Observer`, `Add query` is not displayed <img width="2240" height="1077" alt="Screenshot 2025-12-26 at 2 27 25 PM" src="https://github.com/user-attachments/assets/426de709-d2ce-4bef-96f1-919ad5bddb13" /> - As a `Maintainer`, `Add query` is displayed <img width="2236" height="1084" alt="Screenshot 2025-12-26 at 2 31 16 PM" src="https://github.com/user-attachments/assets/218b0d18-2536-4336-88c8-41e7d09a5e9e" /> ### New query page If the user navigates from `Host details`, `host_id` search parameter is added to the URL and the back button displays `Back to host details`. <img width="1097" height="506" alt="Screenshot 2025-12-26 at 2 15 32 PM" src="https://github.com/user-attachments/assets/61777c85-22f5-49dc-a3e6-dcd706119c70" /> ### Host Queries (/hosts/:hostId/queries/:queryId) `Performance impact` added above the table. <img width="2029" height="626" alt="Screenshot 2025-12-26 at 2 16 00 PM" src="https://github.com/user-attachments/assets/05c6b1bc-0587-4b0a-8167-142787592c6d" /> <img width="1555" height="482" alt="Screenshot 2025-12-26 at 2 16 05 PM" src="https://github.com/user-attachments/assets/b9035b63-51c3-46c0-a903-c16d54c22986" />
2026-01-02 13:06:12 +00:00
hostId?: number;
onExit: () => void;
}
interface ISANQFormData {
queryName: string;
team: Partial<ITeamSummary>;
}
interface ISANQFormErrors {
queryName?: string;
team?: string;
}
const validateFormData = (formData: ISANQFormData): ISANQFormErrors => {
const errors: ISANQFormErrors = {};
if (!formData.queryName || formData.queryName.trim() === "") {
errors.queryName = "Name must be present";
}
return errors;
};
const SaveAsNewQueryModal = ({
router,
location,
initialQueryData,
Surface queries in host details (#37646) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #27322 [Figma](https://www.figma.com/design/v7WjL5zQuFIZerWYaSwy8o/-27322-Surface-custom-host-vitals?node-id=5636-4950&t=LuE3Kp09a5sj24Tt-0) ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually (WIP) ## Screenshots ### Host details <img width="1481" height="1000" alt="Screenshot 2025-12-26 at 2 14 48 PM" src="https://github.com/user-attachments/assets/3d9f02f9-f3a7-4a06-b3e4-414bb7b56e25" /> - `Queries` tab removed. - Shows `Queries` card. #### Queries Card - Added client-side pagination. - Added `Add query` button (screenshots below are with `Admin` role). <img width="710" height="395" alt="Screenshot 2025-12-26 at 2 15 07 PM" src="https://github.com/user-attachments/assets/b4e58269-d1b2-4c87-abfa-2cdfe47b533e" /> <img width="723" height="301" alt="Screenshot 2025-12-26 at 2 15 00 PM" src="https://github.com/user-attachments/assets/2615d5bf-5d75-4e83-bc69-bc884232bf32" /> - As an `Observer`, `Add query` is not displayed <img width="2240" height="1077" alt="Screenshot 2025-12-26 at 2 27 25 PM" src="https://github.com/user-attachments/assets/426de709-d2ce-4bef-96f1-919ad5bddb13" /> - As a `Maintainer`, `Add query` is displayed <img width="2236" height="1084" alt="Screenshot 2025-12-26 at 2 31 16 PM" src="https://github.com/user-attachments/assets/218b0d18-2536-4336-88c8-41e7d09a5e9e" /> ### New query page If the user navigates from `Host details`, `host_id` search parameter is added to the URL and the back button displays `Back to host details`. <img width="1097" height="506" alt="Screenshot 2025-12-26 at 2 15 32 PM" src="https://github.com/user-attachments/assets/61777c85-22f5-49dc-a3e6-dcd706119c70" /> ### Host Queries (/hosts/:hostId/queries/:queryId) `Performance impact` added above the table. <img width="2029" height="626" alt="Screenshot 2025-12-26 at 2 16 00 PM" src="https://github.com/user-attachments/assets/05c6b1bc-0587-4b0a-8167-142787592c6d" /> <img width="1555" height="482" alt="Screenshot 2025-12-26 at 2 16 05 PM" src="https://github.com/user-attachments/assets/b9035b63-51c3-46c0-a903-c16d54c22986" />
2026-01-02 13:06:12 +00:00
hostId,
onExit,
}: ISaveAsNewQueryModal) => {
const { renderFlash } = useContext(NotificationContext);
const { isPremiumTier } = useContext(AppContext);
const [formData, setFormData] = useState<ISANQFormData>({
queryName: `Copy of ${initialQueryData.name}`,
team: {
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-13 03:26:48 +00:00
id: initialQueryData.fleet_id,
name: undefined,
},
});
const [isSaving, setIsSaving] = useState(false);
const [formErrors, setFormErrors] = useState<ISANQFormErrors>({});
const { userTeams } = useTeamIdParam({
router,
location,
includeAllTeams: true,
includeNoTeam: false,
permittedAccessByTeamRole: {
admin: true,
maintainer: true,
observer: false,
observer_plus: false,
technician: false,
},
});
const onInputChange = useCallback(
({
name,
value,
}: {
name: string;
value: string | Partial<ITeamSummary>;
}) => {
const newFormData = { ...formData, [name]: value };
setFormData(newFormData);
const newErrors = validateFormData(newFormData);
const errsToSet: ISANQFormErrors = {};
Object.keys(formErrors).forEach((k) => {
if (k in newErrors) {
errsToSet[k as keyof ISANQFormErrors] =
newErrors[k as keyof ISANQFormErrors];
}
});
setFormErrors(errsToSet);
},
[formData, formErrors]
);
const onInputBlur = () => {
setFormErrors(validateFormData(formData));
};
const onTeamChange = useCallback(
(teamId: number) => {
const selectedTeam = userTeams?.find((team) => team.id === teamId);
setFormData((prevData) => ({
...prevData,
team: {
id: teamId,
name: selectedTeam ? selectedTeam.name : undefined,
},
}));
},
[userTeams]
);
// take all existing data for query from parent, allow editing name and team
const handleSave = async (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault();
const errors = validateFormData(formData);
if (Object.keys(errors).length > 0) {
setFormErrors(errors);
return;
}
setIsSaving(true);
const {
queryName,
team: { id: teamId, name: teamName },
} = formData;
const createBody = {
...initialQueryData,
name: queryName,
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-13 03:26:48 +00:00
fleet_id: teamId === APP_CONTEXT_ALL_TEAMS_ID ? API_ALL_TEAMS_ID : teamId,
};
try {
const { query: newQuery } = await queryAPI.create(createBody);
setIsSaving(false);
renderFlash("success", `Successfully added report ${newQuery.name}.`);
router.push(
Update urls to use "fleets" and "reports" instead of "teams" and "queries" (#41084) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** For #41030 # Details This PR updates front-end routes and redirects the old routes to the new ones. While I typically have shied away from renaming vars and constants in this phase of the renaming work, I chose to rename the path constants here because they're a lot less useful when they have names that don't correspond to the paths they're representing. I did the renames using VSCode's "Rename Symbol" feature which automatically finds and fixes any references. I then asked Claude to verify the changes and it didn't find any dangling references (also the code would fail to compile unless all the new names collided with old ones). # 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 ## Testing - [ ] Added/updated automated tests no relevant tests exist - [X] QA'd all new/changed functionality manually ## Reports (formerly Queries) **New routes:** - [x] /reports/manage — Reports list page - [x] /reports/new — New report editor - [x] /reports/new/live — New report live query - [x] /reports/:id — Report details - [x] /reports/:id/edit — Edit report - [x] /reports/:id/live — Live report run **Redirects from old routes:** - [x] /queries → /reports - [x] /queries/manage → /reports/manage - [x] /queries/new → /reports/new - [x] /queries/new/live → /reports/new/live - [x] /queries/:id → /reports/:id - [x] /queries/:id/edit → /reports/:id/edit - [x] /queries/:id/live → /reports/:id/live ## Host Reports (formerly Host Queries) **New routes:** - [x] /hosts/:host_id/reports/:query_id — Host report results **Redirects from old routes:** - [ ] ~/hosts/:host_id/schedule → /hosts/:host_id/reports~ <- this is not a real URL; removed current broken redirect - [x] /hosts/:host_id/queries/:query_id → /hosts/:host_id/reports/:query_id ## Fleets (formerly Teams) **New routes:** - [x] /settings/fleets — Fleets list page - [x] /settings/fleets/users?fleet_id=:id — Fleet users - [x] /settings/fleets/options?fleet_id=:id — Fleet agent options - [x] /settings/fleets/settings?fleet_id=:id — Fleet settings **Redirects from old routes:** - [x] /settings/teams → /settings/fleets - [x] /settings/teams/users → /settings/fleets/users - [x] /settings/teams/options → /settings/fleets/options - [x] /settings/teams/settings → /settings/fleets/settings - [x] /settings/teams/:team_id → /settings/fleets - [x] /settings/teams/:team_id/users → /settings/fleets - [x] /settings/teams/:team_id/options → /settings/fleets **Navigation & Links** - [x] Top nav "Reports" link goes to /reports/manage - [x] User menu team switcher navigates to /settings/fleets/users?fleet_id=:id - [x] Admin sidebar "Fleets" tab goes to /settings/fleets - [x] "Create a fleet" links (user form, transfer host modal) go to /settings/fleets - [x] "Back to fleets" button on fleet details goes to /settings/fleets - [x] Fleet table name links go to /settings/fleets/users?fleet_id=:id - [x] Host details "Add query" button goes to /reports/new - [x] Select query modal links go to /reports/new and /reports/:id/edit - [x] Query report "full report" link goes to /reports/:id - [x] Browser tab titles show correct names for report pages **Query params preserved through redirects** - [x] /queries/:id?fleet_id=1 → /reports/:id?fleet_id=1 - [x] /settings/teams/users?fleet_id=1 → /settings/fleets/users?fleet_id=1 For unreleased bug fixes in a release candidate, one of: - [X] Confirmed that the fix is not expected to adversely impact load test results
2026-03-06 14:24:50 +00:00
getPathWithQueryParams(PATHS.REPORT_DETAILS(newQuery.id), {
fleet_id: newQuery.team_id,
Surface queries in host details (#37646) <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #27322 [Figma](https://www.figma.com/design/v7WjL5zQuFIZerWYaSwy8o/-27322-Surface-custom-host-vitals?node-id=5636-4950&t=LuE3Kp09a5sj24Tt-0) ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually (WIP) ## Screenshots ### Host details <img width="1481" height="1000" alt="Screenshot 2025-12-26 at 2 14 48 PM" src="https://github.com/user-attachments/assets/3d9f02f9-f3a7-4a06-b3e4-414bb7b56e25" /> - `Queries` tab removed. - Shows `Queries` card. #### Queries Card - Added client-side pagination. - Added `Add query` button (screenshots below are with `Admin` role). <img width="710" height="395" alt="Screenshot 2025-12-26 at 2 15 07 PM" src="https://github.com/user-attachments/assets/b4e58269-d1b2-4c87-abfa-2cdfe47b533e" /> <img width="723" height="301" alt="Screenshot 2025-12-26 at 2 15 00 PM" src="https://github.com/user-attachments/assets/2615d5bf-5d75-4e83-bc69-bc884232bf32" /> - As an `Observer`, `Add query` is not displayed <img width="2240" height="1077" alt="Screenshot 2025-12-26 at 2 27 25 PM" src="https://github.com/user-attachments/assets/426de709-d2ce-4bef-96f1-919ad5bddb13" /> - As a `Maintainer`, `Add query` is displayed <img width="2236" height="1084" alt="Screenshot 2025-12-26 at 2 31 16 PM" src="https://github.com/user-attachments/assets/218b0d18-2536-4336-88c8-41e7d09a5e9e" /> ### New query page If the user navigates from `Host details`, `host_id` search parameter is added to the URL and the back button displays `Back to host details`. <img width="1097" height="506" alt="Screenshot 2025-12-26 at 2 15 32 PM" src="https://github.com/user-attachments/assets/61777c85-22f5-49dc-a3e6-dcd706119c70" /> ### Host Queries (/hosts/:hostId/queries/:queryId) `Performance impact` added above the table. <img width="2029" height="626" alt="Screenshot 2025-12-26 at 2 16 00 PM" src="https://github.com/user-attachments/assets/05c6b1bc-0587-4b0a-8167-142787592c6d" /> <img width="1555" height="482" alt="Screenshot 2025-12-26 at 2 16 05 PM" src="https://github.com/user-attachments/assets/b9035b63-51c3-46c0-a903-c16d54c22986" />
2026-01-02 13:06:12 +00:00
host_id: hostId,
})
);
} catch (createError: unknown) {
let errFlash = "Could not create report. Please try again.";
const reason = getErrorReason(createError);
if (reason.includes("already exists")) {
let teamText;
if (teamId !== APP_CONTEXT_ALL_TEAMS_ID) {
teamText = teamName ? `the ${teamName} fleet` : "this fleet";
} else {
teamText = "all fleets";
}
errFlash = `A report called "${queryName}" already exists for ${teamText}.`;
} else if (reason.includes(INVALID_PLATFORMS_REASON)) {
errFlash = INVALID_PLATFORMS_FLASH_MESSAGE;
}
setIsSaving(false);
renderFlash("error", errFlash);
}
};
return (
<Modal title="Save as new" onExit={onExit}>
<form onSubmit={handleSave} className={baseClass}>
<InputField
name="queryName"
onChange={onInputChange}
onBlur={onInputBlur}
value={formData.queryName}
error={formErrors.queryName}
inputClassName={`${baseClass}__name`}
label="Name"
autofocus
ignore1password
parseTarget
/>
{isPremiumTier && (userTeams?.length || 0) > 1 && (
<div className="form-field">
<div className="form-field__label">Fleet</div>
<TeamsDropdown
asFormField
currentUserTeams={userTeams || []}
selectedTeamId={formData.team.id}
onChange={onTeamChange}
/>
</div>
)}
<div className="modal-cta-wrap">
<Button
type="submit"
className="save-as-new-query"
isLoading={isSaving}
// empty SQL error handled by parent
disabled={Object.keys(formErrors).length > 0 || isSaving}
>
Save
</Button>
<Button onClick={onExit} variant="inverse">
Cancel
</Button>
</div>
</form>
</Modal>
);
};
export default SaveAsNewQueryModal;