Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Add ES|QL Query editable component #199887

Merged
merged 45 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
610622c
add ES|QL Query edit component
maximpn Nov 12, 2024
5d1a70b
add fieldsToValidateOnChange
maximpn Nov 12, 2024
c077171
fix query persistence
maximpn Nov 12, 2024
5630e75
move default queries to query bar folder
maximpn Nov 13, 2024
0fb0ce7
add ES|QL Query validator unit tests
maximpn Nov 13, 2024
8565b78
revert aria-label
maximpn Nov 13, 2024
ab8989e
remove unused translation keys
maximpn Nov 13, 2024
80879b5
fix usePersistentQuery implementation
maximpn Nov 13, 2024
a67d481
update changed test id
maximpn Nov 14, 2024
f875632
add query persistence tests
maximpn Nov 14, 2024
7c11a3b
remove unnecessary re-exports
maximpn Nov 18, 2024
c978760
refactor debounceAsync function
maximpn Nov 19, 2024
139c87f
fix a misprint
maximpn Nov 19, 2024
49448b1
remove unnecessary missing dataView condition
maximpn Nov 19, 2024
ec07d58
move test helpers closer to consumers
maximpn Nov 19, 2024
e8d43ab
uncomment mistakenly commented test
maximpn Nov 19, 2024
0ccae15
refactor ES|QL validation
maximpn Nov 19, 2024
1818031
remove unnecessary memo dependency
maximpn Nov 19, 2024
9165060
update a comment
maximpn Nov 19, 2024
4c9c9e5
combine useFormData usages
maximpn Nov 19, 2024
45f6dbc
remove unused translation keys
maximpn Nov 19, 2024
9d85e1d
rename query_bar folder to query_field
maximpn Nov 19, 2024
cfa7b20
define form field constants for rule type and query
maximpn Nov 19, 2024
6c9d04e
remove commented code
maximpn Nov 19, 2024
485d96a
remove a copy of debounceAsync
maximpn Nov 19, 2024
a47e74e
remove unused translation keys
maximpn Nov 19, 2024
3f97f4c
fix type check errors
maximpn Nov 19, 2024
7a600b5
add explanation comments for usePersistentQuery
maximpn Nov 20, 2024
8c0c8bc
move debounceAsync to @kbn/securitysolution-utils package
maximpn Nov 20, 2024
57079f3
simplify query bar field name usage
maximpn Nov 20, 2024
4581d98
prevent fetch columns request retries
maximpn Nov 20, 2024
1797f0a
reduce unnecessary requests
maximpn Nov 20, 2024
315241c
rename query_field to query_bar_field
maximpn Nov 20, 2024
1e5a1e8
use type hinting
maximpn Nov 20, 2024
23d2834
roll back moving field names to constants
maximpn Nov 20, 2024
203510d
fix getDescriptionItem
maximpn Nov 21, 2024
d26fafe
skip _id column check for prebuilt rules customization workflow
maximpn Nov 21, 2024
90a6421
fix post-rebase issues
maximpn Nov 22, 2024
5afdccc
move query esql required validation inside esql query edit component
maximpn Nov 22, 2024
2490ae8
fix EQL rule creation from a timeline
maximpn Nov 23, 2024
73a0655
move repeating tanstack query options to a function
maximpn Nov 23, 2024
f6ae8d4
avoid calling setPersistentEqlQuery for non EQL rules
maximpn Nov 26, 2024
cbd97a8
revert threat hunting investigation changes
maximpn Nov 26, 2024
7a474af
fix ES|QL query required validator
maximpn Nov 27, 2024
625ffa9
fix a test name
maximpn Nov 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,17 @@
* License v3.0 only", or the "Server Side Public License, v 1".
vitaliidm marked this conversation as resolved.
Show resolved Hide resolved
*/

