UI: Update the save query modal with scheduling-related fields (#12741)
## Addresses #12646 ### See issue for list of completed work  ### Also see PR #12713 **notes for review** on that PR for help manually testing this work in lieu of the completed API. - [x] Changes file added for user-visible changes in `changes/` - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
co-authored by
Jacob Shandling
parent
53f57c44db
commit
1d6870f0a7
@@ -0,0 +1 @@
|
||||
- Update the save query modal to include scheduling-related fields.
|
||||
@@ -77,7 +77,7 @@ export interface IModifyQueryRequestBody
|
||||
// Delete a query by name
|
||||
/** DELETE /api/v1/fleet/queries/{name} */
|
||||
export interface IDeleteQueryRequestBody {
|
||||
team_id?: number; // searches for a global query if ommitted
|
||||
team_id?: number; // searches for a global query if omitted
|
||||
}
|
||||
|
||||
// Delete a query by id
|
||||
|
||||
@@ -150,7 +150,7 @@ const ManageQueriesPage = ({
|
||||
}
|
||||
}, [location, filteredQueriesPath, setFilteredQueriesPath]);
|
||||
|
||||
const onCreateQueryClick = () => router.push(PATHS.NEW_QUERY);
|
||||
const onCreateQueryClick = () => router.push(PATHS.NEW_QUERY(currentTeamId));
|
||||
|
||||
const toggleDeleteQueryModal = useCallback(() => {
|
||||
setShowDeleteQueryModal(!showDeleteQueryModal);
|
||||
|
||||
@@ -12,7 +12,11 @@ import statusAPI from "services/entities/status";
|
||||
import { IHost, IHostResponse } from "interfaces/host";
|
||||
import { ILabel } from "interfaces/label";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IQueryFormData, IQuery, IStoredQueryResponse } from "interfaces/query";
|
||||
import {
|
||||
ICreateQueryRequestBody,
|
||||
IGetQueryResponse,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
import { ITarget } from "interfaces/target";
|
||||
|
||||
import QuerySidePanel from "components/side_panels/QuerySidePanel";
|
||||
@@ -23,12 +27,15 @@ import CustomLink from "components/CustomLink";
|
||||
|
||||
import QueryEditor from "pages/queries/QueryPage/screens/QueryEditor";
|
||||
import RunQuery from "pages/queries/QueryPage/screens/RunQuery";
|
||||
import useTeamIdParam from "hooks/useTeamIdParam";
|
||||
|
||||
interface IQueryPageProps {
|
||||
router: InjectedRouter;
|
||||
params: Params;
|
||||
location: {
|
||||
query: { host_ids: string };
|
||||
pathname: string;
|
||||
query: { host_ids: string; team_id?: string };
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,9 +44,15 @@ const baseClass = "query-page";
|
||||
const QueryPage = ({
|
||||
router,
|
||||
params: { id: paramsQueryId },
|
||||
location: { query: URLQuerySearch },
|
||||
location,
|
||||
}: IQueryPageProps): JSX.Element => {
|
||||
const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
|
||||
const { teamIdForApi: teamIdForQuery } = useTeamIdParam({
|
||||
location,
|
||||
router,
|
||||
includeAllTeams: true,
|
||||
includeNoTeam: false,
|
||||
});
|
||||
|
||||
const handlePageError = useErrorHandler();
|
||||
const {
|
||||
@@ -78,13 +91,13 @@ const QueryPage = ({
|
||||
isLoading: isStoredQueryLoading,
|
||||
data: storedQuery,
|
||||
error: storedQueryError,
|
||||
} = useQuery<IStoredQueryResponse, Error, IQuery>(
|
||||
} = useQuery<IGetQueryResponse, Error, ISchedulableQuery>(
|
||||
["query", queryId],
|
||||
() => queryAPI.load(queryId as number),
|
||||
{
|
||||
enabled: !!queryId,
|
||||
refetchOnWindowFocus: false,
|
||||
select: (data: IStoredQueryResponse) => data.query,
|
||||
select: (data) => data.query,
|
||||
onSuccess: (returnedQuery) => {
|
||||
setLastEditedQueryId(returnedQuery.id);
|
||||
setLastEditedQueryName(returnedQuery.name);
|
||||
@@ -99,9 +112,9 @@ const QueryPage = ({
|
||||
useQuery<IHostResponse, Error, IHost>(
|
||||
"hostFromURL",
|
||||
() =>
|
||||
hostAPI.loadHostDetails(parseInt(URLQuerySearch.host_ids as string, 10)),
|
||||
hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)),
|
||||
{
|
||||
enabled: !!URLQuerySearch.host_ids && !queryParamHostsAdded,
|
||||
enabled: !!location.query.host_ids && !queryParamHostsAdded,
|
||||
select: (data: IHostResponse) => data.host,
|
||||
onSuccess: (host) => {
|
||||
setTargetedHosts((prevHosts) =>
|
||||
@@ -119,7 +132,9 @@ const QueryPage = ({
|
||||
}
|
||||
);
|
||||
|
||||
const { mutateAsync: createQuery } = useMutation((formData: IQueryFormData) =>
|
||||
const {
|
||||
mutateAsync: createQuery,
|
||||
} = useMutation((formData: ICreateQueryRequestBody) =>
|
||||
queryAPI.create(formData)
|
||||
);
|
||||
|
||||
@@ -179,10 +194,11 @@ const QueryPage = ({
|
||||
const goToQueryEditor = useCallback(() => setStep(QUERIES_PAGE_STEPS[1]), []);
|
||||
|
||||
const renderScreen = () => {
|
||||
const step1Opts = {
|
||||
const step1Props = {
|
||||
router,
|
||||
baseClass,
|
||||
queryIdForEdit: queryId,
|
||||
teamIdForQuery,
|
||||
showOpenSchemaActionText,
|
||||
storedQuery,
|
||||
isStoredQueryLoading,
|
||||
@@ -194,7 +210,7 @@ const QueryPage = ({
|
||||
renderLiveQueryWarning,
|
||||
};
|
||||
|
||||
const step2Opts = {
|
||||
const step2Props = {
|
||||
baseClass,
|
||||
queryId,
|
||||
selectedTargets,
|
||||
@@ -211,7 +227,7 @@ const QueryPage = ({
|
||||
setTargetsTotalCount,
|
||||
};
|
||||
|
||||
const step3Opts = {
|
||||
const step3Props = {
|
||||
queryId,
|
||||
selectedTargets,
|
||||
storedQuery,
|
||||
@@ -222,11 +238,11 @@ const QueryPage = ({
|
||||
|
||||
switch (step) {
|
||||
case QUERIES_PAGE_STEPS[2]:
|
||||
return <SelectTargets {...step2Opts} />;
|
||||
return <SelectTargets {...step2Props} />;
|
||||
case QUERIES_PAGE_STEPS[3]:
|
||||
return <RunQuery {...step3Opts} />;
|
||||
return <RunQuery {...step3Props} />;
|
||||
default:
|
||||
return <QueryEditor {...step1Opts} />;
|
||||
return <QueryEditor {...step1Props} />;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { size } from "lodash";
|
||||
|
||||
import { IQueryFormData } from "interfaces/query";
|
||||
import useDeepEffect from "hooks/useDeepEffect";
|
||||
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
|
||||
export interface INewQueryModalProps {
|
||||
baseClass: string;
|
||||
queryValue: string;
|
||||
isLoading: boolean;
|
||||
onCreateQuery: (formData: IQueryFormData) => void;
|
||||
setIsSaveModalOpen: (isOpen: boolean) => void;
|
||||
backendValidators: { [key: string]: string };
|
||||
}
|
||||
|
||||
const validateQueryName = (name: string) => {
|
||||
const errors: { [key: string]: string } = {};
|
||||
|
||||
if (!name) {
|
||||
errors.name = "Query name must be present";
|
||||
}
|
||||
|
||||
const valid = !size(errors);
|
||||
return { valid, errors };
|
||||
};
|
||||
|
||||
const NewQueryModal = ({
|
||||
baseClass,
|
||||
queryValue,
|
||||
isLoading,
|
||||
onCreateQuery,
|
||||
setIsSaveModalOpen,
|
||||
backendValidators,
|
||||
}: INewQueryModalProps): JSX.Element => {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [observerCanRun, setObserverCanRun] = useState(false);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>(
|
||||
backendValidators
|
||||
);
|
||||
|
||||
useDeepEffect(() => {
|
||||
if (name) {
|
||||
setErrors({});
|
||||
}
|
||||
}, [name]);
|
||||
|
||||
useEffect(() => {
|
||||
setErrors(backendValidators);
|
||||
}, [backendValidators]);
|
||||
|
||||
const handleUpdate = (evt: React.MouseEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { valid, errors: newErrors } = validateQueryName(name);
|
||||
setErrors({
|
||||
...errors,
|
||||
...newErrors,
|
||||
});
|
||||
|
||||
if (valid) {
|
||||
onCreateQuery({
|
||||
description,
|
||||
name,
|
||||
query: queryValue,
|
||||
observer_can_run: observerCanRun,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title={"Save query"} onExit={() => setIsSaveModalOpen(false)}>
|
||||
<>
|
||||
<form
|
||||
onSubmit={handleUpdate}
|
||||
className={`${baseClass}__save-modal-form`}
|
||||
autoComplete="off"
|
||||
>
|
||||
<InputField
|
||||
name="name"
|
||||
onChange={(value: string) => setName(value)}
|
||||
value={name}
|
||||
error={errors.name}
|
||||
inputClassName={`${baseClass}__query-save-modal-name`}
|
||||
label="Name"
|
||||
placeholder="What is your query called?"
|
||||
autofocus
|
||||
/>
|
||||
<InputField
|
||||
name="description"
|
||||
onChange={(value: string) => setDescription(value)}
|
||||
value={description}
|
||||
inputClassName={`${baseClass}__query-save-modal-description`}
|
||||
label="Description"
|
||||
type="textarea"
|
||||
placeholder="What information does your query reveal? (optional)"
|
||||
/>
|
||||
<Checkbox
|
||||
name="observerCanRun"
|
||||
onChange={setObserverCanRun}
|
||||
value={observerCanRun}
|
||||
wrapperClassName={`${baseClass}__query-save-modal-observer-can-run-wrapper`}
|
||||
>
|
||||
Observers can run
|
||||
</Checkbox>
|
||||
<p>
|
||||
Users with the Observer role will be able to run this query on hosts
|
||||
where they have access.
|
||||
</p>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="save-query-loading"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Save query
|
||||
</Button>
|
||||
<Button onClick={() => setIsSaveModalOpen(false)} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewQueryModal;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./NewQueryModal";
|
||||
@@ -26,7 +26,7 @@ import Checkbox from "components/forms/fields/Checkbox";
|
||||
import Spinner from "components/Spinner";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
|
||||
import NewQueryModal from "../NewQueryModal";
|
||||
import SaveQueryModal from "../SaveQueryModal";
|
||||
import InfoIcon from "../../../../../../assets/images/icon-info-purple-14x14@2x.png";
|
||||
|
||||
const baseClass = "query-form";
|
||||
@@ -34,12 +34,13 @@ const baseClass = "query-form";
|
||||
interface IQueryFormProps {
|
||||
router: InjectedRouter;
|
||||
queryIdForEdit: number | null;
|
||||
teamIdForQuery?: number;
|
||||
showOpenSchemaActionText: boolean;
|
||||
storedQuery: IQuery | undefined;
|
||||
isStoredQueryLoading: boolean;
|
||||
isQuerySaving: boolean;
|
||||
isQueryUpdating: boolean;
|
||||
onCreateQuery: (formData: IQueryFormData) => void;
|
||||
saveQuery: (formData: IQueryFormData) => void;
|
||||
onOsqueryTableSelect: (tableName: string) => void;
|
||||
goToSelectTargets: () => void;
|
||||
onUpdate: (formData: IQueryFormData) => void;
|
||||
@@ -63,12 +64,13 @@ const validateQuerySQL = (query: string) => {
|
||||
const QueryForm = ({
|
||||
router,
|
||||
queryIdForEdit,
|
||||
teamIdForQuery,
|
||||
showOpenSchemaActionText,
|
||||
storedQuery,
|
||||
isStoredQueryLoading,
|
||||
isQuerySaving,
|
||||
isQueryUpdating,
|
||||
onCreateQuery,
|
||||
saveQuery,
|
||||
onOsqueryTableSelect,
|
||||
goToSelectTargets,
|
||||
onUpdate,
|
||||
@@ -104,7 +106,7 @@ const QueryForm = ({
|
||||
|
||||
const savedQueryMode = !!queryIdForEdit;
|
||||
const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined
|
||||
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
|
||||
const [showSaveQueryModal, setShowSaveQueryModal] = useState(false);
|
||||
const [showQueryEditor, setShowQueryEditor] = useState(
|
||||
isObserverPlus || isAnyTeamObserverPlus || false
|
||||
);
|
||||
@@ -142,6 +144,10 @@ const QueryForm = ({
|
||||
storedQuery.author_id === currentUser.id
|
||||
: isAnyTeamMaintainerOrTeamAdmin;
|
||||
|
||||
const toggleSaveQueryModal = () => {
|
||||
setShowSaveQueryModal(!showSaveQueryModal);
|
||||
};
|
||||
|
||||
const onLoad = (editor: IAceEditor) => {
|
||||
editor.setOptions({
|
||||
enableLinking: true,
|
||||
@@ -260,7 +266,7 @@ const QueryForm = ({
|
||||
|
||||
if (valid) {
|
||||
if (!savedQueryMode) {
|
||||
setIsSaveModalOpen(true);
|
||||
setShowSaveQueryModal(true);
|
||||
} else {
|
||||
onUpdate({
|
||||
name: lastEditedQueryName,
|
||||
@@ -565,12 +571,12 @@ const QueryForm = ({
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{isSaveModalOpen && (
|
||||
<NewQueryModal
|
||||
baseClass={baseClass}
|
||||
{showSaveQueryModal && (
|
||||
<SaveQueryModal
|
||||
queryValue={lastEditedQueryBody}
|
||||
onCreateQuery={onCreateQuery}
|
||||
setIsSaveModalOpen={setIsSaveModalOpen}
|
||||
teamIdForQuery={teamIdForQuery}
|
||||
saveQuery={saveQuery}
|
||||
toggleSaveQueryModal={toggleSaveQueryModal}
|
||||
backendValidators={backendValidators}
|
||||
isLoading={isQuerySaving}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { pull, size } from "lodash";
|
||||
|
||||
import useDeepEffect from "hooks/useDeepEffect";
|
||||
|
||||
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 Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
import {
|
||||
FREQUENCY_DROPDOWN_OPTIONS,
|
||||
LOGGING_TYPE_OPTIONS,
|
||||
MIN_OSQUERY_VERSION_OPTIONS,
|
||||
SCHEDULE_PLATFORM_DROPDOWN_OPTIONS,
|
||||
} from "utilities/constants";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import { IPlatformString } from "interfaces/platform";
|
||||
import {
|
||||
ICreateQueryRequestBody,
|
||||
ISchedulableQuery,
|
||||
QueryLoggingOption,
|
||||
} from "interfaces/schedulable_query";
|
||||
|
||||
const baseClass = "save-query-modal";
|
||||
export interface ISaveQueryModalProps {
|
||||
queryValue: string;
|
||||
teamIdForQuery?: number; // query will be global if omitted
|
||||
isLoading: boolean;
|
||||
saveQuery: (formData: ICreateQueryRequestBody) => void;
|
||||
toggleSaveQueryModal: () => void;
|
||||
backendValidators: { [key: string]: string };
|
||||
existingQuery?: ISchedulableQuery;
|
||||
}
|
||||
|
||||
const validateQueryName = (name: string) => {
|
||||
const errors: { [key: string]: string } = {};
|
||||
|
||||
if (!name) {
|
||||
errors.name = "Query name must be present";
|
||||
}
|
||||
|
||||
const valid = !size(errors);
|
||||
return { valid, errors };
|
||||
};
|
||||
|
||||
const SaveQueryModal = ({
|
||||
queryValue,
|
||||
teamIdForQuery,
|
||||
isLoading,
|
||||
saveQuery,
|
||||
toggleSaveQueryModal,
|
||||
backendValidators,
|
||||
existingQuery,
|
||||
}: ISaveQueryModalProps): JSX.Element => {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [selectedFrequency, setSelectedFrequency] = useState(
|
||||
existingQuery?.interval ?? 3600
|
||||
);
|
||||
const [
|
||||
selectedPlatformOptions,
|
||||
setSelectedPlatformOptions,
|
||||
] = useState<IPlatformString>(existingQuery?.platform ?? "");
|
||||
const [
|
||||
selectedMinOsqueryVersionOptions,
|
||||
setSelectedMinOsqueryVersionOptions,
|
||||
] = useState(existingQuery?.min_osquery_version ?? "");
|
||||
const [
|
||||
selectedLoggingType,
|
||||
setSelectedLoggingType,
|
||||
] = useState<QueryLoggingOption>(existingQuery?.logging ?? "snapshot");
|
||||
const [observerCanRun, setObserverCanRun] = useState(false);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>(
|
||||
backendValidators
|
||||
);
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
|
||||
const toggleAdvancedOptions = () => {
|
||||
setShowAdvancedOptions(!showAdvancedOptions);
|
||||
};
|
||||
|
||||
useDeepEffect(() => {
|
||||
if (name) {
|
||||
setErrors({});
|
||||
}
|
||||
}, [name]);
|
||||
|
||||
useEffect(() => {
|
||||
setErrors(backendValidators);
|
||||
}, [backendValidators]);
|
||||
|
||||
const onClickSaveQuery = (evt: React.MouseEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { valid, errors: newErrors } = validateQueryName(name);
|
||||
setErrors({
|
||||
...errors,
|
||||
...newErrors,
|
||||
});
|
||||
|
||||
if (valid) {
|
||||
saveQuery({
|
||||
// from modal fields
|
||||
name,
|
||||
description,
|
||||
interval: selectedFrequency,
|
||||
observer_can_run: observerCanRun,
|
||||
platform: selectedPlatformOptions,
|
||||
min_osquery_version: selectedMinOsqueryVersionOptions,
|
||||
logging: selectedLoggingType,
|
||||
// from previous New query page
|
||||
query: queryValue,
|
||||
// from doubly previous ManageQueriesPage
|
||||
team_id: teamIdForQuery,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeSelectPlatformOptions = useCallback(
|
||||
(values: string) => {
|
||||
const valArray = values.split(",");
|
||||
|
||||
// Remove All if another OS is chosen
|
||||
// else if Remove OS if All is chosen
|
||||
if (valArray.indexOf("") === 0 && valArray.length > 1) {
|
||||
// TODO - inmprove type safety of all 3 options
|
||||
setSelectedPlatformOptions(
|
||||
pull(valArray, "").join(",") as IPlatformString
|
||||
);
|
||||
} else if (valArray.length > 1 && valArray.indexOf("") > -1) {
|
||||
setSelectedPlatformOptions("");
|
||||
} else {
|
||||
setSelectedPlatformOptions(values as IPlatformString);
|
||||
}
|
||||
},
|
||||
[setSelectedPlatformOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal title={"Save query"} onExit={toggleSaveQueryModal}>
|
||||
<>
|
||||
<form
|
||||
onSubmit={onClickSaveQuery}
|
||||
className={baseClass}
|
||||
autoComplete="off"
|
||||
>
|
||||
<InputField
|
||||
name="name"
|
||||
onChange={(value: string) => setName(value)}
|
||||
value={name}
|
||||
error={errors.name}
|
||||
inputClassName={`${baseClass}__name`}
|
||||
label="Name"
|
||||
placeholder="What is your query called?"
|
||||
autofocus
|
||||
/>
|
||||
<InputField
|
||||
name="description"
|
||||
onChange={(value: string) => setDescription(value)}
|
||||
value={description}
|
||||
inputClassName={`${baseClass}__description`}
|
||||
label="Description"
|
||||
type="textarea"
|
||||
placeholder="What information does your query reveal? (optional)"
|
||||
/>
|
||||
<div>
|
||||
<Dropdown
|
||||
searchable={false}
|
||||
options={FREQUENCY_DROPDOWN_OPTIONS}
|
||||
onChange={(value: number) => {
|
||||
setSelectedFrequency(value);
|
||||
}}
|
||||
placeholder={"Every hour"}
|
||||
value={selectedFrequency}
|
||||
label="Frequency"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
|
||||
/>
|
||||
<p className="help-text">
|
||||
If automations are on, this is how often your query collects data.
|
||||
</p>
|
||||
</div>
|
||||
<Checkbox
|
||||
name="observerCanRun"
|
||||
onChange={setObserverCanRun}
|
||||
value={observerCanRun}
|
||||
wrapperClassName={`${baseClass}__observer-can-run-wrapper`}
|
||||
>
|
||||
Observers can run
|
||||
</Checkbox>
|
||||
<p className="help-text">
|
||||
Users with the Observer role will be able to run this query as a
|
||||
live query.
|
||||
</p>
|
||||
<RevealButton
|
||||
isShowing={showAdvancedOptions}
|
||||
className={`${baseClass}__advanced-options-toggle`}
|
||||
hideText={"Hide advanced options"}
|
||||
showText={"Show advanced options"}
|
||||
caretPosition={"after"}
|
||||
onClick={toggleAdvancedOptions}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<>
|
||||
<Dropdown
|
||||
options={SCHEDULE_PLATFORM_DROPDOWN_OPTIONS}
|
||||
placeholder="Select"
|
||||
label="Platforms"
|
||||
onChange={onChangeSelectPlatformOptions}
|
||||
value={selectedPlatformOptions}
|
||||
multi
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--platform`}
|
||||
/>
|
||||
<p className="help-text">
|
||||
If automations are turned on, your query collects data on
|
||||
compatible platforms.
|
||||
<br />
|
||||
If you want more control, override platforms.
|
||||
</p>
|
||||
<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`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="save-query-loading"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={toggleSaveQueryModal} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveQueryModal;
|
||||
@@ -0,0 +1,32 @@
|
||||
.save-query-modal {
|
||||
.fleet-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin-top: $pad-small;
|
||||
margin-bottom: $pad-large;
|
||||
font-weight: $regular;
|
||||
font-size: 0.75rem;
|
||||
color: $ui-fleet-black-75;
|
||||
}
|
||||
|
||||
&__form-field {
|
||||
&--frequency {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&--platform {
|
||||
margin-bottom: 0;
|
||||
margin-top: $pad-large;
|
||||
}
|
||||
}
|
||||
|
||||
&__observer-can-run-wrapper {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__advanced-options-toggle {
|
||||
font-weight: $xbold;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./SaveQueryModal";
|
||||
@@ -8,6 +8,10 @@ import { AppContext } from "context/app";
|
||||
import { QueryContext } from "context/query";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { IQueryFormData, IQuery } from "interfaces/query";
|
||||
import {
|
||||
ICreateQueryRequestBody,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
import PATHS from "router/paths";
|
||||
import debounce from "utilities/debounce";
|
||||
import deepDifference from "utilities/deep_difference";
|
||||
@@ -19,15 +23,15 @@ interface IQueryEditorProps {
|
||||
router: InjectedRouter;
|
||||
baseClass: string;
|
||||
queryIdForEdit: number | null;
|
||||
teamIdForQuery?: number;
|
||||
storedQuery: IQuery | undefined;
|
||||
storedQueryError: Error | null;
|
||||
showOpenSchemaActionText: boolean;
|
||||
isStoredQueryLoading: boolean;
|
||||
createQuery: UseMutateAsyncFunction<
|
||||
{ query: IQuery },
|
||||
ISchedulableQuery,
|
||||
unknown,
|
||||
IQueryFormData,
|
||||
unknown
|
||||
ICreateQueryRequestBody
|
||||
>;
|
||||
onOsqueryTableSelect: (tableName: string) => void;
|
||||
goToSelectTargets: () => void;
|
||||
@@ -39,6 +43,7 @@ const QueryEditor = ({
|
||||
router,
|
||||
baseClass,
|
||||
queryIdForEdit,
|
||||
teamIdForQuery,
|
||||
storedQuery,
|
||||
storedQueryError,
|
||||
showOpenSchemaActionText,
|
||||
@@ -77,10 +82,10 @@ const QueryEditor = ({
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
|
||||
const onSaveQueryFormSubmit = debounce(async (formData: IQueryFormData) => {
|
||||
const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => {
|
||||
setIsQuerySaving(true);
|
||||
try {
|
||||
const { query }: { query: IQuery } = await createQuery(formData);
|
||||
const query = await createQuery(formData);
|
||||
router.push(PATHS.EDIT_QUERY(query));
|
||||
renderFlash("success", "Query created!");
|
||||
setBackendValidators({});
|
||||
@@ -149,12 +154,13 @@ const QueryEditor = ({
|
||||
</div>
|
||||
<QueryForm
|
||||
router={router}
|
||||
onCreateQuery={onSaveQueryFormSubmit}
|
||||
saveQuery={saveQuery}
|
||||
goToSelectTargets={goToSelectTargets}
|
||||
onOsqueryTableSelect={onOsqueryTableSelect}
|
||||
onUpdate={onUpdateQuery}
|
||||
storedQuery={storedQuery}
|
||||
queryIdForEdit={queryIdForEdit}
|
||||
teamIdForQuery={teamIdForQuery}
|
||||
isStoredQueryLoading={isStoredQueryLoading}
|
||||
showOpenSchemaActionText={showOpenSchemaActionText}
|
||||
onOpenSchemaSidebar={onOpenSchemaSidebar}
|
||||
|
||||
@@ -109,7 +109,8 @@ export default {
|
||||
MANAGE_POLICIES: `${URL_PREFIX}/policies/manage`,
|
||||
NEW_LABEL: `${URL_PREFIX}/labels/new`,
|
||||
NEW_POLICY: `${URL_PREFIX}/policies/new`,
|
||||
NEW_QUERY: `${URL_PREFIX}/queries/new`,
|
||||
NEW_QUERY: (teamId?: number) =>
|
||||
`${URL_PREFIX}/queries/new${teamId ? `?team_id=${teamId}` : ""}`,
|
||||
RESET_PASSWORD: `${URL_PREFIX}/login/reset`,
|
||||
SETUP: `${URL_PREFIX}/setup`,
|
||||
USER_SETTINGS: `${URL_PREFIX}/profile`,
|
||||
|
||||
@@ -4,21 +4,14 @@ import endpoints from "utilities/endpoints";
|
||||
import { IQueryFormData } from "interfaces/query";
|
||||
import { ISelectedTargets } from "interfaces/target";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { ICreateQueryRequestBody } from "interfaces/schedulable_query";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
|
||||
// Mock API requests to be used in developing FE for #7765 in parallel with BE development
|
||||
// import { sendRequest } from "services/mock_service/service/service";
|
||||
|
||||
export default {
|
||||
create: ({ description, name, query, observer_can_run }: IQueryFormData) => {
|
||||
create: (createQueryRequestBody: ICreateQueryRequestBody) => {
|
||||
const { QUERIES } = endpoints;
|
||||
|
||||
return sendRequest("POST", QUERIES, {
|
||||
description,
|
||||
name,
|
||||
query,
|
||||
observer_can_run,
|
||||
});
|
||||
return sendRequest("POST", QUERIES, createQueryRequestBody);
|
||||
},
|
||||
destroy: (id: string | number) => {
|
||||
const { QUERIES } = endpoints;
|
||||
|
||||
@@ -23,7 +23,11 @@ export const DEFAULT_GRAVATAR_LINK_DARK_FALLBACK =
|
||||
"/assets/images/icon-avatar-default-dark-24x24%402x.png";
|
||||
|
||||
export const FREQUENCY_DROPDOWN_OPTIONS = [
|
||||
{ value: 0, label: "Never" },
|
||||
{ value: 300, label: "Every 5 minutes" },
|
||||
{ value: 600, label: "Every 10 minutes" },
|
||||
{ value: 900, label: "Every 15 minutes" },
|
||||
{ value: 1800, label: "Every 30 minutes" },
|
||||
{ value: 3600, label: "Every hour" },
|
||||
{ value: 21600, label: "Every 6 hours" },
|
||||
{ value: 43200, label: "Every 12 hours" },
|
||||
|
||||
Reference in New Issue
Block a user