2025-07-03 20:11:06 +00:00
|
|
|
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;
|
2026-01-02 13:06:12 +00:00
|
|
|
hostId?: number;
|
2025-07-03 20:11:06 +00:00
|
|
|
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,
|
2026-01-02 13:06:12 +00:00
|
|
|
hostId,
|
2025-07-03 20:11:06 +00:00
|
|
|
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,
|
2025-07-03 20:11:06 +00:00
|
|
|
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,
|
2026-02-11 18:00:10 +00:00
|
|
|
technician: false,
|
2025-07-03 20:11:06 +00:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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,
|
2025-07-03 20:11:06 +00:00
|
|
|
};
|
|
|
|
|
try {
|
|
|
|
|
const { query: newQuery } = await queryAPI.create(createBody);
|
|
|
|
|
setIsSaving(false);
|
2026-02-17 21:19:33 +00:00
|
|
|
renderFlash("success", `Successfully added report ${newQuery.name}.`);
|
2025-07-03 20:11:06 +00:00
|
|
|
router.push(
|
2026-03-06 14:24:50 +00:00
|
|
|
getPathWithQueryParams(PATHS.REPORT_DETAILS(newQuery.id), {
|
2026-02-17 21:19:33 +00:00
|
|
|
fleet_id: newQuery.team_id,
|
2026-01-02 13:06:12 +00:00
|
|
|
host_id: hostId,
|
2025-07-03 20:11:06 +00:00
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
} catch (createError: unknown) {
|
2026-02-17 21:19:33 +00:00
|
|
|
let errFlash = "Could not create report. Please try again.";
|
2025-07-03 20:11:06 +00:00
|
|
|
const reason = getErrorReason(createError);
|
|
|
|
|
if (reason.includes("already exists")) {
|
|
|
|
|
let teamText;
|
|
|
|
|
if (teamId !== APP_CONTEXT_ALL_TEAMS_ID) {
|
2026-02-17 21:19:33 +00:00
|
|
|
teamText = teamName ? `the ${teamName} fleet` : "this fleet";
|
2025-07-03 20:11:06 +00:00
|
|
|
} else {
|
2026-02-17 21:19:33 +00:00
|
|
|
teamText = "all fleets";
|
2025-07-03 20:11:06 +00:00
|
|
|
}
|
2026-02-17 21:19:33 +00:00
|
|
|
errFlash = `A report called "${queryName}" already exists for ${teamText}.`;
|
2025-07-03 20:11:06 +00:00
|
|
|
} 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">
|
2026-02-17 21:19:33 +00:00
|
|
|
<div className="form-field__label">Fleet</div>
|
2025-07-03 20:11:06 +00:00
|
|
|
<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}
|
2025-12-04 19:18:48 +00:00
|
|
|
// empty SQL error handled by parent
|
2025-07-03 20:11:06 +00:00
|
|
|
disabled={Object.keys(formErrors).length > 0 || isSaving}
|
|
|
|
|
>
|
|
|
|
|
Save
|
|
|
|
|
</Button>
|
|
|
|
|
<Button onClick={onExit} variant="inverse">
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default SaveAsNewQueryModal;
|