2025-05-28 16:40:13 +00:00
|
|
|
import React, { useState, useEffect, useContext } from "react";
|
2025-03-12 18:54:29 +00:00
|
|
|
import { useQuery } from "react-query";
|
|
|
|
|
|
2025-05-28 16:40:13 +00:00
|
|
|
import { size } from "lodash";
|
2023-07-17 21:09:59 +00:00
|
|
|
|
2024-11-13 14:32:59 +00:00
|
|
|
import { AppContext } from "context/app";
|
|
|
|
|
|
2023-07-17 21:09:59 +00:00
|
|
|
import useDeepEffect from "hooks/useDeepEffect";
|
2025-04-08 13:31:58 +00:00
|
|
|
import { IPlatformSelector } from "hooks/usePlatformSelector";
|
2023-07-17 21:09:59 +00:00
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
FREQUENCY_DROPDOWN_OPTIONS,
|
|
|
|
|
LOGGING_TYPE_OPTIONS,
|
|
|
|
|
MIN_OSQUERY_VERSION_OPTIONS,
|
2025-03-12 18:54:29 +00:00
|
|
|
DEFAULT_USE_QUERY_OPTIONS,
|
2023-07-17 21:09:59 +00:00
|
|
|
} from "utilities/constants";
|
2024-12-05 23:19:56 +00:00
|
|
|
|
2025-01-14 00:45:16 +00:00
|
|
|
import { CommaSeparatedPlatformString } from "interfaces/platform";
|
2023-07-17 21:09:59 +00:00
|
|
|
import {
|
|
|
|
|
ICreateQueryRequestBody,
|
|
|
|
|
ISchedulableQuery,
|
|
|
|
|
QueryLoggingOption,
|
|
|
|
|
} from "interfaces/schedulable_query";
|
2024-12-05 23:19:56 +00:00
|
|
|
|
|
|
|
|
import Checkbox from "components/forms/fields/Checkbox";
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
import InputField from "components/forms/fields/InputField";
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
import Dropdown from "components/forms/fields/Dropdown";
|
|
|
|
|
import Slider from "components/forms/fields/Slider";
|
|
|
|
|
import TooltipWrapper from "components/TooltipWrapper";
|
|
|
|
|
import Icon from "components/Icon";
|
|
|
|
|
import Button from "components/buttons/Button";
|
|
|
|
|
import Modal from "components/Modal";
|
|
|
|
|
import RevealButton from "components/buttons/RevealButton";
|
|
|
|
|
import LogDestinationIndicator from "components/LogDestinationIndicator";
|
2025-03-12 18:54:29 +00:00
|
|
|
import TargetLabelSelector from "components/TargetLabelSelector";
|
|
|
|
|
import labelsAPI, {
|
|
|
|
|
getCustomLabels,
|
|
|
|
|
ILabelsSummaryResponse,
|
|
|
|
|
} from "services/entities/labels";
|
2024-12-05 23:19:56 +00:00
|
|
|
|
2023-10-09 18:31:31 +00:00
|
|
|
import DiscardDataOption from "../DiscardDataOption";
|
2023-07-17 21:09:59 +00:00
|
|
|
|
|
|
|
|
const baseClass = "save-query-modal";
|
2025-06-30 23:00:22 +00:00
|
|
|
export interface ISaveNewQueryModalProps {
|
2023-07-17 21:09:59 +00:00
|
|
|
queryValue: string;
|
2023-07-20 00:50:27 +00:00
|
|
|
apiTeamIdForQuery?: number; // query will be global if omitted
|
2023-07-17 21:09:59 +00:00
|
|
|
isLoading: boolean;
|
|
|
|
|
saveQuery: (formData: ICreateQueryRequestBody) => void;
|
2025-06-30 23:00:22 +00:00
|
|
|
toggleSaveNewQueryModal: () => void;
|
2023-07-17 21:09:59 +00:00
|
|
|
backendValidators: { [key: string]: string };
|
|
|
|
|
existingQuery?: ISchedulableQuery;
|
2023-10-09 18:31:31 +00:00
|
|
|
queryReportsDisabled?: boolean;
|
2025-04-08 13:31:58 +00:00
|
|
|
platformSelector: IPlatformSelector;
|
2023-07-17 21:09:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validateQueryName = (name: string) => {
|
|
|
|
|
const errors: { [key: string]: string } = {};
|
|
|
|
|
|
|
|
|
|
if (!name) {
|
2026-02-17 21:19:33 +00:00
|
|
|
errors.name = "Report name must be present";
|
2023-07-17 21:09:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const valid = !size(errors);
|
|
|
|
|
return { valid, errors };
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-30 23:00:22 +00:00
|
|
|
const SaveNewQueryModal = ({
|
2023-07-17 21:09:59 +00:00
|
|
|
queryValue,
|
2023-07-20 00:50:27 +00:00
|
|
|
apiTeamIdForQuery,
|
2023-07-17 21:09:59 +00:00
|
|
|
isLoading,
|
|
|
|
|
saveQuery,
|
2025-06-30 23:00:22 +00:00
|
|
|
toggleSaveNewQueryModal,
|
2023-07-17 21:09:59 +00:00
|
|
|
backendValidators,
|
|
|
|
|
existingQuery,
|
2023-10-09 18:31:31 +00:00
|
|
|
queryReportsDisabled,
|
2025-04-08 13:31:58 +00:00
|
|
|
platformSelector,
|
2025-06-30 23:00:22 +00:00
|
|
|
}: ISaveNewQueryModalProps): JSX.Element => {
|
2026-01-09 16:37:54 +00:00
|
|
|
const { config, isPremiumTier, currentTeam } = useContext(AppContext);
|
2024-11-13 14:32:59 +00:00
|
|
|
|
2023-07-17 21:09:59 +00:00
|
|
|
const [name, setName] = useState("");
|
|
|
|
|
const [description, setDescription] = useState("");
|
|
|
|
|
const [selectedFrequency, setSelectedFrequency] = useState(
|
|
|
|
|
existingQuery?.interval ?? 3600
|
|
|
|
|
);
|
|
|
|
|
const [
|
|
|
|
|
selectedMinOsqueryVersionOptions,
|
|
|
|
|
setSelectedMinOsqueryVersionOptions,
|
|
|
|
|
] = useState(existingQuery?.min_osquery_version ?? "");
|
|
|
|
|
const [
|
|
|
|
|
selectedLoggingType,
|
|
|
|
|
setSelectedLoggingType,
|
|
|
|
|
] = useState<QueryLoggingOption>(existingQuery?.logging ?? "snapshot");
|
|
|
|
|
const [observerCanRun, setObserverCanRun] = useState(false);
|
2024-11-13 14:32:59 +00:00
|
|
|
const [automationsEnabled, setAutomationsEnabled] = useState(false);
|
2025-03-12 18:54:29 +00:00
|
|
|
const [selectedTargetType, setSelectedTargetType] = useState("All hosts");
|
|
|
|
|
const [selectedLabels, setSelectedLabels] = useState({});
|
2023-10-04 22:19:26 +00:00
|
|
|
const [discardData, setDiscardData] = useState(false);
|
2023-07-17 21:09:59 +00:00
|
|
|
const [errors, setErrors] = useState<{ [key: string]: string }>(
|
|
|
|
|
backendValidators
|
|
|
|
|
);
|
|
|
|
|
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
|
|
|
|
|
2025-03-12 18:54:29 +00:00
|
|
|
const {
|
|
|
|
|
data: { labels } = { labels: [] },
|
|
|
|
|
isFetching: isFetchingLabels,
|
|
|
|
|
} = useQuery<ILabelsSummaryResponse, Error>(
|
|
|
|
|
["custom_labels"],
|
2026-01-09 16:37:54 +00:00
|
|
|
() => labelsAPI.summary(currentTeam?.id, true),
|
2025-03-12 18:54:29 +00:00
|
|
|
{
|
|
|
|
|
...DEFAULT_USE_QUERY_OPTIONS,
|
2026-01-09 16:37:54 +00:00
|
|
|
// Wait for the current team to load from context before pulling labels, otherwise on a page load
|
|
|
|
|
// directly on the page this gets called with currentTeam not set, then again
|
|
|
|
|
// with the correct team value. If we don't trigger on currentTeam changes we'll just start with a
|
|
|
|
|
// null team ID here and never populate with the correct team unless we navigate from another page
|
|
|
|
|
// where team context is already set prior to navigation.
|
|
|
|
|
enabled: isPremiumTier && !!currentTeam,
|
2025-03-12 18:54:29 +00:00
|
|
|
staleTime: 10000,
|
|
|
|
|
select: (res) => ({ labels: getCustomLabels(res.labels) }),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const onSelectLabel = ({
|
|
|
|
|
name: labelName,
|
|
|
|
|
value,
|
|
|
|
|
}: {
|
|
|
|
|
name: string;
|
|
|
|
|
value: boolean;
|
|
|
|
|
}) => {
|
|
|
|
|
setSelectedLabels({
|
|
|
|
|
...selectedLabels,
|
|
|
|
|
[labelName]: value,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2023-07-17 21:09:59 +00:00
|
|
|
const toggleAdvancedOptions = () => {
|
|
|
|
|
setShowAdvancedOptions(!showAdvancedOptions);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useDeepEffect(() => {
|
|
|
|
|
if (name) {
|
|
|
|
|
setErrors({});
|
|
|
|
|
}
|
|
|
|
|
}, [name]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setErrors(backendValidators);
|
|
|
|
|
}, [backendValidators]);
|
|
|
|
|
|
2025-03-12 18:54:29 +00:00
|
|
|
// Disable saving if "Custom" targeting is selected, but no labels are selected.
|
|
|
|
|
const canSave =
|
2025-04-08 13:31:58 +00:00
|
|
|
platformSelector.isAnyPlatformSelected &&
|
|
|
|
|
(selectedTargetType === "All hosts" ||
|
|
|
|
|
Object.entries(selectedLabels).some(([, value]) => {
|
|
|
|
|
return value;
|
|
|
|
|
}));
|
2025-03-12 18:54:29 +00:00
|
|
|
|
2023-07-17 21:09:59 +00:00
|
|
|
const onClickSaveQuery = (evt: React.MouseEvent<HTMLFormElement>) => {
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
|
2024-10-03 22:59:10 +00:00
|
|
|
const trimmedName = name.trim();
|
|
|
|
|
|
|
|
|
|
const { valid, errors: newErrors } = validateQueryName(trimmedName);
|
2023-07-17 21:09:59 +00:00
|
|
|
setErrors({
|
|
|
|
|
...errors,
|
|
|
|
|
...newErrors,
|
|
|
|
|
});
|
2024-10-03 22:59:10 +00:00
|
|
|
setName(trimmedName);
|
2023-07-17 21:09:59 +00:00
|
|
|
|
2025-04-08 13:31:58 +00:00
|
|
|
const newPlatformString = platformSelector
|
|
|
|
|
.getSelectedPlatforms()
|
|
|
|
|
.join(",") as CommaSeparatedPlatformString;
|
|
|
|
|
|
2023-07-17 21:09:59 +00:00
|
|
|
if (valid) {
|
|
|
|
|
saveQuery({
|
|
|
|
|
// from modal fields
|
2024-10-03 22:59:10 +00:00
|
|
|
name: trimmedName,
|
2023-07-17 21:09:59 +00:00
|
|
|
description,
|
|
|
|
|
interval: selectedFrequency,
|
|
|
|
|
observer_can_run: observerCanRun,
|
2024-11-13 14:32:59 +00:00
|
|
|
automations_enabled: automationsEnabled,
|
2023-10-04 22:19:26 +00:00
|
|
|
discard_data: discardData,
|
2025-04-08 13:31:58 +00:00
|
|
|
platform: newPlatformString,
|
2023-07-17 21:09:59 +00:00
|
|
|
min_osquery_version: selectedMinOsqueryVersionOptions,
|
|
|
|
|
logging: selectedLoggingType,
|
|
|
|
|
// from previous New query page
|
|
|
|
|
query: queryValue,
|
|
|
|
|
// from doubly previous ManageQueriesPage
|
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: apiTeamIdForQuery,
|
2025-03-12 18:54:29 +00:00
|
|
|
labels_include_any:
|
|
|
|
|
selectedTargetType === "Custom"
|
|
|
|
|
? Object.entries(selectedLabels)
|
|
|
|
|
.filter(([, selected]) => selected)
|
|
|
|
|
.map(([labelName]) => labelName)
|
|
|
|
|
: [],
|
2023-07-17 21:09:59 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-17 21:19:33 +00:00
|
|
|
<Modal title="Save report" onExit={toggleSaveNewQueryModal}>
|
2023-10-02 19:26:53 +00:00
|
|
|
<form
|
|
|
|
|
onSubmit={onClickSaveQuery}
|
|
|
|
|
className={baseClass}
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
>
|
|
|
|
|
<InputField
|
|
|
|
|
name="name"
|
|
|
|
|
onChange={(value: string) => setName(value)}
|
2024-10-03 22:59:10 +00:00
|
|
|
onBlur={() => {
|
|
|
|
|
setName(name.trim());
|
|
|
|
|
}}
|
2023-10-02 19:26:53 +00:00
|
|
|
value={name}
|
|
|
|
|
error={errors.name}
|
|
|
|
|
inputClassName={`${baseClass}__name`}
|
|
|
|
|
label="Name"
|
|
|
|
|
autofocus
|
|
|
|
|
ignore1password
|
|
|
|
|
/>
|
|
|
|
|
<InputField
|
|
|
|
|
name="description"
|
|
|
|
|
onChange={(value: string) => setDescription(value)}
|
|
|
|
|
value={description}
|
|
|
|
|
inputClassName={`${baseClass}__description`}
|
|
|
|
|
label="Description"
|
|
|
|
|
type="textarea"
|
2026-02-17 21:19:33 +00:00
|
|
|
helpText="What information does your report reveal? (optional)"
|
2023-10-02 19:26:53 +00:00
|
|
|
/>
|
|
|
|
|
<Dropdown
|
|
|
|
|
searchable={false}
|
|
|
|
|
options={FREQUENCY_DROPDOWN_OPTIONS}
|
|
|
|
|
onChange={(value: number) => {
|
|
|
|
|
setSelectedFrequency(value);
|
|
|
|
|
}}
|
2024-02-23 14:57:18 +00:00
|
|
|
placeholder="Every hour"
|
2023-10-02 19:26:53 +00:00
|
|
|
value={selectedFrequency}
|
2025-05-28 16:40:13 +00:00
|
|
|
label="Interval"
|
2023-10-09 18:31:31 +00:00
|
|
|
wrapperClassName={`${baseClass}__form-field form-field--frequency`}
|
2026-02-17 21:19:33 +00:00
|
|
|
helpText="This is how often your report collects data."
|
2023-10-02 19:26:53 +00:00
|
|
|
/>
|
|
|
|
|
<Checkbox
|
|
|
|
|
name="observerCanRun"
|
|
|
|
|
onChange={setObserverCanRun}
|
|
|
|
|
value={observerCanRun}
|
2024-02-23 14:57:18 +00:00
|
|
|
wrapperClassName="observer-can-run-wrapper"
|
2026-02-17 21:19:33 +00:00
|
|
|
helpText="Users with the Observer role will be able to run this report as a live report."
|
2023-07-17 21:09:59 +00:00
|
|
|
>
|
2023-10-02 19:26:53 +00:00
|
|
|
Observers can run
|
|
|
|
|
</Checkbox>
|
2024-11-13 14:32:59 +00:00
|
|
|
<Slider
|
|
|
|
|
onChange={() => setAutomationsEnabled(!automationsEnabled)}
|
|
|
|
|
value={automationsEnabled}
|
|
|
|
|
activeText={
|
|
|
|
|
<>
|
|
|
|
|
Automations on
|
|
|
|
|
{selectedFrequency === 0 && (
|
|
|
|
|
<TooltipWrapper
|
|
|
|
|
tipContent={
|
|
|
|
|
<>
|
|
|
|
|
Automations and reporting will be paused <br />
|
2026-02-17 21:19:33 +00:00
|
|
|
for this report until an interval is set.
|
2024-11-13 14:32:59 +00:00
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
position="right"
|
|
|
|
|
tipOffset={9}
|
|
|
|
|
showArrow
|
|
|
|
|
underline={false}
|
|
|
|
|
>
|
|
|
|
|
<Icon name="warning" />
|
|
|
|
|
</TooltipWrapper>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
inactiveText="Automations off"
|
|
|
|
|
helpText={
|
|
|
|
|
<>
|
|
|
|
|
Historical results will {!automationsEnabled ? "not " : ""}be sent
|
2024-12-05 23:19:56 +00:00
|
|
|
to your log destination:{" "}
|
|
|
|
|
<b>
|
|
|
|
|
<LogDestinationIndicator
|
|
|
|
|
logDestination={config?.logging.result.plugin || ""}
|
2025-06-19 14:39:07 +00:00
|
|
|
filesystemDestination={
|
2025-06-19 19:51:49 +00:00
|
|
|
config?.logging.result.config?.result_log_file
|
2025-06-19 14:39:07 +00:00
|
|
|
}
|
2025-07-09 18:37:54 +00:00
|
|
|
webhookDestination={config?.logging.result.config?.result_url}
|
2024-12-05 23:19:56 +00:00
|
|
|
excludeTooltip
|
|
|
|
|
/>
|
|
|
|
|
</b>
|
|
|
|
|
.
|
2024-11-13 14:32:59 +00:00
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
/>
|
2025-04-08 13:31:58 +00:00
|
|
|
{platformSelector.render()}
|
2025-03-12 18:54:29 +00:00
|
|
|
{isPremiumTier && (
|
|
|
|
|
<TargetLabelSelector
|
|
|
|
|
selectedTargetType={selectedTargetType}
|
|
|
|
|
selectedLabels={selectedLabels}
|
|
|
|
|
className={`${baseClass}__target`}
|
|
|
|
|
onSelectTargetType={setSelectedTargetType}
|
|
|
|
|
onSelectLabel={onSelectLabel}
|
|
|
|
|
labels={labels || []}
|
|
|
|
|
customHelpText={
|
|
|
|
|
<span className="form-field__help-text">
|
2026-02-17 21:19:33 +00:00
|
|
|
Report will target hosts that <b>have any</b> of these labels:
|
2025-03-12 18:54:29 +00:00
|
|
|
</span>
|
|
|
|
|
}
|
2025-04-14 14:12:07 +00:00
|
|
|
suppressTitle
|
2025-03-12 18:54:29 +00:00
|
|
|
/>
|
|
|
|
|
)}
|
2023-10-02 19:26:53 +00:00
|
|
|
<RevealButton
|
|
|
|
|
isShowing={showAdvancedOptions}
|
2024-02-23 14:57:18 +00:00
|
|
|
className="advanced-options-toggle"
|
|
|
|
|
hideText="Hide advanced options"
|
|
|
|
|
showText="Show advanced options"
|
|
|
|
|
caretPosition="after"
|
2023-10-02 19:26:53 +00:00
|
|
|
onClick={toggleAdvancedOptions}
|
|
|
|
|
/>
|
|
|
|
|
{showAdvancedOptions && (
|
|
|
|
|
<>
|
|
|
|
|
<Dropdown
|
|
|
|
|
options={MIN_OSQUERY_VERSION_OPTIONS}
|
|
|
|
|
onChange={setSelectedMinOsqueryVersionOptions}
|
|
|
|
|
placeholder="Select"
|
|
|
|
|
value={selectedMinOsqueryVersionOptions}
|
|
|
|
|
label="Minimum osquery version"
|
|
|
|
|
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}
|
|
|
|
|
/>
|
|
|
|
|
<Dropdown
|
|
|
|
|
options={LOGGING_TYPE_OPTIONS}
|
|
|
|
|
onChange={setSelectedLoggingType}
|
|
|
|
|
placeholder="Select"
|
|
|
|
|
value={selectedLoggingType}
|
|
|
|
|
label="Logging"
|
|
|
|
|
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--logging`}
|
|
|
|
|
/>
|
2023-10-09 18:31:31 +00:00
|
|
|
{queryReportsDisabled !== undefined && (
|
|
|
|
|
<DiscardDataOption
|
|
|
|
|
{...{
|
|
|
|
|
queryReportsDisabled,
|
|
|
|
|
selectedLoggingType,
|
|
|
|
|
discardData,
|
|
|
|
|
setDiscardData,
|
|
|
|
|
breakHelpText: true,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2023-10-02 19:26:53 +00:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
<div className="modal-cta-wrap">
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
className="save-query-loading"
|
2025-03-12 18:54:29 +00:00
|
|
|
isLoading={isLoading || isFetchingLabels}
|
|
|
|
|
disabled={!canSave}
|
2023-07-17 21:09:59 +00:00
|
|
|
>
|
2023-10-02 19:26:53 +00:00
|
|
|
Save
|
|
|
|
|
</Button>
|
2025-06-30 23:00:22 +00:00
|
|
|
<Button onClick={toggleSaveNewQueryModal} variant="inverse">
|
2023-10-02 19:26:53 +00:00
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
2023-07-17 21:09:59 +00:00
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-30 23:00:22 +00:00
|
|
|
export default SaveNewQueryModal;
|