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

[SIEM] Fixes UX issues around prebuilt ML Rules #62396

Merged
merged 7 commits into from
Apr 6, 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 @@ -10,6 +10,16 @@ import React from 'react';
import '../../mock/match_media';
import { HeaderGlobal } from './index';

jest.mock('react-router-dom', () => ({
useLocation: () => ({
pathname: '/app/siem#/hosts/allHosts',
hash: '',
search: '',
state: '',
}),
withRouter: () => jest.fn(),
}));

jest.mock('ui/new_platform');

// Test will fail because we will to need to mock some core services to make the test work
Expand All @@ -19,6 +29,10 @@ jest.mock('../search_bar', () => ({
}));

describe('HeaderGlobal', () => {
beforeEach(() => {
jest.resetAllMocks();
});

test('it renders', () => {
const wrapper = shallow(<HeaderGlobal />);

Expand Down
116 changes: 61 additions & 55 deletions x-pack/legacy/plugins/siem/public/components/header_global/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { pickBy } from 'lodash/fp';
import React from 'react';
import styled, { css } from 'styled-components';

import { useLocation } from 'react-router-dom';
import { gutterTimeline } from '../../lib/helpers';
import { navTabs } from '../../pages/home/home_navigations';
import { SiemPageName } from '../../pages/home/types';
Expand Down Expand Up @@ -36,63 +37,68 @@ FlexItem.displayName = 'FlexItem';
interface HeaderGlobalProps {
hideDetectionEngine?: boolean;
}
export const HeaderGlobal = React.memo<HeaderGlobalProps>(({ hideDetectionEngine = false }) => (
<Wrapper className="siemHeaderGlobal">
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" wrap>
<WithSource sourceId="default">
{({ indicesExist }) => (
<>
<FlexItem>
<EuiFlexGroup alignItems="center" responsive={false}>
<FlexItem grow={false}>
<EuiLink href={getOverviewUrl()}>
<EuiIcon aria-label={i18n.SIEM} type="securityAnalyticsApp" size="l" />
</EuiLink>
</FlexItem>
export const HeaderGlobal = React.memo<HeaderGlobalProps>(({ hideDetectionEngine = false }) => {
const currentLocation = useLocation();

<FlexItem component="nav">
{indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<SiemNavigation
display="condensed"
navTabs={
hideDetectionEngine
? pickBy((_, key) => key !== SiemPageName.detections, navTabs)
: navTabs
}
/>
) : (
<SiemNavigation
display="condensed"
navTabs={pickBy((_, key) => key === SiemPageName.overview, navTabs)}
/>
)}
</FlexItem>
</EuiFlexGroup>
</FlexItem>

<FlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap>
{indicesExistOrDataTemporarilyUnavailable(indicesExist) && (
return (
<Wrapper className="siemHeaderGlobal">
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" wrap>
<WithSource sourceId="default">
{({ indicesExist }) => (
<>
<FlexItem>
<EuiFlexGroup alignItems="center" responsive={false}>
<FlexItem grow={false}>
<MlPopover />
<EuiLink href={getOverviewUrl()}>
<EuiIcon aria-label={i18n.SIEM} type="securityAnalyticsApp" size="l" />
</EuiLink>
</FlexItem>

<FlexItem component="nav">
{indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<SiemNavigation
display="condensed"
navTabs={
hideDetectionEngine
? pickBy((_, key) => key !== SiemPageName.detections, navTabs)
: navTabs
}
/>
) : (
<SiemNavigation
display="condensed"
navTabs={pickBy((_, key) => key === SiemPageName.overview, navTabs)}
/>
)}
</FlexItem>
)}
</EuiFlexGroup>
</FlexItem>

<FlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="add-data"
href="kibana#home/tutorial_directory/siem"
iconType="plusInCircle"
>
{i18n.BUTTON_ADD_DATA}
</EuiButtonEmpty>
</FlexItem>
</EuiFlexGroup>
</FlexItem>
</>
)}
</WithSource>
</EuiFlexGroup>
</Wrapper>
));
<FlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap>
{indicesExistOrDataTemporarilyUnavailable(indicesExist) &&
currentLocation.pathname.includes(`/${SiemPageName.detections}/`) && (
<FlexItem grow={false}>
<MlPopover />
</FlexItem>
)}

<FlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="add-data"
href="kibana#home/tutorial_directory/siem"
iconType="plusInCircle"
>
{i18n.BUTTON_ADD_DATA}
</EuiButtonEmpty>
</FlexItem>
</EuiFlexGroup>
</FlexItem>
</>
)}
</WithSource>
</EuiFlexGroup>
</Wrapper>
);
});
HeaderGlobal.displayName = 'HeaderGlobal';

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export const MlPopover = React.memo(() => {
iconSide="right"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
>
{i18n.ANOMALY_DETECTION}
{i18n.ML_JOB_SETTINGS}
</EuiButtonEmpty>
}
isOpen={isPopoverOpen}
Expand All @@ -142,14 +142,14 @@ export const MlPopover = React.memo(() => {
dispatch({ type: 'refresh' });
}}
>
{i18n.ANOMALY_DETECTION}
{i18n.ML_JOB_SETTINGS}
</EuiButtonEmpty>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(!isPopoverOpen)}
>
<PopoverContentsDiv data-test-subj="ml-popover-contents">
<EuiPopoverTitle>{i18n.ANOMALY_DETECTION_TITLE}</EuiPopoverTitle>
<EuiPopoverTitle>{i18n.ML_JOB_SETTINGS}</EuiPopoverTitle>
<PopoverDescription />

<EuiSpacer />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const PopoverDescriptionComponent = () => (
<EuiText size="s">
<FormattedMessage
id="xpack.siem.components.mlPopup.anomalyDetectionDescription"
defaultMessage="Run any of the Machine Learning jobs below to view anomalous events throughout the SIEM application. We’ve provided a few common detection jobs to get you started. If you wish to add your own custom jobs, simply create and tag them with “SIEM” from the {machineLearning} application for inclusion here."
defaultMessage="Run any of the Machine Learning jobs below to prepare for creating signal detection rules that produce signals for detected anomalies, and to view anomalous events throughout the SIEM application. We’ve provided a collection of common detection jobs to get you started. If you wish to add your own custom ML jobs, create and add them to the “SIEM” group from the {machineLearning} application."
values={{
machineLearning: (
<EuiLink href={`${useBasePath()}/app/ml`} target="_blank">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,10 @@

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

export const ANOMALY_DETECTION = i18n.translate(
'xpack.siem.components.mlPopup.anomalyDetectionButtonLabel',
export const ML_JOB_SETTINGS = i18n.translate(
'xpack.siem.components.mlPopup.mlJobSettingsButtonLabel',
{
defaultMessage: 'Anomaly detection',
}
);

export const ANOMALY_DETECTION_TITLE = i18n.translate(
'xpack.siem.components.mlPopup.anomalyDetectionTitle',
{
defaultMessage: 'Anomaly detection settings',
defaultMessage: 'ML job settings',
}
);

Expand Down
24 changes: 24 additions & 0 deletions x-pack/legacy/plugins/siem/public/components/toasters/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,30 @@ export const displayErrorToast = (
});
};

/**
* Displays a warning toast for the provided title and message
*
* @param title warning message to display in toaster and modal
* @param dispatchToaster provided by useStateToaster()
* @param id unique ID if necessary
*/
export const displayWarningToast = (
title: string,
dispatchToaster: React.Dispatch<ActionToaster>,
id: string = uuid.v4()
): void => {
const toast: AppToast = {
id,
title,
color: 'warning',
iconType: 'help',
};
dispatchToaster({
type: 'addToaster',
toast,
});
};

/**
* Displays a success toast for the provided title and message
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import {
enableRulesAction,
exportRulesAction,
} from './actions';
import { ActionToaster } from '../../../../components/toasters';
import { ActionToaster, displayWarningToast } from '../../../../components/toasters';
import { Rule } from '../../../../containers/detection_engine/rules';
import * as detectionI18n from '../../translations';

interface GetBatchItems {
closePopover: () => void;
dispatch: Dispatch<Action>;
dispatchToaster: Dispatch<ActionToaster>;
hasMlPermissions: boolean;
loadingRuleIds: string[];
reFetchRules: (refreshPrePackagedRule?: boolean) => void;
rules: Rule[];
Expand All @@ -31,6 +33,7 @@ export const getBatchItems = ({
closePopover,
dispatch,
dispatchToaster,
hasMlPermissions,
loadingRuleIds,
reFetchRules,
rules,
Expand All @@ -57,7 +60,22 @@ export const getBatchItems = ({
const deactivatedIds = selectedRuleIds.filter(
id => !rules.find(r => r.id === id)?.enabled ?? false
);
await enableRulesAction(deactivatedIds, true, dispatch, dispatchToaster);

const deactivatedIdsNoML = deactivatedIds.filter(
id => rules.find(r => r.id === id)?.type !== 'machine_learning' ?? false
);

const mlRuleCount = deactivatedIds.length - deactivatedIdsNoML.length;
if (!hasMlPermissions && mlRuleCount > 0) {
displayWarningToast(detectionI18n.ML_RULES_UNAVAILABLE(mlRuleCount), dispatchToaster);
}

await enableRulesAction(
hasMlPermissions ? deactivatedIds : deactivatedIdsNoML,
true,
dispatch,
dispatchToaster
);
}}
>
{i18n.BATCH_ACTION_ACTIVATE_SELECTED}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
EuiTableActionsColumnType,
EuiText,
EuiHealth,
EuiToolTip,
} from '@elastic/eui';
import { FormattedRelative } from '@kbn/i18n/react';
import * as H from 'history';
Expand All @@ -36,6 +37,8 @@ import {
} from './actions';
import { Action } from './reducer';
import { LocalizedDateTooltip } from '../../../../components/localized_date_tooltip';
import * as detectionI18n from '../../translations';
import { isMlRule } from '../../../../../common/detection_engine/ml_helpers';

export const getActions = (
dispatch: React.Dispatch<Action>,
Expand Down Expand Up @@ -88,6 +91,7 @@ interface GetColumns {
dispatch: React.Dispatch<Action>;
dispatchToaster: Dispatch<ActionToaster>;
history: H.History;
hasMlPermissions: boolean;
hasNoPermissions: boolean;
loadingRuleIds: string[];
reFetchRules: (refreshPrePackagedRule?: boolean) => void;
Expand All @@ -98,6 +102,7 @@ export const getColumns = ({
dispatch,
dispatchToaster,
history,
hasMlPermissions,
hasNoPermissions,
loadingRuleIds,
reFetchRules,
Expand Down Expand Up @@ -182,14 +187,25 @@ export const getColumns = ({
field: 'enabled',
name: i18n.COLUMN_ACTIVATE,
render: (value: Rule['enabled'], item: Rule) => (
<RuleSwitch
data-test-subj="enabled"
dispatch={dispatch}
id={item.id}
enabled={item.enabled}
isDisabled={hasNoPermissions}
isLoading={loadingRuleIds.includes(item.id)}
/>
<EuiToolTip
position="top"
content={
isMlRule(item.type) && !hasMlPermissions
? detectionI18n.ML_RULES_DISABLED_MESSAGE
: undefined
}
>
<RuleSwitch
data-test-subj="enabled"
dispatch={dispatch}
id={item.id}
enabled={item.enabled}
isDisabled={
hasNoPermissions || (isMlRule(item.type) && !hasMlPermissions && !item.enabled)
}
isLoading={loadingRuleIds.includes(item.id)}
/>
</EuiToolTip>
),
sortable: true,
width: '95px',
Expand Down
Loading