fleet/frontend/pages/queries/edit/components/SaveAsNewQueryModal/SaveAsNewQueryModal.tsx
Scott Gress fe7be1833a
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 08:24:50 -06:00

234 lines
6.3 KiB
TypeScript

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";
// @ts-ignore
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;
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,
hostId,
onExit,
}: ISaveAsNewQueryModal) => {
const { renderFlash } = useContext(NotificationContext);
const { isPremiumTier } = useContext(AppContext);
const [formData, setFormData] = useState<ISANQFormData>({
queryName: `Copy of ${initialQueryData.name}`,
team: {
id: initialQueryData.team_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,
team_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(
getPathWithQueryParams(PATHS.REPORT_DETAILS(newQuery.id), {
fleet_id: newQuery.team_id,
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;