897 lines
30 KiB
TypeScript
897 lines
30 KiB
TypeScript
import React, {
|
|
useState,
|
|
useContext,
|
|
useEffect,
|
|
KeyboardEvent,
|
|
useCallback,
|
|
} from "react";
|
|
import { InjectedRouter } from "react-router";
|
|
import { pull, size } from "lodash";
|
|
import classnames from "classnames";
|
|
import { useDebouncedCallback } from "use-debounce";
|
|
import { COLORS } from "styles/var/colors";
|
|
|
|
import PATHS from "router/paths";
|
|
import { AppContext } from "context/app";
|
|
import { QueryContext } from "context/query";
|
|
import { NotificationContext } from "context/notification";
|
|
import {
|
|
addGravatarUrlToResource,
|
|
secondsToDhms,
|
|
TAGGED_TEMPLATES,
|
|
} from "utilities/helpers";
|
|
import {
|
|
FREQUENCY_DROPDOWN_OPTIONS,
|
|
SCHEDULE_PLATFORM_DROPDOWN_OPTIONS,
|
|
MIN_OSQUERY_VERSION_OPTIONS,
|
|
LOGGING_TYPE_OPTIONS,
|
|
} from "utilities/constants";
|
|
import usePlatformCompatibility from "hooks/usePlatformCompatibility";
|
|
import { IApiError } from "interfaces/errors";
|
|
import {
|
|
ISchedulableQuery,
|
|
ICreateQueryRequestBody,
|
|
QueryLoggingOption,
|
|
} from "interfaces/schedulable_query";
|
|
import { SelectedPlatformString } from "interfaces/platform";
|
|
import queryAPI from "services/entities/queries";
|
|
|
|
import { IAceEditor } from "react-ace/lib/types";
|
|
import ReactTooltip from "react-tooltip";
|
|
|
|
import Avatar from "components/Avatar";
|
|
import FleetAce from "components/FleetAce";
|
|
// @ts-ignore
|
|
import validateQuery from "components/forms/validators/validate_query";
|
|
import Button from "components/buttons/Button";
|
|
import RevealButton from "components/buttons/RevealButton";
|
|
import Checkbox from "components/forms/fields/Checkbox";
|
|
// @ts-ignore
|
|
import Dropdown from "components/forms/fields/Dropdown";
|
|
import Spinner from "components/Spinner";
|
|
import Icon from "components/Icon/Icon";
|
|
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
|
|
import SaveQueryModal from "../SaveQueryModal";
|
|
import ConfirmSaveChangesModal from "../ConfirmSaveChangesModal";
|
|
import DiscardDataOption from "../DiscardDataOption";
|
|
|
|
const baseClass = "edit-query-form";
|
|
|
|
interface IEditQueryFormProps {
|
|
router: InjectedRouter;
|
|
queryIdForEdit: number | null;
|
|
apiTeamIdForQuery?: number;
|
|
teamNameForQuery?: string;
|
|
showOpenSchemaActionText: boolean;
|
|
storedQuery: ISchedulableQuery | undefined;
|
|
isStoredQueryLoading: boolean;
|
|
isQuerySaving: boolean;
|
|
isQueryUpdating: boolean;
|
|
onSubmitNewQuery: (formData: ICreateQueryRequestBody) => void;
|
|
onOsqueryTableSelect: (tableName: string) => void;
|
|
onUpdate: (formData: ICreateQueryRequestBody) => void;
|
|
onOpenSchemaSidebar: () => void;
|
|
renderLiveQueryWarning: () => JSX.Element | null;
|
|
backendValidators: { [key: string]: string };
|
|
hostId?: number;
|
|
queryReportsDisabled?: boolean;
|
|
showConfirmSaveChangesModal: boolean;
|
|
setShowConfirmSaveChangesModal: (bool: boolean) => void;
|
|
}
|
|
|
|
const validateQuerySQL = (query: string) => {
|
|
const errors: { [key: string]: string } = {};
|
|
const { error: queryError, valid: queryValid } = validateQuery(query);
|
|
|
|
if (!queryValid) {
|
|
errors.query = queryError;
|
|
}
|
|
|
|
const valid = !size(errors);
|
|
return { valid, errors };
|
|
};
|
|
|
|
// Includes a custom frequency set through fleetctl at top of frequency dropdown
|
|
const customFrequencyOptions = (frequency: number) => {
|
|
if (
|
|
!FREQUENCY_DROPDOWN_OPTIONS.some((option) => option.value === frequency)
|
|
) {
|
|
return [
|
|
{
|
|
value: frequency,
|
|
label: `Every ${secondsToDhms(frequency)}`,
|
|
},
|
|
...FREQUENCY_DROPDOWN_OPTIONS,
|
|
];
|
|
}
|
|
return FREQUENCY_DROPDOWN_OPTIONS;
|
|
};
|
|
|
|
const EditQueryForm = ({
|
|
router,
|
|
queryIdForEdit,
|
|
apiTeamIdForQuery,
|
|
teamNameForQuery,
|
|
showOpenSchemaActionText,
|
|
storedQuery,
|
|
isStoredQueryLoading,
|
|
isQuerySaving,
|
|
isQueryUpdating,
|
|
onSubmitNewQuery,
|
|
onOsqueryTableSelect,
|
|
onUpdate,
|
|
onOpenSchemaSidebar,
|
|
renderLiveQueryWarning,
|
|
backendValidators,
|
|
hostId,
|
|
queryReportsDisabled,
|
|
showConfirmSaveChangesModal,
|
|
setShowConfirmSaveChangesModal,
|
|
}: IEditQueryFormProps): JSX.Element => {
|
|
// Note: The QueryContext values should always be used for any mutable query data such as query name
|
|
// The storedQuery prop should only be used to access immutable metadata such as author id
|
|
const {
|
|
lastEditedQueryId,
|
|
lastEditedQueryName,
|
|
lastEditedQueryDescription,
|
|
lastEditedQueryBody,
|
|
lastEditedQueryObserverCanRun,
|
|
lastEditedQueryFrequency,
|
|
lastEditedQueryPlatforms,
|
|
lastEditedQueryMinOsqueryVersion,
|
|
lastEditedQueryLoggingType,
|
|
lastEditedQueryDiscardData,
|
|
setLastEditedQueryName,
|
|
setLastEditedQueryDescription,
|
|
setLastEditedQueryBody,
|
|
setLastEditedQueryObserverCanRun,
|
|
setLastEditedQueryFrequency,
|
|
setLastEditedQueryPlatforms,
|
|
setLastEditedQueryMinOsqueryVersion,
|
|
setLastEditedQueryLoggingType,
|
|
setLastEditedQueryDiscardData,
|
|
setEditingExistingQuery,
|
|
} = useContext(QueryContext);
|
|
|
|
const {
|
|
currentUser,
|
|
isOnlyObserver,
|
|
isGlobalObserver,
|
|
isAnyTeamMaintainerOrTeamAdmin,
|
|
isGlobalAdmin,
|
|
isGlobalMaintainer,
|
|
isObserverPlus,
|
|
isAnyTeamObserverPlus,
|
|
config,
|
|
} = useContext(AppContext);
|
|
const { renderFlash } = useContext(NotificationContext);
|
|
|
|
const savedQueryMode = !!queryIdForEdit;
|
|
const disabledLiveQuery = config?.server_settings.live_query_disabled;
|
|
const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined
|
|
// NOTE: SaveQueryModal is only being used to create a new query in this component.
|
|
// It's easy to confuse with other names like promptSaveQuery, promptSaveAsNewQuery, etc.,
|
|
// which are used in connection with existing (i.e. previously saved) queries rather
|
|
// than new queries. Consider renaming some things to distinguish the various flows.
|
|
const [showSaveQueryModal, setShowSaveQueryModal] = useState(false);
|
|
const [showQueryEditor, setShowQueryEditor] = useState(
|
|
isObserverPlus || isAnyTeamObserverPlus || false
|
|
);
|
|
const [isEditingName, setIsEditingName] = useState(false);
|
|
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
|
const [isSaveAsNewLoading, setIsSaveAsNewLoading] = useState(false);
|
|
const [frequencyOptions, setFrequencyOptions] = useState(
|
|
FREQUENCY_DROPDOWN_OPTIONS
|
|
);
|
|
const [isInitialFrequency, setIsInitialFrequency] = useState(true);
|
|
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
|
|
|
const platformCompatibility = usePlatformCompatibility();
|
|
const { setCompatiblePlatforms } = platformCompatibility;
|
|
|
|
const debounceSQL = useDebouncedCallback((sql: string) => {
|
|
const { errors: newErrors } = validateQuerySQL(sql);
|
|
|
|
setErrors({
|
|
...newErrors,
|
|
});
|
|
}, 500);
|
|
|
|
queryIdForEdit = queryIdForEdit || 0;
|
|
|
|
useEffect(() => {
|
|
if (!isStoredQueryLoading && queryIdForEdit === lastEditedQueryId) {
|
|
setCompatiblePlatforms(lastEditedQueryBody);
|
|
}
|
|
debounceSQL(lastEditedQueryBody);
|
|
}, [lastEditedQueryBody, lastEditedQueryId, isStoredQueryLoading]);
|
|
|
|
// Creates custom frequency options when initializing and not when toggling
|
|
useEffect(() => {
|
|
if (isInitialFrequency) {
|
|
setFrequencyOptions(customFrequencyOptions(lastEditedQueryFrequency));
|
|
}
|
|
}, [lastEditedQueryFrequency, isInitialFrequency]);
|
|
|
|
const toggleSaveQueryModal = () => {
|
|
setShowSaveQueryModal(!showSaveQueryModal);
|
|
};
|
|
|
|
const toggleConfirmSaveChangesModal = () => {
|
|
setShowConfirmSaveChangesModal(!showConfirmSaveChangesModal);
|
|
};
|
|
|
|
const onLoad = (editor: IAceEditor) => {
|
|
editor.setOptions({
|
|
enableLinking: true,
|
|
enableMultiselect: false, // Disables command + click creating multiple cursors
|
|
});
|
|
|
|
// @ts-expect-error
|
|
// the string "linkClick" is not officially in the lib but we need it
|
|
editor.on("linkClick", (data: EditorSession) => {
|
|
const { type, value } = data.token;
|
|
|
|
if (type === "osquery-token") {
|
|
return onOsqueryTableSelect(value);
|
|
}
|
|
|
|
return false;
|
|
});
|
|
};
|
|
|
|
const onChangeQuery = (sqlString: string) => {
|
|
setLastEditedQueryBody(sqlString);
|
|
};
|
|
|
|
const onInputKeypress = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
if (event.key.toLowerCase() === "enter" && !event.shiftKey) {
|
|
event.preventDefault();
|
|
event.currentTarget.blur();
|
|
setIsEditingName(false);
|
|
setIsEditingDescription(false);
|
|
}
|
|
};
|
|
|
|
const onChangeSelectFrequency = useCallback(
|
|
(value: number) => {
|
|
setLastEditedQueryFrequency(value);
|
|
setIsInitialFrequency(false);
|
|
},
|
|
[setLastEditedQueryFrequency]
|
|
);
|
|
|
|
const toggleAdvancedOptions = () => {
|
|
setShowAdvancedOptions(!showAdvancedOptions);
|
|
};
|
|
|
|
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) {
|
|
setLastEditedQueryPlatforms(
|
|
pull(valArray, "").join(",") as SelectedPlatformString
|
|
);
|
|
} else if (valArray.length > 1 && valArray.indexOf("") > -1) {
|
|
setLastEditedQueryPlatforms("");
|
|
} else {
|
|
setLastEditedQueryPlatforms(values as SelectedPlatformString);
|
|
}
|
|
},
|
|
[setLastEditedQueryPlatforms]
|
|
);
|
|
|
|
const onChangeMinOsqueryVersionOptions = useCallback(
|
|
(value: string) => {
|
|
setLastEditedQueryMinOsqueryVersion(value);
|
|
},
|
|
[setLastEditedQueryMinOsqueryVersion]
|
|
);
|
|
|
|
const onChangeSelectLoggingType = useCallback(
|
|
(value: QueryLoggingOption) => {
|
|
setLastEditedQueryLoggingType(value);
|
|
},
|
|
[setLastEditedQueryLoggingType]
|
|
);
|
|
|
|
const promptSaveAsNewQuery = () => (
|
|
evt: React.MouseEvent<HTMLButtonElement>
|
|
) => {
|
|
evt.preventDefault();
|
|
|
|
if (savedQueryMode && !lastEditedQueryName) {
|
|
return setErrors({
|
|
...errors,
|
|
name: "Query name must be present",
|
|
});
|
|
}
|
|
|
|
let valid = true;
|
|
const { valid: isValidated } = validateQuerySQL(lastEditedQueryBody);
|
|
|
|
valid = isValidated;
|
|
|
|
if (valid) {
|
|
setIsSaveAsNewLoading(true);
|
|
queryAPI
|
|
.create({
|
|
name: lastEditedQueryName,
|
|
description: lastEditedQueryDescription,
|
|
query: lastEditedQueryBody,
|
|
team_id: apiTeamIdForQuery,
|
|
observer_can_run: lastEditedQueryObserverCanRun,
|
|
interval: lastEditedQueryFrequency,
|
|
platform: lastEditedQueryPlatforms,
|
|
min_osquery_version: lastEditedQueryMinOsqueryVersion,
|
|
logging: lastEditedQueryLoggingType,
|
|
})
|
|
.then((response: { query: ISchedulableQuery }) => {
|
|
setIsSaveAsNewLoading(false);
|
|
router.push(
|
|
PATHS.QUERY_DETAILS(
|
|
response.query.id,
|
|
response.query.team_id ?? undefined
|
|
)
|
|
);
|
|
renderFlash("success", `Successfully added query.`);
|
|
})
|
|
.catch((createError: { data: IApiError }) => {
|
|
if (createError.data.errors[0].reason.includes("already exists")) {
|
|
queryAPI
|
|
.create({
|
|
name: `Copy of ${lastEditedQueryName}`,
|
|
description: lastEditedQueryDescription,
|
|
query: lastEditedQueryBody,
|
|
team_id: apiTeamIdForQuery,
|
|
observer_can_run: lastEditedQueryObserverCanRun,
|
|
interval: lastEditedQueryFrequency,
|
|
platform: lastEditedQueryPlatforms,
|
|
min_osquery_version: lastEditedQueryMinOsqueryVersion,
|
|
logging: lastEditedQueryLoggingType,
|
|
})
|
|
.then((response: { query: ISchedulableQuery }) => {
|
|
setIsSaveAsNewLoading(false);
|
|
router.push(PATHS.EDIT_QUERY(response.query.id));
|
|
renderFlash(
|
|
"success",
|
|
`Successfully added query as "Copy of ${lastEditedQueryName}".`
|
|
);
|
|
})
|
|
.catch((createCopyError: { data: IApiError }) => {
|
|
if (
|
|
createCopyError.data.errors[0].reason.includes(
|
|
"already exists"
|
|
)
|
|
) {
|
|
let teamErrorText;
|
|
if (apiTeamIdForQuery !== 0) {
|
|
if (teamNameForQuery) {
|
|
teamErrorText = `the ${teamNameForQuery} team`;
|
|
} else {
|
|
teamErrorText = "this team";
|
|
}
|
|
} else {
|
|
teamErrorText = "all teams";
|
|
}
|
|
renderFlash(
|
|
"error",
|
|
`A query called "Copy of ${lastEditedQueryName}" already exists for ${teamErrorText}.`
|
|
);
|
|
}
|
|
setIsSaveAsNewLoading(false);
|
|
});
|
|
} else {
|
|
setIsSaveAsNewLoading(false);
|
|
renderFlash("error", "Could not create query. Please try again.");
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const promptSaveQuery = () => (evt: React.MouseEvent<HTMLButtonElement>) => {
|
|
evt.preventDefault();
|
|
|
|
if (savedQueryMode && !lastEditedQueryName) {
|
|
return setErrors({
|
|
...errors,
|
|
name: "Query name must be present",
|
|
});
|
|
}
|
|
|
|
let valid = true;
|
|
const { valid: isValidated } = validateQuerySQL(lastEditedQueryBody);
|
|
|
|
valid = isValidated;
|
|
|
|
if (valid) {
|
|
if (!savedQueryMode) {
|
|
setShowSaveQueryModal(true);
|
|
} else {
|
|
onUpdate({
|
|
name: lastEditedQueryName,
|
|
description: lastEditedQueryDescription,
|
|
query: lastEditedQueryBody,
|
|
observer_can_run: lastEditedQueryObserverCanRun,
|
|
interval: lastEditedQueryFrequency,
|
|
platform: lastEditedQueryPlatforms,
|
|
min_osquery_version: lastEditedQueryMinOsqueryVersion,
|
|
logging: lastEditedQueryLoggingType,
|
|
discard_data: lastEditedQueryDiscardData,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const renderAuthor = (): JSX.Element | null => {
|
|
return storedQuery ? (
|
|
<>
|
|
<b>Author</b>
|
|
<div>
|
|
<Avatar
|
|
user={addGravatarUrlToResource({
|
|
email: storedQuery.author_email,
|
|
})}
|
|
size="xsmall"
|
|
/>
|
|
<span>
|
|
{storedQuery.author_name === currentUser?.name
|
|
? "You"
|
|
: storedQuery.author_name}
|
|
</span>
|
|
</div>
|
|
</>
|
|
) : null;
|
|
};
|
|
|
|
const renderLabelComponent = (): JSX.Element | null => {
|
|
if (!showOpenSchemaActionText) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Button variant="text-icon" onClick={onOpenSchemaSidebar}>
|
|
<>
|
|
<Icon name="info" size="small" />
|
|
Show schema
|
|
</>
|
|
</Button>
|
|
);
|
|
};
|
|
|
|
const renderPlatformCompatibility = () => {
|
|
if (isStoredQueryLoading || queryIdForEdit !== lastEditedQueryId) {
|
|
return null;
|
|
}
|
|
|
|
return platformCompatibility.render();
|
|
};
|
|
|
|
const queryNameClasses = classnames("query-name-wrapper", {
|
|
[`${baseClass}--editing`]: isEditingName,
|
|
});
|
|
|
|
const queryDescriptionClasses = classnames("query-description-wrapper", {
|
|
[`${baseClass}--editing`]: isEditingDescription,
|
|
});
|
|
|
|
const renderName = () => {
|
|
if (savedQueryMode) {
|
|
return (
|
|
<>
|
|
<div className={queryNameClasses}>
|
|
<AutoSizeInputField
|
|
name="query-name"
|
|
placeholder="Add name here"
|
|
value={lastEditedQueryName}
|
|
inputClassName={`${baseClass}__query-name`}
|
|
maxLength="160"
|
|
hasError={errors && errors.name}
|
|
onChange={setLastEditedQueryName}
|
|
onFocus={() => setIsEditingName(true)}
|
|
onBlur={() => setIsEditingName(false)}
|
|
onKeyPress={onInputKeypress}
|
|
isFocused={isEditingName}
|
|
/>
|
|
<Button
|
|
variant="text-icon"
|
|
className="edit-link"
|
|
onClick={() => setIsEditingName(true)}
|
|
>
|
|
<Icon
|
|
name="pencil"
|
|
className={`edit-icon ${isEditingName ? "hide" : ""}`}
|
|
/>
|
|
</Button>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return <h1 className={`${baseClass}__query-name no-hover`}>New query</h1>;
|
|
};
|
|
|
|
const renderDescription = () => {
|
|
if (savedQueryMode) {
|
|
return (
|
|
<>
|
|
<div className={queryDescriptionClasses}>
|
|
<AutoSizeInputField
|
|
name="query-description"
|
|
placeholder="Add description here."
|
|
value={lastEditedQueryDescription}
|
|
maxLength="250"
|
|
inputClassName={`${baseClass}__query-description`}
|
|
onChange={setLastEditedQueryDescription}
|
|
onFocus={() => setIsEditingDescription(true)}
|
|
onBlur={() => setIsEditingDescription(false)}
|
|
onKeyPress={onInputKeypress}
|
|
isFocused={isEditingDescription}
|
|
/>
|
|
<Button
|
|
variant="text-icon"
|
|
className="edit-link"
|
|
onClick={() => setIsEditingDescription(true)}
|
|
>
|
|
<Icon
|
|
name="pencil"
|
|
className={`edit-icon ${isEditingDescription ? "hide" : ""}`}
|
|
/>
|
|
</Button>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Observers and observer+ of existing query
|
|
const renderNonEditableForm = (
|
|
<form className={`${baseClass}`}>
|
|
<div className={`${baseClass}__title-bar`}>
|
|
<div className="name-description">
|
|
<h1 className={`${baseClass}__query-name no-hover`}>
|
|
{lastEditedQueryName}
|
|
</h1>
|
|
<p className={`${baseClass}__query-description no-hover`}>
|
|
{lastEditedQueryDescription}
|
|
</p>
|
|
</div>
|
|
<div className="author">{renderAuthor()}</div>
|
|
</div>
|
|
{((!isObserverPlus && isGlobalObserver) || !isAnyTeamObserverPlus) && (
|
|
<RevealButton
|
|
isShowing={showQueryEditor}
|
|
className={baseClass}
|
|
hideText="Hide SQL"
|
|
showText="Show SQL"
|
|
onClick={() => setShowQueryEditor(!showQueryEditor)}
|
|
/>
|
|
)}
|
|
{showQueryEditor && (
|
|
<FleetAce
|
|
value={lastEditedQueryBody}
|
|
name="query editor"
|
|
label="Query"
|
|
wrapperClassName={`${baseClass}__text-editor-wrapper`}
|
|
readOnly={
|
|
(!isObserverPlus && !isAnyTeamObserverPlus) || savedQueryMode
|
|
}
|
|
labelActionComponent={isObserverPlus && renderLabelComponent()}
|
|
wrapEnabled
|
|
data-testid="ace-editor"
|
|
/>
|
|
)}
|
|
<span className={`${baseClass}__platform-compatibility`}>
|
|
{renderPlatformCompatibility()}
|
|
</span>
|
|
{renderLiveQueryWarning()}
|
|
{(lastEditedQueryObserverCanRun ||
|
|
isObserverPlus ||
|
|
isAnyTeamObserverPlus) && (
|
|
<div className={`button-wrap ${baseClass}__button-wrap--new-query`}>
|
|
<div
|
|
data-tip
|
|
data-for="live-query-button"
|
|
// Tooltip shows when live queries are globally disabled
|
|
data-tip-disable={!disabledLiveQuery}
|
|
>
|
|
<Button
|
|
className={`${baseClass}__run`}
|
|
variant="blue-green"
|
|
onClick={() => {
|
|
router.push(
|
|
PATHS.LIVE_QUERY(queryIdForEdit) +
|
|
TAGGED_TEMPLATES.queryByHostRoute(hostId)
|
|
);
|
|
}}
|
|
disabled={disabledLiveQuery}
|
|
>
|
|
Live query
|
|
</Button>
|
|
</div>
|
|
<ReactTooltip
|
|
className={`live-query-button-tooltip`}
|
|
place="top"
|
|
effect="solid"
|
|
backgroundColor={COLORS["tooltip-bg"]}
|
|
id="live-query-button"
|
|
data-html
|
|
>
|
|
Live queries are disabled in organization settings
|
|
</ReactTooltip>
|
|
</div>
|
|
)}
|
|
</form>
|
|
);
|
|
|
|
const hasSavePermissions = isGlobalAdmin || isGlobalMaintainer;
|
|
|
|
const currentlySavingQueryResults =
|
|
storedQuery &&
|
|
!storedQuery.discard_data &&
|
|
!["differential", "differential_ignore_removals"].includes(
|
|
storedQuery.logging
|
|
);
|
|
const changedSQL = storedQuery && lastEditedQueryBody !== storedQuery.query;
|
|
const changedLoggingToDifferential = [
|
|
"differential",
|
|
"differential_ignore_removals",
|
|
].includes(lastEditedQueryLoggingType);
|
|
|
|
const enabledDiscardData =
|
|
storedQuery && lastEditedQueryDiscardData && !storedQuery.discard_data;
|
|
|
|
const confirmChanges =
|
|
currentlySavingQueryResults &&
|
|
(changedSQL || changedLoggingToDifferential || enabledDiscardData);
|
|
|
|
const showChangedSQLCopy =
|
|
changedSQL && !changedLoggingToDifferential && !enabledDiscardData;
|
|
|
|
// Global admin, any maintainer, any observer+ on new query
|
|
const renderEditableQueryForm = () => {
|
|
// Save disabled for team maintainer/admins viewing global queries
|
|
const disableSavePermissionDenied =
|
|
isAnyTeamMaintainerOrTeamAdmin &&
|
|
!storedQuery?.team_id &&
|
|
!!queryIdForEdit;
|
|
|
|
// Save and save as new disabled for query name blank on existing query or sql errors
|
|
const disableSaveFormErrors =
|
|
(lastEditedQueryName === "" && !!lastEditedQueryId) || !!size(errors);
|
|
|
|
return (
|
|
<>
|
|
<form className={`${baseClass}`} autoComplete="off">
|
|
<div className={`${baseClass}__title-bar`}>
|
|
<div className="name-description">
|
|
{renderName()}
|
|
{renderDescription()}
|
|
</div>
|
|
<div className="author">{savedQueryMode && renderAuthor()}</div>
|
|
</div>
|
|
<FleetAce
|
|
value={lastEditedQueryBody}
|
|
error={errors.query}
|
|
label="Query"
|
|
labelActionComponent={renderLabelComponent()}
|
|
name="query editor"
|
|
onLoad={onLoad}
|
|
wrapperClassName={`${baseClass}__text-editor-wrapper form-field`}
|
|
onChange={onChangeQuery}
|
|
handleSubmit={
|
|
confirmChanges ? toggleConfirmSaveChangesModal : promptSaveQuery
|
|
}
|
|
wrapEnabled
|
|
focus={!savedQueryMode}
|
|
/>
|
|
<span className={`${baseClass}__platform-compatibility`}>
|
|
{renderPlatformCompatibility()}
|
|
</span>
|
|
{savedQueryMode && (
|
|
<>
|
|
<Dropdown
|
|
searchable={false}
|
|
options={frequencyOptions}
|
|
onChange={onChangeSelectFrequency}
|
|
placeholder={"Every day"}
|
|
value={lastEditedQueryFrequency}
|
|
label={"Frequency"}
|
|
wrapperClassName={`${baseClass}__form-field form-field--frequency`}
|
|
helpText="This is how often your query collects data."
|
|
/>
|
|
<Checkbox
|
|
value={lastEditedQueryObserverCanRun}
|
|
onChange={(value: boolean) =>
|
|
setLastEditedQueryObserverCanRun(value)
|
|
}
|
|
helpText="Users with the observer role will be able to run this query on hosts where they have access."
|
|
>
|
|
Observers can run
|
|
</Checkbox>
|
|
<RevealButton
|
|
isShowing={showAdvancedOptions}
|
|
className={"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="Platform"
|
|
onChange={onChangeSelectPlatformOptions}
|
|
value={lastEditedQueryPlatforms}
|
|
multi
|
|
wrapperClassName={`${baseClass}__form-field form-field--platform`}
|
|
helpText="By default, your query collects data on all compatible platforms."
|
|
/>
|
|
<Dropdown
|
|
options={MIN_OSQUERY_VERSION_OPTIONS}
|
|
onChange={onChangeMinOsqueryVersionOptions}
|
|
placeholder="Select"
|
|
value={lastEditedQueryMinOsqueryVersion}
|
|
label="Minimum osquery version"
|
|
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}
|
|
/>
|
|
<Dropdown
|
|
options={LOGGING_TYPE_OPTIONS}
|
|
onChange={onChangeSelectLoggingType}
|
|
placeholder="Select"
|
|
value={lastEditedQueryLoggingType}
|
|
label="Logging"
|
|
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--logging`}
|
|
/>
|
|
{queryReportsDisabled !== undefined && (
|
|
<DiscardDataOption
|
|
selectedLoggingType={lastEditedQueryLoggingType}
|
|
discardData={lastEditedQueryDiscardData}
|
|
setDiscardData={setLastEditedQueryDiscardData}
|
|
queryReportsDisabled={queryReportsDisabled}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
{renderLiveQueryWarning()}
|
|
<div className={`button-wrap ${baseClass}__button-wrap--new-query`}>
|
|
{(hasSavePermissions || isAnyTeamMaintainerOrTeamAdmin) && (
|
|
<>
|
|
{savedQueryMode && (
|
|
<Button
|
|
variant="text-link"
|
|
onClick={promptSaveAsNewQuery()}
|
|
disabled={disableSaveFormErrors}
|
|
className="save-as-new-loading"
|
|
isLoading={isSaveAsNewLoading}
|
|
>
|
|
Save as new
|
|
</Button>
|
|
)}
|
|
<div className={`${baseClass}__button-wrap--save-query-button`}>
|
|
<div
|
|
data-tip
|
|
data-for="save-query-button"
|
|
// Tooltip shows for team maintainer/admins viewing global queries
|
|
data-tip-disable={!disableSavePermissionDenied}
|
|
>
|
|
<Button
|
|
className="save-loading"
|
|
variant="brand"
|
|
onClick={
|
|
confirmChanges
|
|
? toggleConfirmSaveChangesModal
|
|
: promptSaveQuery()
|
|
}
|
|
// Button disabled for team maintainer/admins viewing global queries
|
|
disabled={
|
|
disableSavePermissionDenied || disableSaveFormErrors
|
|
}
|
|
isLoading={isQueryUpdating}
|
|
>
|
|
Save
|
|
</Button>
|
|
</div>{" "}
|
|
<ReactTooltip
|
|
className={`save-query-button-tooltip`}
|
|
place="top"
|
|
effect="solid"
|
|
backgroundColor={COLORS["tooltip-bg"]}
|
|
id="save-query-button"
|
|
data-html
|
|
>
|
|
<>
|
|
You can only save changes
|
|
<br /> to a team level query.
|
|
</>
|
|
</ReactTooltip>
|
|
</div>
|
|
</>
|
|
)}
|
|
<div
|
|
data-tip
|
|
data-for="live-query-button"
|
|
// Tooltip shows when live queries are globally disabled
|
|
data-tip-disable={!disabledLiveQuery}
|
|
>
|
|
<Button
|
|
className={`${baseClass}__run`}
|
|
variant="blue-green"
|
|
onClick={() => {
|
|
setEditingExistingQuery(true); // Persists edited query data through live query flow
|
|
router.push(
|
|
PATHS.LIVE_QUERY(queryIdForEdit) +
|
|
TAGGED_TEMPLATES.queryByHostRoute(hostId)
|
|
);
|
|
}}
|
|
disabled={disabledLiveQuery}
|
|
>
|
|
Live query
|
|
</Button>
|
|
</div>
|
|
<ReactTooltip
|
|
className={`live-query-button-tooltip`}
|
|
place="top"
|
|
effect="solid"
|
|
backgroundColor={COLORS["tooltip-bg"]}
|
|
id="live-query-button"
|
|
data-html
|
|
>
|
|
Live queries are disabled in organization settings
|
|
</ReactTooltip>
|
|
</div>
|
|
</form>
|
|
{showSaveQueryModal && (
|
|
<SaveQueryModal
|
|
queryValue={lastEditedQueryBody}
|
|
apiTeamIdForQuery={apiTeamIdForQuery}
|
|
saveQuery={onSubmitNewQuery}
|
|
toggleSaveQueryModal={toggleSaveQueryModal}
|
|
backendValidators={backendValidators}
|
|
isLoading={isQuerySaving}
|
|
queryReportsDisabled={queryReportsDisabled}
|
|
/>
|
|
)}
|
|
{showConfirmSaveChangesModal && (
|
|
<ConfirmSaveChangesModal
|
|
onSaveChanges={promptSaveQuery()}
|
|
isUpdating={isQueryUpdating}
|
|
onClose={toggleConfirmSaveChangesModal}
|
|
showChangedSQLCopy={showChangedSQLCopy}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
if (isStoredQueryLoading) {
|
|
return <Spinner />;
|
|
}
|
|
|
|
const noEditPermissions =
|
|
(isGlobalObserver && !isObserverPlus) || // Global observer but not Observer+
|
|
(isObserverPlus && queryIdForEdit !== 0) || // Global observer+ on existing query
|
|
(isOnlyObserver && !isAnyTeamObserverPlus && !isGlobalObserver) || // Only team observer but not team Observer+
|
|
(isAnyTeamObserverPlus && // Team Observer+ on existing query
|
|
!isAnyTeamMaintainerOrTeamAdmin &&
|
|
queryIdForEdit !== 0);
|
|
|
|
// Render non-editable form only
|
|
if (noEditPermissions) {
|
|
return renderNonEditableForm;
|
|
}
|
|
|
|
// Render default editable form
|
|
return renderEditableQueryForm();
|
|
};
|
|
|
|
export default EditQueryForm;
|