import { ESQLAst, getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { type ESQLAstQueryExpression, parse } from '@kbn/esql-ast';

export const isAggregatingQuery = (ast: ESQLAst): boolean => {
return ast.some((astItem) => astItem.type === 'command' && astItem.name === 'stats');
};
export const isAggregatingQuery = (astExpression: ESQLAstQueryExpression): boolean =>
astExpression.commands.some((command) => command.name === 'stats');

/**
* compute if esqlQuery is aggregating/grouping, i.e. using STATS...BY command
* @param esqlQuery
* @returns boolean
*/
export const computeIsESQLQueryAggregating = (esqlQuery: string): boolean => {
const { ast } = getAstAndSyntaxErrors(esqlQuery);
return isAggregatingQuery(ast);
const { root } = parse(esqlQuery);
return isAggregatingQuery(root);
};

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import type { FieldHook } from '../../../../shared_imports';
import { FilterBar } from '../../../../common/components/filter_bar';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import type { EqlOptions } from '../../../../../common/search_strategy';
import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar_field';
import { useKibana } from '../../../../common/lib/kibana';
import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar';
import type { EqlQueryBarFooterProps } from './footer';
import { EqlQueryBarFooter } from './footer';
import { getValidationResults } from './validators';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import { debounceAsync } from '@kbn/securitysolution-utils';
import type { FormData, FieldConfig, ValidationFuncArg } from '../../../../shared_imports';
import { UseMultiFields } from '../../../../shared_imports';
import type { EqlFieldsComboBoxOptions, EqlOptions } from '../../../../../common/search_strategy';
import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar_field';
import { queryRequiredValidatorFactory } from '../../../rule_creation_ui/validators/query_required_validator_factory';
import { eqlQueryValidatorFactory } from './eql_query_validator_factory';
import { EqlQueryBar } from './eql_query_bar';
import * as i18n from './translations';
import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar';

interface EqlQueryEditProps {
path: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import { isEmpty } from 'lodash';
import type { FormData, ValidationError, ValidationFunc } from '../../../../shared_imports';
import { KibanaServices } from '../../../../common/lib/kibana';
import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar_field';
import type { EqlOptions } from '../../../../../common/search_strategy';
import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar';
import type { EqlResponseError } from '../../../../common/hooks/eql/api';
import { EQL_ERROR_CODES, validateEql } from '../../../../common/hooks/eql/api';
import { EQL_VALIDATION_REQUEST_ERROR } from './translations';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,19 @@
* 2.0.
*/

import React from 'react';
import React, { memo } from 'react';
import { EuiPopover, EuiText, EuiButtonIcon, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import * as i18n from './translations';

import { useBoolState } from '../../../../common/hooks/use_bool_state';
import { useBoolean } from '@kbn/react-hooks';
import { useKibana } from '../../../../common/lib/kibana';
import * as i18n from './translations';

/**
* Icon and popover that gives hint to users how to get started with ES|QL rules
*/
const EsqlInfoIconComponent = () => {
export const EsqlInfoIcon = memo(function EsqlInfoIcon(): JSX.Element {
const { docLinks } = useKibana().services;

const [isPopoverOpen, , closePopover, togglePopover] = useBoolState();
const [isPopoverOpen, { off: closePopover, on: togglePopover }] = useBoolean(false);

const button = (
<EuiButtonIcon iconType="iInCircle" onClick={togglePopover} aria-label={i18n.ARIA_LABEL} />
Expand All @@ -29,13 +27,13 @@ const EsqlInfoIconComponent = () => {
<EuiPopover button={button} isOpen={isPopoverOpen} closePopover={closePopover}>
<EuiText size="s">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlInfoTooltipContent"
id="xpack.securitySolution.ruleManagement.esqlQuery.esqlInfoTooltipContent"
defaultMessage="Check out our {createEsqlRuleTypeLink} to get started using ES|QL rules."
values={{
createEsqlRuleTypeLink: (
<EuiLink href={docLinks.links.securitySolution.createEsqlRuleType} target="_blank">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlInfoTooltipLink"
id="xpack.securitySolution.ruleManagement.esqlQuery.esqlInfoTooltipLink"
defaultMessage="documentation"
/>
</EuiLink>
Expand All @@ -45,8 +43,4 @@ const EsqlInfoIconComponent = () => {
</EuiText>
</EuiPopover>
);
};

export const EsqlInfoIcon = React.memo(EsqlInfoIconComponent);

EsqlInfoIcon.displayName = 'EsqlInfoIcon';
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { memo, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import type { DataViewBase } from '@kbn/es-query';
import { debounceAsync } from '@kbn/securitysolution-utils';
import type { FieldConfig } from '../../../../shared_imports';
import { UseField } from '../../../../shared_imports';
import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar_field';
import { QueryBarField } from '../../../rule_creation_ui/components/query_bar_field';
import { esqlQueryRequiredValidatorFactory } from './validators/esql_query_required_validator_factory';
import { esqlQueryValidatorFactory } from './validators/esql_query_validator_factory';
import { EsqlInfoIcon } from './esql_info_icon';
import * as i18n from './translations';

interface EsqlQueryEditProps {
path: string;
fieldsToValidateOnChange?: string | string[];
dataView: DataViewBase;
required?: boolean;
loading?: boolean;
disabled?: boolean;
skipIdColumnCheck?: boolean;
onValidityChange?: (arg: boolean) => void;
}

export const EsqlQueryEdit = memo(function EsqlQueryEdit({
path,
fieldsToValidateOnChange,
dataView,
required = false,
loading = false,
disabled = false,
skipIdColumnCheck,
onValidityChange,
}: EsqlQueryEditProps): JSX.Element {
const queryClient = useQueryClient();
const componentProps = useMemo(
() => ({
isDisabled: disabled,
isLoading: loading,
indexPattern: dataView,
idAria: 'ruleEsqlQueryBar',
dataTestSubj: 'ruleEsqlQueryBar',
onValidityChange,
}),
[dataView, loading, disabled, onValidityChange]
);
const fieldConfig: FieldConfig<FieldValueQueryBar> = useMemo(
() => ({
label: i18n.ESQL_QUERY,
labelAppend: <EsqlInfoIcon />,
fieldsToValidateOnChange: fieldsToValidateOnChange
? [path, fieldsToValidateOnChange].flat()
: undefined,
validations: [
...(required
? [
{
validator: esqlQueryRequiredValidatorFactory(),
},
]
: []),
{
validator: debounceAsync(
esqlQueryValidatorFactory({ queryClient, skipIdColumnCheck }),
300
),
},
],
}),
[required, path, fieldsToValidateOnChange, queryClient, skipIdColumnCheck]
);

return (
<UseField
path={path}
component={QueryBarField}
componentProps={componentProps}
config={fieldConfig}
/>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './esql_query_edit';
export * from './validators/error_codes';
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@

import { i18n } from '@kbn/i18n';

export const ESQL_QUERY = i18n.translate(
'xpack.securitySolution.ruleManagement.fields.esqlQuery.label',
{
defaultMessage: 'ES|QL query',
}
);

export const ARIA_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlInfoAriaLabel',
'xpack.securitySolution.ruleManagement.fields.esqlQuery.ariaLabel',
{
defaultMessage: `Open help popover`,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export enum ESQL_ERROR_CODES {
INVALID_ESQL = 'ERR_INVALID_ESQL',
INVALID_SYNTAX = 'ERR_INVALID_SYNTAX',
ERR_MISSING_ID_FIELD_FROM_RESULT = 'ERR_MISSING_ID_FIELD_FROM_RESULT',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { fieldValidators, type FormData, type ValidationFunc } from '../../../../../shared_imports';
import type { FieldValueQueryBar } from '../../../../rule_creation_ui/components/query_bar_field';
import * as i18n from './translations';

export function esqlQueryRequiredValidatorFactory(): ValidationFunc<
FormData,
string,
FieldValueQueryBar
> {
return async (data) => {
xcrzx marked this conversation as resolved.
Show resolved Hide resolved
const { value } = data;
const esqlQuery = value.query.query as string;

if (esqlQuery.trim() === '') {
return;
}

return fieldValidators.emptyField(i18n.ESQL_QUERY_VALIDATION_REQUIRED)({
...data,
value: esqlQuery,
});
xcrzx marked this conversation as resolved.
Show resolved Hide resolved
};
}
Loading