Skip to content

Commit 60896e8

Browse files
authored
[SIEM] [Detection Engine] Add edit on rule creation (#51670)
* Add creation rule on Detection Engine * review + bug fixes * review II + clean up * fix persistence saved query * fix eui prop + add type security to add rule * fix more bug from review III * review IV * add edit on creation on rule * review * fix status icon color * fix filter label translation
1 parent 3ed18b4 commit 60896e8

File tree

18 files changed

+634
-127
lines changed

18 files changed

+634
-127
lines changed

x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { EuiButtonEmpty, EuiButtonIcon, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui';
8-
import { isEmpty, isEqual } from 'lodash/fp';
8+
import { isEmpty } from 'lodash/fp';
99
import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react';
1010

1111
import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports';
@@ -21,15 +21,22 @@ interface AddItemProps {
2121

2222
export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: AddItemProps) => {
2323
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
24-
const [items, setItems] = useState(['']);
25-
const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(false);
24+
// const [items, setItems] = useState(['']);
25+
const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(-1);
2626

27-
const lastInputRef = useRef<HTMLInputElement | null>(null);
27+
const inputsRef = useRef<HTMLInputElement[]>([]);
2828

2929
const removeItem = useCallback(
3030
(index: number) => {
3131
const values = field.value as string[];
3232
field.setValue([...values.slice(0, index), ...values.slice(index + 1)]);
33+
inputsRef.current = [
34+
...inputsRef.current.slice(0, index),
35+
...inputsRef.current.slice(index + 1),
36+
];
37+
if (inputsRef.current[index] != null) {
38+
inputsRef.current[index].value = 're-render';
39+
}
3340
},
3441
[field]
3542
);
@@ -38,16 +45,26 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad
3845
const values = field.value as string[];
3946
if (!isEmpty(values[values.length - 1])) {
4047
field.setValue([...values, '']);
48+
} else {
49+
field.setValue(['']);
4150
}
4251
}, [field]);
4352

4453
const updateItem = useCallback(
4554
(event: ChangeEvent<HTMLInputElement>, index: number) => {
55+
event.persist();
4656
const values = field.value as string[];
4757
const value = event.target.value;
4858
if (isEmpty(value)) {
49-
setHaveBeenKeyboardDeleted(true);
5059
field.setValue([...values.slice(0, index), ...values.slice(index + 1)]);
60+
inputsRef.current = [
61+
...inputsRef.current.slice(0, index),
62+
...inputsRef.current.slice(index + 1),
63+
];
64+
setHaveBeenKeyboardDeleted(inputsRef.current.length - 1);
65+
if (inputsRef.current[index] != null) {
66+
inputsRef.current[index].value = 're-render';
67+
}
5168
} else {
5269
field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]);
5370
}
@@ -56,31 +73,30 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad
5673
);
5774

5875
const handleLastInputRef = useCallback(
59-
(element: HTMLInputElement | null) => {
60-
lastInputRef.current = element;
76+
(index: number, element: HTMLInputElement | null) => {
77+
if (element != null) {
78+
inputsRef.current = [
79+
...inputsRef.current.slice(0, index),
80+
element,
81+
...inputsRef.current.slice(index + 1),
82+
];
83+
}
6184
},
62-
[lastInputRef]
85+
[inputsRef]
6386
);
6487

6588
useEffect(() => {
66-
if (!isEqual(field.value, items)) {
67-
setItems(
68-
isEmpty(field.value)
69-
? ['']
70-
: haveBeenKeyboardDeleted
71-
? [...(field.value as string[]), '']
72-
: (field.value as string[])
73-
);
74-
setHaveBeenKeyboardDeleted(false);
75-
}
76-
}, [field.value]);
77-
78-
useEffect(() => {
79-
if (!haveBeenKeyboardDeleted && lastInputRef != null && lastInputRef.current != null) {
80-
lastInputRef.current.focus();
89+
if (
90+
haveBeenKeyboardDeleted !== -1 &&
91+
!isEmpty(inputsRef.current) &&
92+
inputsRef.current[haveBeenKeyboardDeleted] != null
93+
) {
94+
inputsRef.current[haveBeenKeyboardDeleted].focus();
95+
setHaveBeenKeyboardDeleted(-1);
8196
}
82-
}, [haveBeenKeyboardDeleted, lastInputRef]);
97+
}, [haveBeenKeyboardDeleted, inputsRef.current]);
8398

