Skip to content

Commit 49f1c3b

Browse files
[Detection engine] Some UX for rule creation (#54471) (#54519)
* wip * update timelien select to design * Rename label to design Timeline Select match design with favorite Now, you are able to add mutiple items for url and false positive Add tm for Mitre Att&ck (tnaks Frank) And match mitre selection to design * cleanup with michael Co-authored-by: Elastic Machine <[email protected]> Co-authored-by: Elastic Machine <[email protected]>
1 parent 02eead7 commit 49f1c3b

File tree

10 files changed

+121
-80
lines changed

10 files changed

+121
-80
lines changed

x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx

Lines changed: 79 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
EuiTextColor,
1616
EuiFilterButton,
1717
EuiFilterGroup,
18-
EuiSpacer,
18+
EuiPortal,
1919
} from '@elastic/eui';
2020
import { Option } from '@elastic/eui/src/components/selectable/types';
2121
import { isEmpty } from 'lodash/fp';
@@ -37,12 +37,24 @@ const SearchTimelineSuperSelectGlobalStyle = createGlobalStyle`
3737
}
3838
`;
3939

40-
const MyEuiHighlight = styled(EuiHighlight)<{ selected: boolean }>`
41-
padding-left: ${({ selected }) => (selected ? '3px' : '0px')};
40+
const MyEuiFlexItem = styled(EuiFlexItem)`
41+
display: inline-block;
42+
max-width: 296px;
43+
overflow: hidden;
44+
text-overflow: ellipsis;
45+
white-space: nowrap;
4246
`;
4347

44-
const MyEuiTextColor = styled(EuiTextColor)<{ selected: boolean }>`
45-
padding-left: ${({ selected }) => (selected ? '20px' : '0px')};
48+
const EuiSelectableContainer = styled.div`
49+
.euiSelectable {
50+
.euiFormControlLayout__childrenWrapper {
51+
display: flex;
52+
}
53+
}
54+
`;
55+
56+
const MyEuiFlexGroup = styled(EuiFlexGroup)`
57+
padding 0px 4px;
4658
`;
4759

