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

[Detection engine] Some UX for rule creation #54471

Merged
merged 6 commits into from
Jan 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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