99+
const values = field.value as string[];
84100
return (
85101
<EuiFormRow
86102
label={field.label}
@@ -92,10 +108,15 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad
92108
describedByIds={idAria ? [idAria] : undefined}
93109
>
94110
<>
95-
{items.map((item, index) => {
111+
{values.map((item, index) => {
96112
const euiFieldProps = {
97113
disabled: isDisabled,
98-
...(index === items.length - 1 ? { inputRef: handleLastInputRef } : {}),
114+
...(index === values.length - 1
115+
? { inputRef: handleLastInputRef.bind(null, index) }
116+
: {}),
117+
...(inputsRef.current[index] != null && inputsRef.current[index].value !== item
118+
? { value: item }
119+
: {}),
99120
};
100121
return (
101122
<div key={index}>
@@ -109,13 +130,12 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad
109130
aria-label={I18n.DELETE}
110131
/>
111132
}
112-
value={item}
113133
onChange={e => updateItem(e, index)}
114134
compressed
115135
fullWidth
116136
{...euiFieldProps}
117137
/>
118-
{items.length - 1 !== index && <EuiSpacer size="s" />}
138+
{values.length - 1 !== index && <EuiSpacer size="s" />}
119139
</div>
120140
);
121141
})}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React, { memo } from 'react';
8+
import { EuiTextColor } from '@elastic/eui';
9+
import { i18n } from '@kbn/i18n';
10+
11+
import { esFilters } from '../../../../../../../../../../src/plugins/data/public';
12+
import { existsOperator, isOneOfOperator } from './filter_operator';
13+
14+
interface Props {
15+
filter: esFilters.Filter;
16+
valueLabel?: string;
17+
}
18+
19+
export const FilterLabel = memo<Props>(({ filter, valueLabel }) => {
20+
const prefixText = filter.meta.negate
21+
? ` ${i18n.translate('xpack.siem.detectionEngine.createRule.filterLabel.negatedFilterPrefix', {
22+
defaultMessage: 'NOT ',
23+
})}`
24+
: '';
25+
const prefix =
26+
filter.meta.negate && !filter.meta.disabled ? (
27+
<EuiTextColor color="danger">{prefixText}</EuiTextColor>
28+
) : (
29+
prefixText
30+
);
31+
32+
if (filter.meta.alias !== null) {
33+
return (
34+
<>
35+
{prefix}
36+
{filter.meta.alias}
37+
</>
38+
);
39+
}
40+
41+
switch (filter.meta.type) {
42+
case esFilters.FILTERS.EXISTS:
43+
return (
44+
<>
45+
{prefix}
46+
{`${filter.meta.key}: ${existsOperator.message}`}
47+
</>
48+
);
49+
case esFilters.FILTERS.GEO_BOUNDING_BOX:
50+
return (
51+
<>
52+
{prefix}
53+
{`${filter.meta.key}: ${valueLabel}`}
54+
</>
55+
);
56+
case esFilters.FILTERS.GEO_POLYGON:
57+
return (
58+
<>
59+
{prefix}
60+
{`${filter.meta.key}: ${valueLabel}`}
61+
</>
62+
);
63+
case esFilters.FILTERS.PHRASES:
64+
return (
65+
<>
66+
{prefix}
67+
{filter.meta.key} {isOneOfOperator.message} {valueLabel}
68+
</>
69+
);
70+
case esFilters.FILTERS.QUERY_STRING:
71+
return (
72+
<>
73+
{prefix}
74+
{valueLabel}
75+
</>
76+
);
77+
case esFilters.FILTERS.PHRASE:
78+
case esFilters.FILTERS.RANGE:
79+
return (
80+
<>
81+
{prefix}
82+
{`${filter.meta.key}: ${valueLabel}`}
83+
</>
84+
);
85+
default:
86+
return (
87+
<>
88+
{prefix}
89+
{JSON.stringify(filter.query)}
90+
</>
91+
);
92+
}
93+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { i18n } from '@kbn/i18n';
8+
9+
import { esFilters } from '../../../../../../../../../../src/plugins/data/public';
10+
11+
export interface Operator {
12+
message: string;
13+
type: esFilters.FILTERS;
14+
negate: boolean;
15+
fieldTypes?: string[];
16+
}
17+
18+
export const isOperator = {
19+
message: i18n.translate(
20+
'xpack.siem.detectionEngine.createRule.filterLabel.isOperatorOptionLabel',
21+
{
22+
defaultMessage: 'is',
23+
}
24+
),
25+
type: esFilters.FILTERS.PHRASE,
26+
negate: false,
27+
};
28+
29+
export const isNotOperator = {
30+
message: i18n.translate(
31+
'xpack.siem.detectionEngine.createRule.filterLabel.isNotOperatorOptionLabel',
32+
{
33+
defaultMessage: 'is not',
34+
}
35+
),
36+
type: esFilters.FILTERS.PHRASE,
37+
negate: true,
38+
};
39+
40+
export const isOneOfOperator = {
41+
message: i18n.translate(
42+
'xpack.siem.detectionEngine.createRule.filterLabel.isOneOfOperatorOptionLabel',
43+
{
44+
defaultMessage: 'is one of',
45+
}
46+
),
47+
type: esFilters.FILTERS.PHRASES,
48+
negate: false,
49+
fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'],
50+
};
51+
52+
export const isNotOneOfOperator = {
53+
message: i18n.translate(
54+
'xpack.siem.detectionEngine.createRule.filterLabel.isNotOneOfOperatorOptionLabel',
55+
{
56+
defaultMessage: 'is not one of',
57+
}
58+
),
59+
type: esFilters.FILTERS.PHRASES,
60+
negate: true,
61+
fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'],
62+
};
63+
64+
export const isBetweenOperator = {
65+
message: i18n.translate(
66+
'xpack.siem.detectionEngine.createRule.filterLabel.isBetweenOperatorOptionLabel',
67+
{
68+
defaultMessage: 'is between',
69+
}
70+
),
71+
type: esFilters.FILTERS.RANGE,
72+
negate: false,
73+
fieldTypes: ['number', 'date', 'ip'],
74+
};
75+
76+
export const isNotBetweenOperator = {
77+
message: i18n.translate(
78+
'xpack.siem.detectionEngine.createRule.filterLabel.isNotBetweenOperatorOptionLabel',
79+
{
80+
defaultMessage: 'is not between',
81+
}
82+
),
83+
type: esFilters.FILTERS.RANGE,
84+
negate: true,
85+
fieldTypes: ['number', 'date', 'ip'],
86+
};
87+
88+
export const existsOperator = {
89+
message: i18n.translate(
90+
'xpack.siem.detectionEngine.createRule.filterLabel.existsOperatorOptionLabel',
91+
{
92+
defaultMessage: 'exists',
93+
}
94+
),
95+
type: esFilters.FILTERS.EXISTS,
96+
negate: false,
97+
};
98+
99+
export const doesNotExistOperator = {
100+
message: i18n.translate(
101+
'xpack.siem.detectionEngine.createRule.filterLabel.doesNotExistOperatorOptionLabel',
102+
{
103+
defaultMessage: 'does not exist',
104+
}
105+
),
106+
type: esFilters.FILTERS.EXISTS,
107+
negate: true,
108+
};
109+
110+
export const FILTER_OPERATORS: Operator[] = [
111+
isOperator,
112+
isNotOperator,
113+
isOneOfOperator,
114+
isNotOneOfOperator,
115+
isBetweenOperator,
116+
isNotBetweenOperator,
117+
existsOperator,
118+
doesNotExistOperator,
119+
];

0 commit comments

Comments
 (0)