Skip to content

Commit

Permalink
[Detection engine] Some UX for rule creation (#54471)
Browse files Browse the repository at this point in the history
* 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 <elasticmachine@users.noreply.github.com>
  • Loading branch information
XavierM and elasticmachine committed Jan 10, 2020
1 parent 357be59 commit 51e51ca
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
EuiTextColor,
EuiFilterButton,
EuiFilterGroup,
EuiSpacer,
EuiPortal,
} from '@elastic/eui';
import { Option } from '@elastic/eui/src/components/selectable/types';
import { isEmpty } from 'lodash/fp';
Expand All @@ -37,12 +37,24 @@ const SearchTimelineSuperSelectGlobalStyle = createGlobalStyle`
}
`;

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

const MyEuiTextColor = styled(EuiTextColor)<{ selected: boolean }>`
padding-left: ${({ selected }) => (selected ? '20px' : '0px')};
const EuiSelectableContainer = styled.div`
.euiSelectable {
.euiFormControlLayout__childrenWrapper {
display: flex;
}
}
`;

const MyEuiFlexGroup = styled(EuiFlexGroup)`
padding 0px 4px;
`;

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

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

const renderTimelineOption = useCallback((option, searchValue) => {
return (
<>
{option.checked === 'on' && <EuiIcon type="check" color="primary" />}
<MyEuiHighlight search={searchValue} selected={option.checked === 'on'}>
{isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title}
</MyEuiHighlight>
<br />
<MyEuiTextColor color="subdued" component="span" selected={option.checked === 'on'}>
<small>
{option.description != null && option.description.trim().length > 0
? option.description
: getEmptyTagValue()}
</small>
</MyEuiTextColor>
</>
<EuiFlexGroup
gutterSize="s"
justifyContent="spaceBetween"
alignItems="center"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiIcon type={`${option.checked === 'on' ? 'check' : 'none'}`} color="primary" />
</EuiFlexItem>
<EuiFlexItem grow={true}>
<EuiFlexGroup gutterSize="none" direction="column">
<MyEuiFlexItem grow={false}>
<EuiHighlight search={searchValue}>
{isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title}
</EuiHighlight>
</MyEuiFlexItem>
<MyEuiFlexItem grow={false}>
<EuiTextColor color="subdued" component="span">
<small>
{option.description != null && option.description.trim().length > 0
? option.description
: getEmptyTagValue()}
</small>
</EuiTextColor>
</MyEuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type={`${option.favorite ? 'starFilled' : 'starEmpty'}`} />
</EuiFlexItem>
</EuiFlexGroup>
);
}, []);

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

const favoritePortal = useMemo(
() =>
searchRef != null ? (
<EuiPortal insert={{ sibling: searchRef, position: 'after' }}>
<MyEuiFlexGroup gutterSize="xs" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiFilterGroup>
<EuiFilterButton
size="l"
data-test-subj="only-favorites-toggle"
hasActiveFilters={onlyFavorites}
onClick={handleOnToggleOnlyFavorites}
>
{i18nTimeline.ONLY_FAVORITES}
</EuiFilterButton>
</EuiFilterGroup>
</EuiFlexItem>
</MyEuiFlexGroup>
</EuiPortal>
) : null,
[searchRef, onlyFavorites, handleOnToggleOnlyFavorites]
);

return (
<EuiInputPopover
id="searchTimelinePopover"
Expand All @@ -204,22 +257,7 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
onlyUserFavorite={onlyFavorites}
>
{({ timelines, loading, totalCount }) => (
<>
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiFilterGroup>
<EuiFilterButton
size="xs"
data-test-subj="only-favorites-toggle"
hasActiveFilters={onlyFavorites}
onClick={handleOnToggleOnlyFavorites}
>
{i18nTimeline.ONLY_FAVORITES}
</EuiFilterButton>
</EuiFilterGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<EuiSelectableContainer>
<EuiSelectable
height={POPOVER_HEIGHT}
isLoading={loading && timelines.length === 0}
Expand All @@ -239,6 +277,9 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER,
onSearch: onSearchTimeline,
incremental: false,
inputRef: (ref: HTMLElement) => {
setSearchRef(ref);
},
}}
singleSelection={true}
options={[
Expand All @@ -249,6 +290,7 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
(t, index) =>
({
description: t.description,
favorite: !isEmpty(t.favorite),
label: t.title,
id: t.savedObjectId,
key: `${t.title}-${index}`,
Expand All @@ -261,11 +303,12 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
{(list, search) => (
<>
{search}
{favoritePortal}
{list}
</>
)}
</EuiSelectable>
</>
</EuiSelectableContainer>
)}
</AllTimelinesQuery>
<SearchTimelineSuperSelectGlobalStyle />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const AddItem = ({
isDisabled,
validate,
}: AddItemProps) => {
const [showValidation, setShowValidation] = useState(false);
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(-1);

Expand All @@ -53,7 +54,8 @@ export const AddItem = ({
const removeItem = useCallback(
(index: number) => {
const values = field.value as string[];
field.setValue([...values.slice(0, index), ...values.slice(index + 1)]);
const newValues = [...values.slice(0, index), ...values.slice(index + 1)];
field.setValue(newValues.length === 0 ? [''] : newValues);
inputsRef.current = [
...inputsRef.current.slice(0, index),
...inputsRef.current.slice(index + 1),
Expand All @@ -70,34 +72,15 @@ export const AddItem = ({

const addItem = useCallback(() => {
const values = field.value as string[];
if (!isEmpty(values) && values[values.length - 1]) {
field.setValue([...values, '']);
} else if (isEmpty(values)) {
field.setValue(['']);
}
field.setValue([...values, '']);
}, [field]);

const updateItem = useCallback(
(event: ChangeEvent<HTMLInputElement>, index: number) => {
event.persist();
const values = field.value as string[];
const value = event.target.value;
if (isEmpty(value)) {
field.setValue([...values.slice(0, index), ...values.slice(index + 1)]);
inputsRef.current = [
...inputsRef.current.slice(0, index),
...inputsRef.current.slice(index + 1),
];
setHaveBeenKeyboardDeleted(inputsRef.current.length - 1);
inputsRef.current = inputsRef.current.map((ref, i) => {
if (i >= index && inputsRef.current[index] != null) {
ref.value = 're-render';
}
return ref;
});
} else {
field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]);
}
field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]);
},
[field]
);
Expand Down Expand Up @@ -131,8 +114,8 @@ export const AddItem = ({
<MyEuiFormRow
label={field.label}
labelAppend={field.labelAppend}
error={errorMessage}
isInvalid={isInvalid}
error={showValidation ? errorMessage : null}
isInvalid={showValidation && isInvalid}
fullWidth
data-test-subj={dataTestSubj}
describedByIds={idAria ? [idAria] : undefined}
Expand All @@ -148,19 +131,24 @@ export const AddItem = ({
inputsRef.current[index] == null
? { value: item }
: {}),
isInvalid: validate == null ? false : validate(item),
isInvalid: validate == null ? false : showValidation && validate(item),
};
return (
<div key={index}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow>
<EuiFieldText onChange={e => updateItem(e, index)} fullWidth {...euiFieldProps} />
<EuiFieldText
onBlur={() => setShowValidation(true)}
onChange={e => updateItem(e, index)}
fullWidth
{...euiFieldProps}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="danger"
iconType="trash"
isDisabled={isDisabled}
isDisabled={isDisabled || (isEmpty(item) && values.length === 1)}
onClick={() => removeItem(index)}
aria-label={RuleI18n.DELETE}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ const getDescriptionItem = (
description: timeline.title ?? DEFAULT_TIMELINE_TITLE,
},
];
} else if (field === 'riskScore') {
const description: string = get(field, value);
return [
{
title: label,
description,
},
];
}
const description: string = get(field, value);
if (!isEmpty(description)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const FILTERS_LABEL = i18n.translate('xpack.siem.detectionEngine.createRu
});

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

export const SAVED_ID_LABEL = i18n.translate('xpack.siem.detectionEngine.createRule.savedIdLabel', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
EuiText,
} from '@elastic/eui';
import { isEmpty, kebabCase, camelCase } from 'lodash/fp';
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import styled from 'styled-components';

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

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

const removeItem = useCallback(
Expand Down Expand Up @@ -137,15 +138,16 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow>
<EuiComboBox
placeholder={i18n.TECHNIQUES_PLACEHOLDER}
placeholder={item.tactic.name === 'none' ? '' : i18n.TECHNIQUES_PLACEHOLDER}
options={techniquesOptions.filter(t => t.tactics.includes(kebabCase(item.tactic.name)))}
selectedOptions={item.techniques}
onChange={updateTechniques.bind(null, index)}
isDisabled={disabled}
isDisabled={disabled || item.tactic.name === 'none'}
fullWidth={true}
isInvalid={invalid}
isInvalid={showValidation && invalid}
onBlur={() => setShowValidation(true)}
/>
{invalid && (
{showValidation && invalid && (
<EuiText color="danger" size="xs">
<p>{errorMessage}</p>
</EuiText>
Expand All @@ -155,7 +157,7 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI
<EuiButtonIcon
color="danger"
iconType="trash"
isDisabled={disabled}
isDisabled={disabled || item.tactic.name === 'none'}
onClick={() => removeItem(index)}
aria-label={Rulei18n.DELETE}
/>
Expand Down Expand Up @@ -186,7 +188,7 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI
{index === 0 ? (
<EuiFormRow
label={`${field.label} ${i18n.TECHNIQUE}`}
isInvalid={isInvalid}
isInvalid={showValidation && isInvalid}
fullWidth
describedByIds={idAria ? [`${idAria} ${i18n.TECHNIQUE}`] : undefined}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const TECHNIQUE = i18n.translate(
);

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

export const TECHNIQUES_PLACEHOLDER = i18n.translate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export const schema: FormSchema = {
label: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldFalsePositiveLabel',
{
defaultMessage: 'False positives',
defaultMessage: 'False positives examples',
}
),
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
Expand All @@ -145,7 +145,7 @@ export const schema: FormSchema = {
label: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldMitreThreatLabel',
{
defaultMessage: 'MITRE ATT&CK',
defaultMessage: 'MITRE ATT&CK\\u2122',
}
),
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
Expand Down
Loading

0 comments on commit 51e51ca

Please sign in to comment.