4860
interface SearchTimelineSuperSelectProps {
@@ -83,6 +95,7 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
8395
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
8496
const [searchTimelineValue, setSearchTimelineValue] = useState('');
8597
const [onlyFavorites, setOnlyFavorites] = useState(false);
98+
const [searchRef, setSearchRef] = useState<HTMLElement | null>(null);
8699

87100
const onSearchTimeline = useCallback(val => {
88101
setSearchTimelineValue(val);
@@ -102,20 +115,37 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
102115

103116
const renderTimelineOption = useCallback((option, searchValue) => {
104117
return (
105-
<>
106-
{option.checked === 'on' && <EuiIcon type="check" color="primary" />}
107-
<MyEuiHighlight search={searchValue} selected={option.checked === 'on'}>
108-
{isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title}
109-
</MyEuiHighlight>
110-
<br />
111-
<MyEuiTextColor color="subdued" component="span" selected={option.checked === 'on'}>
112-
<small>
113-
{option.description != null && option.description.trim().length > 0
114-
? option.description
115-
: getEmptyTagValue()}
116-
</small>
117-
</MyEuiTextColor>
118-
</>
118+
<EuiFlexGroup
119+
gutterSize="s"
120+
justifyContent="spaceBetween"
121+
alignItems="center"
122+
responsive={false}
123+
>
124+
<EuiFlexItem grow={false}>
125+
<EuiIcon type={`${option.checked === 'on' ? 'check' : 'none'}`} color="primary" />
126+
</EuiFlexItem>
127+
<EuiFlexItem grow={true}>
128+
<EuiFlexGroup gutterSize="none" direction="column">
129+
<MyEuiFlexItem grow={false}>
130+
<EuiHighlight search={searchValue}>
131+
{isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title}
132+
</EuiHighlight>
133+
</MyEuiFlexItem>
134+
<MyEuiFlexItem grow={false}>
135+
<EuiTextColor color="subdued" component="span">
136+
<small>
137+
{option.description != null && option.description.trim().length > 0
138+
? option.description
139+
: getEmptyTagValue()}
140+
</small>
141+
</EuiTextColor>
142+
</MyEuiFlexItem>
143+
</EuiFlexGroup>
144+
</EuiFlexItem>
145+
<EuiFlexItem grow={false}>
146+
<EuiIcon type={`${option.favorite ? 'starFilled' : 'starEmpty'}`} />
147+
</EuiFlexItem>
148+
</EuiFlexGroup>
119149
);
120150
}, []);
121151

@@ -187,6 +217,29 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
187217
[handleOpenPopover, isDisabled, timelineId, timelineTitle]
188218
);
189219

220+
const favoritePortal = useMemo(
221+
() =>
222+
searchRef != null ? (
223+
<EuiPortal insert={{ sibling: searchRef, position: 'after' }}>
224+
<MyEuiFlexGroup gutterSize="xs" justifyContent="flexEnd">
225+
<EuiFlexItem grow={false}>
226+
<EuiFilterGroup>
227+
<EuiFilterButton
228+
size="l"
229+
data-test-subj="only-favorites-toggle"
230+
hasActiveFilters={onlyFavorites}
231+
onClick={handleOnToggleOnlyFavorites}
232+
>
233+
{i18nTimeline.ONLY_FAVORITES}
234+
</EuiFilterButton>
235+
</EuiFilterGroup>
236+
</EuiFlexItem>
237+
</MyEuiFlexGroup>
238+
</EuiPortal>
239+
) : null,
240+
[searchRef, onlyFavorites, handleOnToggleOnlyFavorites]
241+
);
242+
190243
return (
191244
<EuiInputPopover
192245
id="searchTimelinePopover"
@@ -204,22 +257,7 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
204257
onlyUserFavorite={onlyFavorites}
205258
>
206259
{({ timelines, loading, totalCount }) => (
207-
<>
208-
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
209-
<EuiFlexItem grow={false}>
210-
<EuiFilterGroup>
211-
<EuiFilterButton
212-
size="xs"
213-
data-test-subj="only-favorites-toggle"
214-
hasActiveFilters={onlyFavorites}
215-
onClick={handleOnToggleOnlyFavorites}
216-
>
217-
{i18nTimeline.ONLY_FAVORITES}
218-
</EuiFilterButton>
219-
</EuiFilterGroup>
220-
</EuiFlexItem>
221-
</EuiFlexGroup>
222-
<EuiSpacer size="xs" />
260+
<EuiSelectableContainer>
223261
<EuiSelectable
224262
height={POPOVER_HEIGHT}
225263
isLoading={loading && timelines.length === 0}
@@ -239,6 +277,9 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
239277
placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER,
240278
onSearch: onSearchTimeline,
241279
incremental: false,
280+
inputRef: (ref: HTMLElement) => {
281+
setSearchRef(ref);
282+
},
242283
}}
243284
singleSelection={true}
244285
options={[
@@ -249,6 +290,7 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
249290
(t, index) =>
250291
({
251292
description: t.description,
293+
favorite: !isEmpty(t.favorite),
252294
label: t.title,
253295
id: t.savedObjectId,
254296
key: `${t.title}-${index}`,
@@ -261,11 +303,12 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
261303
{(list, search) => (
262304
<>
263305
{search}
306+
{favoritePortal}
264307
{list}
265308
</>
266309
)}
267310
</EuiSelectable>
268-
</>
311+
</EuiSelectableContainer>
269312
)}
270313
</AllTimelinesQuery>
271314
<SearchTimelineSuperSelectGlobalStyle />

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

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const AddItem = ({
4545
isDisabled,
4646
validate,
4747
}: AddItemProps) => {
48+
const [showValidation, setShowValidation] = useState(false);
4849
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
4950
const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(-1);
5051

@@ -53,7 +54,8 @@ export const AddItem = ({
5354
const removeItem = useCallback(
5455
(index: number) => {
5556
const values = field.value as string[];
56-
field.setValue([...values.slice(0, index), ...values.slice(index + 1)]);
57+
const newValues = [...values.slice(0, index), ...values.slice(index + 1)];
58+
field.setValue(newValues.length === 0 ? [''] : newValues);
5759
inputsRef.current = [
5860
...inputsRef.current.slice(0, index),
5961
...inputsRef.current.slice(index + 1),
@@ -70,34 +72,15 @@ export const AddItem = ({
7072

7173
const addItem = useCallback(() => {
7274
const values = field.value as string[];
73-
if (!isEmpty(values) && values[values.length - 1]) {
74-
field.setValue([...values, '']);
75-
} else if (isEmpty(values)) {
76-
field.setValue(['']);
77-
}
75+
field.setValue([...values, '']);
7876
}, [field]);
7977

8078
const updateItem = useCallback(
8179
(event: ChangeEvent<HTMLInputElement>, index: number) => {
8280
event.persist();
8381
const values = field.value as string[];
8482
const value = event.target.value;
85-
if (isEmpty(value)) {
86-
field.setValue([...values.slice(0, index), ...values.slice(index + 1)]);
87-
inputsRef.current = [
88-
...inputsRef.current.slice(0, index),
89-
...inputsRef.current.slice(index + 1),
90-
];
91-
setHaveBeenKeyboardDeleted(inputsRef.current.length - 1);
92-
inputsRef.current = inputsRef.current.map((ref, i) => {
93-
if (i >= index && inputsRef.current[index] != null) {
94-
ref.value = 're-render';
95-
}
96-
return ref;
97-
});
98-
} else {
99-
field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]);
100-
}
83+
field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]);
10184
},
10285
[field]
10386
);
@@ -131,8 +114,8 @@ export const AddItem = ({
131114
<MyEuiFormRow
132115
label={field.label}
133116
labelAppend={field.labelAppend}
134-
error={errorMessage}
135-
isInvalid={isInvalid}
117+
error={showValidation ? errorMessage : null}
118+
isInvalid={showValidation && isInvalid}
136119
fullWidth
137120
data-test-subj={dataTestSubj}
138121
describedByIds={idAria ? [idAria] : undefined}
@@ -148,19 +131,24 @@ export const AddItem = ({
148131
inputsRef.current[index] == null
149132
? { value: item }
150133
: {}),
151-
isInvalid: validate == null ? false : validate(item),
134+
isInvalid: validate == null ? false : showValidation && validate(item),
152135
};
153136
return (
154137
<div key={index}>
155138
<EuiFlexGroup gutterSize="s" alignItems="center">
156139
<EuiFlexItem grow>
157-
<EuiFieldText onChange={e => updateItem(e, index)} fullWidth {...euiFieldProps} />
140+
<EuiFieldText
141+
onBlur={() => setShowValidation(true)}
142+
onChange={e => updateItem(e, index)}
143+
fullWidth
144+
{...euiFieldProps}
145+
/>
158146
</EuiFlexItem>
159147
<EuiFlexItem grow={false}>
160148
<EuiButtonIcon
161149
color="danger"
162150
iconType="trash"
163-
isDisabled={isDisabled}
151+
isDisabled={isDisabled || (isEmpty(item) && values.length === 1)}
164152
onClick={() => removeItem(index)}
165153
aria-label={RuleI18n.DELETE}
166154
/>

x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ const getDescriptionItem = (
143143
description: timeline.title ?? DEFAULT_TIMELINE_TITLE,
144144
},
145145
];
146+
} else if (field === 'riskScore') {
147+
const description: string = get(field, value);
148+
return [
149+
{
150+
title: label,
151+
description,
152+
},
153+
];
146154
}
147155
const description: string = get(field, value);
148156
if (!isEmpty(description)) {

x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const FILTERS_LABEL = i18n.translate('xpack.siem.detectionEngine.createRu
1111
});
1212

1313
export const QUERY_LABEL = i18n.translate('xpack.siem.detectionEngine.createRule.QueryLabel', {
14-
defaultMessage: 'Query',
14+
defaultMessage: 'Custom query',
1515
});
1616

1717
export const SAVED_ID_LABEL = i18n.translate('xpack.siem.detectionEngine.createRule.savedIdLabel', {

x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
EuiText,
1717
} from '@elastic/eui';
1818
import { isEmpty, kebabCase, camelCase } from 'lodash/fp';
19-
import React, { useCallback } from 'react';
19+
import React, { useCallback, useState } from 'react';
2020
import styled from 'styled-components';
2121

2222
import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques';
@@ -41,6 +41,7 @@ interface AddItemProps {
4141
}
4242

4343
export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddItemProps) => {
44+
const [showValidation, setShowValidation] = useState(false);
4445
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
4546

4647
const removeItem = useCallback(
@@ -137,15 +138,16 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI
137138
<EuiFlexGroup gutterSize="s" alignItems="center">
138139
<EuiFlexItem grow>
139140
<EuiComboBox
140-
placeholder={i18n.TECHNIQUES_PLACEHOLDER}
141+
placeholder={item.tactic.name === 'none' ? '' : i18n.TECHNIQUES_PLACEHOLDER}
141142
options={techniquesOptions.filter(t => t.tactics.includes(kebabCase(item.tactic.name)))}
142143
selectedOptions={item.techniques}
143144
onChange={updateTechniques.bind(null, index)}
144-
isDisabled={disabled}
145+
isDisabled={disabled || item.tactic.name === 'none'}
145146
fullWidth={true}
146-
isInvalid={invalid}
147+
isInvalid={showValidation && invalid}
148+
onBlur={() => setShowValidation(true)}
147149
/>
148-
{invalid && (
150+
{showValidation && invalid && (
149151
<EuiText color="danger" size="xs">
150152
<p>{errorMessage}</p>
151153
</EuiText>
@@ -155,7 +157,7 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI
155157
<EuiButtonIcon
156158
color="danger"
157159
iconType="trash"
158-
isDisabled={disabled}
160+
isDisabled={disabled || item.tactic.name === 'none'}
159161
onClick={() => removeItem(index)}
160162
aria-label={Rulei18n.DELETE}
161163
/>
@@ -186,7 +188,7 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI
186188
{index === 0 ? (
187189
<EuiFormRow
188190
label={`${field.label} ${i18n.TECHNIQUE}`}
189-
isInvalid={isInvalid}
191+
isInvalid={showValidation && isInvalid}
190192
fullWidth
191193
describedByIds={idAria ? [`${idAria} ${i18n.TECHNIQUE}`] : undefined}
192194
>

x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const TECHNIQUE = i18n.translate(
1818
);
1919

2020
export const ADD_MITRE_ATTACK = i18n.translate('xpack.siem.detectionEngine.mitreAttack.addTitle', {
21-
defaultMessage: 'Add MITRE ATT&CK threat',
21+
defaultMessage: 'Add MITRE ATT&CK\\u2122 threat',
2222
});
2323

2424
export const TECHNIQUES_PLACEHOLDER = i18n.translate(

x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export const schema: FormSchema = {
136136
label: i18n.translate(
137137
'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldFalsePositiveLabel',
138138
{
139-
defaultMessage: 'False positives',
139+
defaultMessage: 'False positives examples',
140140
}
141141
),
142142
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
@@ -145,7 +145,7 @@ export const schema: FormSchema = {
145145
label: i18n.translate(
146146
'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldMitreThreatLabel',
147147
{
148-
defaultMessage: 'MITRE ATT&CK',
148+
defaultMessage: 'MITRE ATT&CK\\u2122',
149149
}
150150
),
151151
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,

0 commit comments

Comments
 (0)