[7.6] [SIEM] Overview page feedback (#56261) (#56277)

* [SIEM] Overview page feedback (#56261)

## [SIEM] Overview page feedback

Implements feedback and fixes to the Overview page

### Overview (default theme)

![01-overview-default-theme](https://user-images.githubusercontent.com/4459398/73315509-899c5500-41ed-11ea-9949-82853dd4ba59.png)

### Overview (dark theme)

![02-overview-dark-theme](https://user-images.githubusercontent.com/4459398/73315527-902acc80-41ed-11ea-9701-6a2c5fa40cce.png)

## Highlights

* The new order of widgets is Signals, Alerts, Events, Host Events, Network events, per https://github.com/elastic/siem-team/issues/494

* Changed the default `External alerts count` `Stack by` to `event.module` https://github.com/elastic/siem-team/issues/491

*  Added `event.module` to the `Events count` histogram https://github.com/elastic/siem-team/issues/491

* Widget titles will no longer include the currently selected `Stack by option`. The widgets will use the same static title text that appears on the other pages (i.e.. `Signals count`, `External alerts count`, and `Events count`) https://github.com/elastic/siem-team/issues/491

* The `Signals count` includes a `Stack by` that defaults to `signal.rule.threat.tatic.name`

* Standardized on a 300px widget height for all histograms in the app (thanks @MichaelMarcialis for paring on this!)

* The `Open as duplicate timeline` action is `Recent timelines` is now only shown when hovering over a recent timeline

## Loading States

* The `Recent timelines` and `Security news` widgets now use the horizontal bar loading indicator

* The `Host events` and `Network events` widgets now use the horizontal bar loading indicator

* The `Host events` and `Network events` Showing _n_ events subtitles are now hidden on initial load

* The counts in the `Host events` and `Network events` Showing _n_ events subtitles are now hidden on initial load

* We no longer hide some histogram subtitles after initial load, to prevent shifting of content when a user makes a `Stack by` selection

## News Feed Error State

![news-feed-error-state](https://user-images.githubusercontent.com/4459398/73316060-1e538280-41ef-11ea-83f5-b8d6e9fa3741.png)

* Fixed an issue where the `Security news` header was hidden when an invalid URL is configured

* Added a space between the word `via` and the `SIEM advanced settings` link

* Removed the capital “N” from "News" in the error message

## Misc Visual Changes

* Fixed text truncation of the `Severity` column in the `Detections` page's `Signals` table

* Added the “showing” subtitle to the `Signals count` histogram on the Detections page

* Increased the `Stack by` histogram selector and the `View signals | alerts | events' buttons from 8 to 24px

* Tweaked the border rendering in the Overview `Host Events` and `Network events` widget headers

* Added 8px of spacing between the Overview `Host Events` and `Network events` widget accordion headers and their contents

* Fixed an issue where the `Host events` and `Networ events` widgets didn't render in ie11 https://github.com/elastic/siem-team/issues/499

## Non-Visual Fixes

* Removed an incorrect usage of `usememo`

* Removed the placeholder client-side username query from `x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx`

* Updated the query of the Overview `Host events` widget to filter by "host.name exists"

* Updated the query of the Overview `Network events` widget to filter by "source.ip exists or destination.ip : exists"

* Removed the following unused translations that were failing the i18n Compatibility Checks:

```
xpack.siem.overview.alertsCountByTitle
xpack.siem.overview.eventsCountByTitle
xpack.siem.overview.signalsByCategoryTitle
```

The following files were updated:

* `x-pack/plugins/translations/translations/zh-CN.json`
* `x-pack/plugins/translations/translations/ja-JP.json`
This commit is contained in:
Andrew Goldstein 2020-01-29 02:43:38 -07:00 committed by GitHub
parent 63e435c866
commit ff7aa450d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1863 additions and 1399 deletions

View file

@ -6,6 +6,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import { ScaleType } from '@elastic/charts';
import styled from 'styled-components';
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui';
import { noop } from 'lodash/fp';
@ -25,8 +26,21 @@ import {
import { ChartSeriesData } from '../charts/common';
import { InspectButtonContainer } from '../inspect';
const DEFAULT_PANEL_HEIGHT = 300;
const HeaderChildrenFlexItem = styled(EuiFlexItem)`
margin-left: 24px;
`;
const HistogramPanel = styled(Panel)<{ height?: number }>`
display: flex;
flex-direction: column;
${({ height }) => (height != null ? `height: ${height}px;` : '')}
`;
export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
MatrixHistogramQueryProps> = ({
chartHeight,
dataKey,
defaultStackByOption,
endDate,
@ -43,6 +57,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
isInspected,
legendPosition = 'right',
mapping,
panelHeight = DEFAULT_PANEL_HEIGHT,
query,
scaleType = ScaleType.Time,
setQuery,
@ -56,6 +71,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
yTickFormatter,
}) => {
const barchartConfigs = getBarchartConfigs({
chartHeight,
from: startDate,
legendPosition,
to: endDate,
@ -143,7 +159,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
return (
<>
<InspectButtonContainer show={!isInitialLoading}>
<Panel data-test-subj={`${id}Panel`}>
<HistogramPanel data-test-subj={`${id}Panel`} height={panelHeight}>
{loading && !isInitialLoading && (
<EuiProgress
data-test-subj="initialLoadingPanelMatrixOverTime"
@ -154,11 +170,6 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
)}
{isInitialLoading ? (
<>
<HeaderSection id={id} title={titleWithStackByField} />
<MatrixLoader />
</>
) : (
<>
<HeaderSection
id={id}
@ -176,13 +187,36 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps &
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>{headerChildren}</EuiFlexItem>
<HeaderChildrenFlexItem grow={false}>{headerChildren}</HeaderChildrenFlexItem>
</EuiFlexGroup>
</HeaderSection>
<MatrixLoader />
</>
) : (
<>
<HeaderSection
id={id}
title={titleWithStackByField}
subtitle={!isInitialLoading && (totalCount >= 0 ? subtitleWithCounts : null)}
>
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
{stackByOptions?.length > 1 && (
<EuiSelect
onChange={setSelectedChartOptionCallback}
options={stackByOptions}
prepend={i18n.STACK_BY}
value={selectedStackByOption?.value}
/>
)}
</EuiFlexItem>
<HeaderChildrenFlexItem grow={false}>{headerChildren}</HeaderChildrenFlexItem>
</EuiFlexGroup>
</HeaderSection>
<BarChart barChart={barChartData} configs={barchartConfigs} />
</>
)}
</Panel>
</HistogramPanel>
</InspectButtonContainer>
<EuiSpacer size="l" />
</>

View file

@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import styled from 'styled-components';
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
height: 350px; /* to avoid jump when histogram loads */
flex 1;
`;
const MatrixLoaderComponent = () => (

View file

@ -31,6 +31,7 @@ export type GetSubTitle = (count: number) => string;
export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string;
export interface MatrixHistogramBasicProps {
chartHeight?: number;
defaultIndex: string[];
defaultStackByOption: MatrixHistogramOption;
endDate: number;
@ -39,6 +40,7 @@ export interface MatrixHistogramBasicProps {
id: string;
legendPosition?: Position;
mapping?: MatrixHistogramMappingTypes;
panelHeight?: number;
setQuery: SetQuery;
sourceId: string;
startDate: number;

View file

@ -11,6 +11,7 @@ import { MatrixHistogramDataTypes, MatrixHistogramMappingTypes } from './types';
import { histogramDateTimeFormatter } from '../utils';
interface GetBarchartConfigsProps {
chartHeight?: number;
from: number;
legendPosition?: Position;
to: number;
@ -20,7 +21,10 @@ interface GetBarchartConfigsProps {
showLegend?: boolean;
}
export const DEFAULT_CHART_HEIGHT = 174;
export const getBarchartConfigs = ({
chartHeight,
from,
legendPosition,
to,
@ -65,7 +69,7 @@ export const getBarchartConfigs = ({
},
},
},
customHeight: 324,
customHeight: chartHeight ?? DEFAULT_CHART_HEIGHT,
});
export const formatToChartDataItem = ([key, value]: [

View file

@ -4,39 +4,42 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiLoadingSpinner, EuiSpacer } from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
import React from 'react';
import { NoNews } from './no_news';
import { LoadingPlaceholders } from '../page/overview/loading_placeholders';
import { NEWS_FEED_TITLE } from '../../pages/overview/translations';
import { Post } from './post';
import { SidebarHeader } from '../sidebar_header';
import { NoNews } from './no_news';
import { Post } from './post';
import { NewsItem } from './types';
interface Props {
news: NewsItem[] | null | undefined;
}
export const NewsFeed = React.memo<Props>(({ news }) => {
if (news == null) {
return <EuiLoadingSpinner size="m" />;
}
const SHOW_PLACEHOLDERS = 5;
const LINES_PER_LOADING_PLACEHOLDER = 4;
if (news.length === 0) {
return <NoNews />;
}
return (
<>
<SidebarHeader title={NEWS_FEED_TITLE} />
{news.map((n: NewsItem) => (
const NewsFeedComponent: React.FC<Props> = ({ news }) => (
<>
<SidebarHeader title={NEWS_FEED_TITLE} />
{news == null ? (
<LoadingPlaceholders lines={LINES_PER_LOADING_PLACEHOLDER} placeholders={SHOW_PLACEHOLDERS} />
) : news.length === 0 ? (
<NoNews />
) : (
news.map((n: NewsItem) => (
<React.Fragment key={n.hash}>
<Post newsItem={n} />
<EuiSpacer size="l" />
</React.Fragment>
))}
</>
);
});
))
)}
</>
);
NewsFeed.displayName = 'NewsFeed';
NewsFeedComponent.displayName = 'NewsFeedComponent';
export const NewsFeed = React.memo(NewsFeedComponent);

View file

@ -12,7 +12,7 @@ import * as i18n from '../translations';
export const NoNews = React.memo(() => (
<>
<EuiText color="subdued" size="s">
{i18n.NO_NEWS_MESSAGE}
{i18n.NO_NEWS_MESSAGE}{' '}
<EuiLink href={'/app/kibana#/management/kibana/settings'}>
{i18n.ADVANCED_SETTINGS_LINK_TITLE}
</EuiLink>

View file

@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n';
export const NO_NEWS_MESSAGE = i18n.translate('xpack.siem.newsFeed.noNewsMessage', {
defaultMessage:
'Your current News feed URL returned no recent news. You may update the URL or disable security news via',
'Your current news feed URL returned no recent news. You may update the URL or disable security news via',
});
export const ADVANCED_SETTINGS_LINK_TITLE = i18n.translate(

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiLoadingContent, EuiSpacer } from '@elastic/eui';
import React from 'react';
const LoadingPlaceholdersComponent: React.FC<{
lines: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
placeholders: number;
}> = ({ lines, placeholders }) => (
<>
{[...Array(placeholders).keys()].map((_, i) => (
<React.Fragment key={i}>
<EuiLoadingContent lines={lines} />
{i !== placeholders - 1 && <EuiSpacer size="l" />}
</React.Fragment>
))}
</>
);
LoadingPlaceholdersComponent.displayName = 'LoadingPlaceholdersComponent';
export const LoadingPlaceholders = React.memo(LoadingPlaceholdersComponent);

View file

@ -0,0 +1,144 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { cloneDeep } from 'lodash/fp';
import { mount } from 'enzyme';
import React from 'react';
import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock';
import { OverviewHost } from '.';
import { createStore, State } from '../../../../store';
import { overviewHostQuery } from '../../../../containers/overview/overview_host/index.gql_query';
import { GetOverviewHostQuery } from '../../../../graphql/types';
import { MockedProvider } from 'react-apollo/test-utils';
import { wait } from '../../../../lib/helpers';
jest.mock('../../../../lib/kibana');
const startDate = 1579553397080;
const endDate = 1579639797080;
interface MockedProvidedQuery {
request: {
query: GetOverviewHostQuery.Query;
fetchPolicy: string;
variables: GetOverviewHostQuery.Variables;
};
result: {
data: {
source: unknown;
};
};
}
const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
{
request: {
query: overviewHostQuery,
fetchPolicy: 'cache-and-network',
variables: {
sourceId: 'default',
timerange: { interval: '12h', from: startDate, to: endDate },
filterQuery: undefined,
defaultIndex: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'winlogbeat-*',
],
inspect: false,
},
},
result: {
data: {
source: {
id: 'default',
OverviewHost: {
auditbeatAuditd: 1,
auditbeatFIM: 1,
auditbeatLogin: 1,
auditbeatPackage: 1,
auditbeatProcess: 1,
auditbeatUser: 1,
endgameDns: 1,
endgameFile: 1,
endgameImageLoad: 1,
endgameNetwork: 1,
endgameProcess: 1,
endgameRegistry: 1,
endgameSecurity: 1,
filebeatSystemModule: 1,
winlogbeatSecurity: 1,
winlogbeatMWSysmonOperational: 1,
},
},
},
},
},
];
describe('OverviewHost', () => {
const state: State = mockGlobalState;
let store = createStore(state, apolloClientObservable);
beforeEach(() => {
const myState = cloneDeep(state);
store = createStore(myState, apolloClientObservable);
});
test('it renders the expected widget title', () => {
const wrapper = mount(
<TestProviders store={store}>
<OverviewHost endDate={endDate} setQuery={jest.fn()} startDate={startDate} />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-section-title"]')
.first()
.text()
).toEqual('Host events');
});
test('it renders an empty subtitle while loading', () => {
const wrapper = mount(
<TestProviders>
<OverviewHost endDate={endDate} setQuery={jest.fn()} startDate={startDate} />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-panel-subtitle"]')
.first()
.text()
).toEqual('');
});
test('it renders the expected event count in the subtitle after loading events', async () => {
const wrapper = mount(
<TestProviders>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<OverviewHost endDate={endDate} setQuery={jest.fn()} startDate={startDate} />
</MockedProvider>
</TestProviders>
);
await wait();
wrapper.update();
expect(
wrapper
.find('[data-test-subj="header-panel-subtitle"]')
.first()
.text()
).toEqual('Showing: 16 events');
});
});

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { isEmpty } from 'lodash/fp';
import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { FormattedMessage } from '@kbn/i18n/react';
@ -41,7 +42,7 @@ export interface OwnProps {
}
const OverviewHostStatsManage = manageQuery(OverviewHostStats);
type OverviewHostProps = OwnProps;
export type OverviewHostProps = OwnProps;
const OverviewHostComponent: React.FC<OverviewHostProps> = ({
endDate,
@ -56,6 +57,7 @@ const OverviewHostComponent: React.FC<OverviewHostProps> = ({
<InspectButtonContainer>
<EuiPanel>
<OverviewHostQuery
data-test-subj="overview-host-query"
endDate={endDate}
filterQuery={filterQuery}
sourceId="default"
@ -71,17 +73,20 @@ const OverviewHostComponent: React.FC<OverviewHostProps> = ({
return (
<>
<HeaderSection
border
id={OverviewHostQueryId}
subtitle={
<FormattedMessage
defaultMessage="Showing: {formattedHostEventsCount} {hostEventsCount, plural, one {event} other {events}}"
id="xpack.siem.overview.overviewHost.hostsSubtitle"
values={{
formattedHostEventsCount,
hostEventsCount,
}}
/>
!isEmpty(overviewHost) ? (
<FormattedMessage
defaultMessage="Showing: {formattedHostEventsCount} {hostEventsCount, plural, one {event} other {events}}"
id="xpack.siem.overview.overviewHost.hostsSubtitle"
values={{
formattedHostEventsCount,
hostEventsCount,
}}
/>
) : (
<>{''}</>
)
}
title={
<FormattedMessage

View file

@ -6,7 +6,7 @@
import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useMemo } from 'react';
import React from 'react';
import styled from 'styled-components';
import { OverviewHostData } from '../../../../graphql/types';
@ -203,7 +203,11 @@ const Title = styled.div`
margin-left: 24px;
`;
export const OverviewHostStats = React.memo<OverviewHostProps>(({ data, loading }) => {
const AccordionContent = styled.div`
margin-top: 8px;
`;
const OverviewHostStatsComponent: React.FC<OverviewHostProps> = ({ data, loading }) => {
const allHostStats = getOverviewHostStats(data);
const allHostStatsCount = allHostStats.reduce((total, stat) => total + stat.count, 0);
@ -213,56 +217,55 @@ export const OverviewHostStats = React.memo<OverviewHostProps>(({ data, loading
const statsForGroup = allHostStats.filter(s => statGroup.statIds.includes(s.id));
const statsForGroupCount = statsForGroup.reduce((total, stat) => total + stat.count, 0);
const accordionButton = useMemo(
() => (
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText>{statGroup.name}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<StatValue
count={statsForGroupCount}
isGroupStat={true}
isLoading={loading}
max={allHostStatsCount}
/>
</EuiFlexItem>
</EuiFlexGroup>
),
[statGroup, statsForGroupCount, loading, allHostStatsCount]
);
return (
<React.Fragment key={statGroup.groupId}>
<EuiHorizontalRule margin="xs" />
<EuiAccordion
id={`host-stat-accordion-group${statGroup.groupId}`}
buttonContent={accordionButton}
buttonContentClassName="accordion-button"
>
{statsForGroup.map(stat => (
<EuiFlexGroup key={stat.id} justifyContent="spaceBetween">
buttonContent={
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">
<Title>{stat.title}</Title>
</EuiText>
<EuiText>{statGroup.name}</EuiText>
</EuiFlexItem>
<EuiFlexItem data-test-subj={`host-stat-${stat.id}`} grow={false}>
<EuiFlexItem grow={false}>
<StatValue
count={stat.count}
isGroupStat={false}
count={statsForGroupCount}
isGroupStat={true}
isLoading={loading}
max={statsForGroupCount}
max={allHostStatsCount}
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
}
buttonContentClassName="accordion-button"
>
<AccordionContent>
{statsForGroup.map(stat => (
<EuiFlexGroup key={stat.id} justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">
<Title>{stat.title}</Title>
</EuiText>
</EuiFlexItem>
<EuiFlexItem data-test-subj={`host-stat-${stat.id}`} grow={false}>
<StatValue
count={stat.count}
isGroupStat={false}
isLoading={loading}
max={statsForGroupCount}
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
</AccordionContent>
</EuiAccordion>
{i !== hostStatGroups.length - 1 && <EuiHorizontalRule margin="xs" />}
</React.Fragment>
);
})}
</HostStatsContainer>
);
});
};
OverviewHostStats.displayName = 'OverviewHostStats';
OverviewHostStatsComponent.displayName = 'OverviewHostStatsComponent';
export const OverviewHostStats = React.memo(OverviewHostStatsComponent);

View file

@ -0,0 +1,137 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { cloneDeep } from 'lodash/fp';
import { mount } from 'enzyme';
import React from 'react';
import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock';
import { OverviewNetwork } from '.';
import { createStore, State } from '../../../../store';
import { overviewNetworkQuery } from '../../../../containers/overview/overview_network/index.gql_query';
import { GetOverviewHostQuery } from '../../../../graphql/types';
import { MockedProvider } from 'react-apollo/test-utils';
import { wait } from '../../../../lib/helpers';
jest.mock('../../../../lib/kibana');
const startDate = 1579553397080;
const endDate = 1579639797080;
interface MockedProvidedQuery {
request: {
query: GetOverviewHostQuery.Query;
fetchPolicy: string;
variables: GetOverviewHostQuery.Variables;
};
result: {
data: {
source: unknown;
};
};
}
const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
{
request: {
query: overviewNetworkQuery,
fetchPolicy: 'cache-and-network',
variables: {
sourceId: 'default',
timerange: { interval: '12h', from: startDate, to: endDate },
filterQuery: undefined,
defaultIndex: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'winlogbeat-*',
],
inspect: false,
},
},
result: {
data: {
source: {
id: 'default',
OverviewNetwork: {
auditbeatSocket: 1,
filebeatCisco: 1,
filebeatNetflow: 1,
filebeatPanw: 1,
filebeatSuricata: 1,
filebeatZeek: 1,
packetbeatDNS: 1,
packetbeatFlow: 1,
packetbeatTLS: 1,
},
},
},
},
},
];
describe('OverviewNetwork', () => {
const state: State = mockGlobalState;
let store = createStore(state, apolloClientObservable);
beforeEach(() => {
const myState = cloneDeep(state);
store = createStore(myState, apolloClientObservable);
});
test('it renders the expected widget title', () => {
const wrapper = mount(
<TestProviders store={store}>
<OverviewNetwork endDate={endDate} setQuery={jest.fn()} startDate={startDate} />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-section-title"]')
.first()
.text()
).toEqual('Network events');
});
test('it renders an empty subtitle while loading', () => {
const wrapper = mount(
<TestProviders>
<OverviewNetwork endDate={endDate} setQuery={jest.fn()} startDate={startDate} />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-panel-subtitle"]')
.first()
.text()
).toEqual('');
});
test('it renders the expected event count in the subtitle after loading events', async () => {
const wrapper = mount(
<TestProviders>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<OverviewNetwork endDate={endDate} setQuery={jest.fn()} startDate={startDate} />
</MockedProvider>
</TestProviders>
);
await wait();
wrapper.update();
expect(
wrapper
.find('[data-test-subj="header-panel-subtitle"]')
.first()
.text()
).toEqual('Showing: 9 events');
});
});

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { isEmpty } from 'lodash/fp';
import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { FormattedMessage } from '@kbn/i18n/react';
@ -23,7 +24,7 @@ import { getOverviewNetworkStats, OverviewNetworkStats } from '../overview_netwo
import { getNetworkUrl } from '../../../link_to';
import { InspectButtonContainer } from '../../../inspect';
export interface OwnProps {
export interface OverviewNetworkProps {
startDate: number;
endDate: number;
filterQuery?: ESQuery | string;
@ -42,35 +43,40 @@ export interface OwnProps {
const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats);
export const OverviewNetwork = React.memo<OwnProps>(
({ endDate, filterQuery, startDate, setQuery }) => {
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
const OverviewNetworkComponent: React.FC<OverviewNetworkProps> = ({
endDate,
filterQuery,
startDate,
setQuery,
}) => {
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
return (
<EuiFlexItem>
<InspectButtonContainer>
<EuiPanel>
<OverviewNetworkQuery
endDate={endDate}
filterQuery={filterQuery}
sourceId="default"
startDate={startDate}
>
{({ overviewNetwork, loading, id, inspect, refetch }) => {
const networkEventsCount = getOverviewNetworkStats(overviewNetwork).reduce(
(total, stat) => total + stat.count,
0
);
const formattedNetworkEventsCount = numeral(networkEventsCount).format(
defaultNumberFormat
);
return (
<EuiFlexItem>
<InspectButtonContainer>
<EuiPanel>
<OverviewNetworkQuery
data-test-subj="overview-network-query"
endDate={endDate}
filterQuery={filterQuery}
sourceId="default"
startDate={startDate}
>
{({ overviewNetwork, loading, id, inspect, refetch }) => {
const networkEventsCount = getOverviewNetworkStats(overviewNetwork).reduce(
(total, stat) => total + stat.count,
0
);
const formattedNetworkEventsCount = numeral(networkEventsCount).format(
defaultNumberFormat
);
return (
<>
<HeaderSection
border
id={OverviewNetworkQueryId}
subtitle={
return (
<>
<HeaderSection
id={OverviewNetworkQueryId}
subtitle={
!isEmpty(overviewNetwork) ? (
<FormattedMessage
defaultMessage="Showing: {formattedNetworkEventsCount} {networkEventsCount, plural, one {event} other {events}}"
id="xpack.siem.overview.overviewNetwork.networkSubtitle"
@ -79,39 +85,43 @@ export const OverviewNetwork = React.memo<OwnProps>(
networkEventsCount,
}}
/>
}
title={
<FormattedMessage
id="xpack.siem.overview.networkTitle"
defaultMessage="Network events"
/>
}
>
<EuiButton href={getNetworkUrl()}>
<FormattedMessage
id="xpack.siem.overview.networkAction"
defaultMessage="View network"
/>
</EuiButton>
</HeaderSection>
) : (
<>{''}</>
)
}
title={
<FormattedMessage
id="xpack.siem.overview.networkTitle"
defaultMessage="Network events"
/>
}
>
<EuiButton href={getNetworkUrl()}>
<FormattedMessage
id="xpack.siem.overview.networkAction"
defaultMessage="View network"
/>
</EuiButton>
</HeaderSection>
<OverviewNetworkStatsManage
loading={loading}
data={overviewNetwork}
id={id}
inspect={inspect}
setQuery={setQuery}
refetch={refetch}
/>
</>
);
}}
</OverviewNetworkQuery>
</EuiPanel>
</InspectButtonContainer>
</EuiFlexItem>
);
}
);
<OverviewNetworkStatsManage
loading={loading}
data={overviewNetwork}
id={id}
inspect={inspect}
setQuery={setQuery}
refetch={refetch}
/>
</>
);
}}
</OverviewNetworkQuery>
</EuiPanel>
</InspectButtonContainer>
</EuiFlexItem>
);
};
OverviewNetwork.displayName = 'OverviewNetwork';
OverviewNetworkComponent.displayName = 'OverviewNetworkComponent';
export const OverviewNetwork = React.memo(OverviewNetworkComponent);

View file

@ -4,6 +4,9 @@ exports[`Overview Network Stat Data rendering it renders the default OverviewNet
<styled.div
data-test-subj="overview-network-stats"
>
<EuiHorizontalRule
margin="xs"
/>
<EuiAccordion
buttonContent={
<ForwardRef
@ -24,7 +27,7 @@ exports[`Overview Network Stat Data rendering it renders the default OverviewNet
<EuiFlexItem
grow={false}
>
<Memo(StatValue)
<Memo(StatValueComponent)
count={12}
isGroupStat={true}
isLoading={false}
@ -38,38 +41,40 @@ exports[`Overview Network Stat Data rendering it renders the default OverviewNet
initialIsOpen={false}
paddingSize="none"
>
<EuiFlexGroup
justifyContent="spaceBetween"
key="auditbeatSocket"
>
<EuiFlexItem
grow={false}
<styled.div>
<EuiFlexGroup
justifyContent="spaceBetween"
key="auditbeatSocket"
>
<EuiText
color="subdued"
size="s"
<EuiFlexItem
grow={false}
>
<styled.div>
<FormattedMessage
defaultMessage="Socket"
id="xpack.siem.overview.auditBeatSocketTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-auditbeatSocket"
grow={false}
>
<StatValue
count={12}
isGroupStat={false}
isLoading={false}
max={12}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Socket"
id="xpack.siem.overview.auditBeatSocketTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-auditbeatSocket"
grow={false}
>
<Memo(StatValueComponent)
count={12}
isGroupStat={false}
isLoading={false}
max={12}
/>
</EuiFlexItem>
</EuiFlexGroup>
</styled.div>
</EuiAccordion>
<EuiHorizontalRule
margin="xs"
@ -94,7 +99,7 @@ exports[`Overview Network Stat Data rendering it renders the default OverviewNet
<EuiFlexItem
grow={false}
>
<Memo(StatValue)
<Memo(StatValueComponent)
count={70860}
isGroupStat={true}
isLoading={false}
@ -108,166 +113,168 @@ exports[`Overview Network Stat Data rendering it renders the default OverviewNet
initialIsOpen={false}
paddingSize="none"
>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatCisco"
>
<EuiFlexItem
grow={false}
<styled.div>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatCisco"
>
<EuiText
color="subdued"
size="s"
<EuiFlexItem
grow={false}
>
<styled.div>
<FormattedMessage
defaultMessage="Cisco"
id="xpack.siem.overview.filebeatCiscoTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatCisco"
grow={false}
>
<StatValue
count={999}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatNetflow"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Cisco"
id="xpack.siem.overview.filebeatCiscoTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatCisco"
grow={false}
>
<styled.div>
<FormattedMessage
defaultMessage="Netflow"
id="xpack.siem.overview.filebeatNetflowTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatNetflow"
grow={false}
<Memo(StatValueComponent)
count={999}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatNetflow"
>
<StatValue
count={7777}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatPanw"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
<EuiFlexItem
grow={false}
>
<styled.div>
<FormattedMessage
defaultMessage="Palo Alto Networks"
id="xpack.siem.overview.filebeatPanwTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatPanw"
grow={false}
>
<StatValue
count={66}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatSuricata"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Netflow"
id="xpack.siem.overview.filebeatNetflowTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatNetflow"
grow={false}
>
<styled.div>
<FormattedMessage
defaultMessage="Suricata"
id="xpack.siem.overview.fileBeatSuricataTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatSuricata"
grow={false}
<Memo(StatValueComponent)
count={7777}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatPanw"
>
<StatValue
count={60015}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatZeek"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
<EuiFlexItem
grow={false}
>
<styled.div>
<FormattedMessage
defaultMessage="Zeek"
id="xpack.siem.overview.fileBeatZeekTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatZeek"
grow={false}
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Palo Alto Networks"
id="xpack.siem.overview.filebeatPanwTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatPanw"
grow={false}
>
<Memo(StatValueComponent)
count={66}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatSuricata"
>
<StatValue
count={2003}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Suricata"
id="xpack.siem.overview.fileBeatSuricataTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatSuricata"
grow={false}
>
<Memo(StatValueComponent)
count={60015}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="filebeatZeek"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Zeek"
id="xpack.siem.overview.fileBeatZeekTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-filebeatZeek"
grow={false}
>
<Memo(StatValueComponent)
count={2003}
isGroupStat={false}
isLoading={false}
max={70860}
/>
</EuiFlexItem>
</EuiFlexGroup>
</styled.div>
</EuiAccordion>
<EuiHorizontalRule
margin="xs"
@ -292,7 +299,7 @@ exports[`Overview Network Stat Data rendering it renders the default OverviewNet
<EuiFlexItem
grow={false}
>
<Memo(StatValue)
<Memo(StatValueComponent)
count={13677323}
isGroupStat={true}
isLoading={false}
@ -306,102 +313,104 @@ exports[`Overview Network Stat Data rendering it renders the default OverviewNet
initialIsOpen={false}
paddingSize="none"
>
<EuiFlexGroup
justifyContent="spaceBetween"
key="packetbeatDNS"
>
<EuiFlexItem
grow={false}
<styled.div>
<EuiFlexGroup
justifyContent="spaceBetween"
key="packetbeatDNS"
>
<EuiText
color="subdued"
size="s"
<EuiFlexItem
grow={false}
>
<styled.div>
<FormattedMessage
defaultMessage="DNS"
id="xpack.siem.overview.packetBeatDnsTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-packetbeatDNS"
grow={false}
>
<StatValue
count={10277307}
isGroupStat={false}
isLoading={false}
max={13677323}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="packetbeatFlow"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="DNS"
id="xpack.siem.overview.packetBeatDnsTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-packetbeatDNS"
grow={false}
>
<styled.div>
<FormattedMessage
defaultMessage="Flow"
id="xpack.siem.overview.packetBeatFlowTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-packetbeatFlow"
grow={false}
<Memo(StatValueComponent)
count={10277307}
isGroupStat={false}
isLoading={false}
max={13677323}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="packetbeatFlow"
>
<StatValue
count={16}
isGroupStat={false}
isLoading={false}
max={13677323}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="packetbeatTLS"
>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
<EuiFlexItem
grow={false}
>
<styled.div>
<FormattedMessage
defaultMessage="TLS"
id="xpack.siem.overview.packetbeatTLSTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-packetbeatTLS"
grow={false}
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="Flow"
id="xpack.siem.overview.packetBeatFlowTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-packetbeatFlow"
grow={false}
>
<Memo(StatValueComponent)
count={16}
isGroupStat={false}
isLoading={false}
max={13677323}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
key="packetbeatTLS"
>
<StatValue
count={3400000}
isGroupStat={false}
isLoading={false}
max={13677323}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem
grow={false}
>
<EuiText
color="subdued"
size="s"
>
<styled.div>
<FormattedMessage
defaultMessage="TLS"
id="xpack.siem.overview.packetbeatTLSTitle"
values={Object {}}
/>
</styled.div>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
data-test-subj="network-stat-packetbeatTLS"
grow={false}
>
<Memo(StatValueComponent)
count={3400000}
isGroupStat={false}
isLoading={false}
max={13677323}
/>
</EuiFlexItem>
</EuiFlexGroup>
</styled.div>
</EuiAccordion>
</styled.div>
`;

View file

@ -6,7 +6,7 @@
import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useMemo } from 'react';
import React from 'react';
import styled from 'styled-components';
import { OverviewNetworkData } from '../../../../graphql/types';
@ -126,6 +126,10 @@ const Title = styled.div`
margin-left: 24px;
`;
const AccordionContent = styled.div`
margin-top: 8px;
`;
export const OverviewNetworkStats = React.memo<OverviewNetworkProps>(({ data, loading }) => {
const allNetworkStats = getOverviewNetworkStats(data);
const allNetworkStatsCount = allNetworkStats.reduce((total, stat) => total + stat.count, 0);
@ -136,54 +140,51 @@ export const OverviewNetworkStats = React.memo<OverviewNetworkProps>(({ data, lo
const statsForGroup = allNetworkStats.filter(s => statGroup.statIds.includes(s.id));
const statsForGroupCount = statsForGroup.reduce((total, stat) => total + stat.count, 0);
const accordionButton = useMemo(
() => (
<EuiFlexGroup
data-test-subj={`network-stat-group-${statGroup.groupId}`}
justifyContent="spaceBetween"
>
<EuiFlexItem grow={false}>
<EuiText>{statGroup.name}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<StatValue
count={statsForGroupCount}
isGroupStat={true}
isLoading={loading}
max={allNetworkStatsCount}
/>
</EuiFlexItem>
</EuiFlexGroup>
),
[statGroup, statsForGroupCount, loading, allNetworkStatsCount]
);
return (
<React.Fragment key={statGroup.groupId}>
<EuiHorizontalRule margin="xs" />
<EuiAccordion
id={`network-stat-accordion-group${statGroup.groupId}`}
buttonContent={accordionButton}
buttonContentClassName="accordion-button"
>
{statsForGroup.map(stat => (
<EuiFlexGroup key={stat.id} justifyContent="spaceBetween">
buttonContent={
<EuiFlexGroup
data-test-subj={`network-stat-group-${statGroup.groupId}`}
justifyContent="spaceBetween"
>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">
<Title>{stat.title}</Title>
</EuiText>
<EuiText>{statGroup.name}</EuiText>
</EuiFlexItem>
<EuiFlexItem data-test-subj={`network-stat-${stat.id}`} grow={false}>
<EuiFlexItem grow={false}>
<StatValue
count={stat.count}
isGroupStat={false}
count={statsForGroupCount}
isGroupStat={true}
isLoading={loading}
max={statsForGroupCount}
max={allNetworkStatsCount}
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
}
buttonContentClassName="accordion-button"
>
<AccordionContent>
{statsForGroup.map(stat => (
<EuiFlexGroup key={stat.id} justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">
<Title>{stat.title}</Title>
</EuiText>
</EuiFlexItem>
<EuiFlexItem data-test-subj={`network-stat-${stat.id}`} grow={false}>
<StatValue
count={stat.count}
isGroupStat={false}
isLoading={loading}
max={statsForGroupCount}
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
</AccordionContent>
</EuiAccordion>
{i !== networkStatGroups.length - 1 && <EuiHorizontalRule margin="xs" />}
</React.Fragment>
);
})}

View file

@ -4,51 +4,67 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiProgress, EuiText } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent, EuiProgress, EuiText } from '@elastic/eui';
import numeral from '@elastic/numeral';
import React from 'react';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants';
import { useUiSetting$ } from '../../../lib/kibana';
const ProgressContainer = styled.div`
width: 100px;
margin-left: 8px;
min-width: 100px;
`;
export const StatValue = React.memo<{
const LoadingContent = styled(EuiLoadingContent)`
.euiLoadingContent__singleLine {
margin-bottom: 0px;
}
`;
const StatValueComponent: React.FC<{
count: number;
isLoading: boolean;
isGroupStat: boolean;
isLoading: boolean;
max: number;
}>(({ count, isGroupStat, isLoading, max }) => {
}> = ({ count, isGroupStat, isLoading, max }) => {
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
const [isInitialLoading, setIsInitialLoading] = useState(true);
useEffect(() => {
if (isInitialLoading && !isLoading) {
setIsInitialLoading(false);
}
}, [isLoading, isInitialLoading, setIsInitialLoading]);
return (
<>
{isLoading ? (
<EuiLoadingSpinner data-test-subj="stat-value-loading-spinner" size="m" />
) : (
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiText color={isGroupStat ? 'default' : 'subdued'} size={isGroupStat ? 'm' : 's'}>
{numeral(count).format(defaultNumberFormat)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<ProgressContainer>
<EuiProgress
color={isGroupStat ? 'primary' : 'subdued'}
max={max}
size="m"
value={count}
/>
</ProgressContainer>
</EuiFlexItem>
</EuiFlexGroup>
)}
</>
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
{!isInitialLoading && (
<EuiText color={isGroupStat ? 'default' : 'subdued'} size={isGroupStat ? 'm' : 's'}>
{numeral(count).format(defaultNumberFormat)}
</EuiText>
)}
</EuiFlexItem>
<EuiFlexItem grow={true}>
<ProgressContainer>
{isLoading ? (
<LoadingContent data-test-subj="stat-value-loading-spinner" lines={1} />
) : (
<EuiProgress
color={isGroupStat ? 'primary' : 'subdued'}
max={max}
size="m"
value={count}
/>
)}
</ProgressContainer>
</EuiFlexItem>
</EuiFlexGroup>
);
});
};
StatValue.displayName = 'StatValue';
StatValueComponent.displayName = 'StatValueComponent';
export const StatValue = React.memo(StatValueComponent);

View file

@ -45,14 +45,14 @@ export const RecentTimelineCounts = React.memo<{
timeline: OpenTimelineResult;
}>(({ timeline }) => {
return (
<>
<div>
<IconWithCount
count={getPinnedEventCount(timeline)}
icon="pinFilled"
tooltip={i18n.PINNED_EVENTS}
/>
<IconWithCount count={getNotesCount(timeline)} icon="editorComment" tooltip={i18n.NOTES} />
</>
</div>
);
});

View file

@ -4,62 +4,26 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiLink,
EuiToolTip,
EuiButtonIcon,
} from '@elastic/eui';
import React from 'react';
import { EuiText, EuiLink } from '@elastic/eui';
import React, { useCallback } from 'react';
import { isUntitled } from '../../open_timeline/helpers';
import { OnOpenTimeline, OpenTimelineResult } from '../../open_timeline/types';
import * as i18n from '../translations';
export interface MeApiResponse {
username: string;
}
export const RecentTimelineHeader = React.memo<{
onOpenTimeline: OnOpenTimeline;
timeline: OpenTimelineResult;
}>(({ onOpenTimeline, timeline }) => {
const { title, savedObjectId } = timeline;
}>(({ onOpenTimeline, timeline, timeline: { title, savedObjectId } }) => {
const onClick = useCallback(
() => onOpenTimeline({ duplicate: false, timelineId: `${savedObjectId}` }),
[onOpenTimeline, savedObjectId]
);
return (
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText size="s">
<EuiLink
onClick={() => onOpenTimeline({ duplicate: false, timelineId: `${savedObjectId}` })}
>
{isUntitled(timeline) ? i18n.UNTITLED_TIMELINE : title}
</EuiLink>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip content={i18n.OPEN_AS_DUPLICATE}>
<EuiButtonIcon
aria-label={i18n.OPEN_AS_DUPLICATE}
data-test-subj="open-duplicate"
isDisabled={savedObjectId == null}
iconSize="s"
iconType="copy"
onClick={() =>
onOpenTimeline({
duplicate: true,
timelineId: `${savedObjectId}`,
})
}
size="s"
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<EuiText size="s">
<EuiLink onClick={onClick}>{isUntitled(timeline) ? i18n.UNTITLED_TIMELINE : title}</EuiLink>
</EuiText>
);
});

View file

@ -5,23 +5,22 @@
*/
import ApolloClient from 'apollo-client';
import { EuiHorizontalRule, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui';
import React, { useCallback } from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { ActionCreator } from 'typescript-fsa';
import chrome from 'ui/chrome';
import { AllTimelinesQuery } from '../../containers/timeline/all';
import { SortFieldTimeline, Direction } from '../../graphql/types';
import { fetchUsername, getMeApiUrl } from './helpers';
import { queryTimelineById, dispatchUpdateTimeline } from '../open_timeline/helpers';
import { DispatchUpdateTimeline, OnOpenTimeline } from '../open_timeline/types';
import { RecentTimelines } from './recent_timelines';
import { LoadingPlaceholders } from '../page/overview/loading_placeholders';
import { updateIsLoading as dispatchUpdateIsLoading } from '../../store/timeline/actions';
import { FilterMode } from './types';
import { RecentTimelines } from './recent_timelines';
import * as i18n from './translations';
import { FilterMode } from './types';
export interface MeApiResponse {
username: string;
@ -42,8 +41,6 @@ export type Props = OwnProps & DispatchProps;
const StatefulRecentTimelinesComponent = React.memo<Props>(
({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => {
const actionDispatcher = updateIsLoading as ActionCreator<{ id: string; isLoading: boolean }>;
const [username, setUsername] = useState<string | null | undefined>(undefined);
const LoadingSpinner = useMemo(() => <EuiLoadingSpinner size="m" />, []);
const onOpenTimeline: OnOpenTimeline = useCallback(
({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => {
queryTimelineById({
@ -57,38 +54,6 @@ const StatefulRecentTimelinesComponent = React.memo<Props>(
[apolloClient, updateIsLoading, updateTimeline]
);
useEffect(() => {
let canceled = false;
const fetchData = async () => {
try {
const loggedInUser = await fetchUsername(getMeApiUrl(chrome.getBasePath));
if (!canceled) {
setUsername(loggedInUser);
}
} catch (e) {
if (!canceled) {
setUsername(null);
}
}
};
fetchData();
return () => {
canceled = true;
};
}, []);
if (username === undefined) {
return LoadingSpinner;
} else if (username == null) {
return null;
}
// TODO: why does `createdBy: <username>` specified as a `search` query does not match results?
const noTimelinesMessage =
filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES;
@ -108,7 +73,7 @@ const StatefulRecentTimelinesComponent = React.memo<Props>(
{({ timelines, loading }) => (
<>
{loading ? (
<>{LoadingSpinner}</>
<LoadingPlaceholders lines={2} placeholders={filterBy === 'favorites' ? 1 : 5} />
) : (
<RecentTimelines
noTimelinesMessage={noTimelinesMessage}

View file

@ -4,13 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiSpacer, EuiText } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiText,
EuiToolTip,
EuiButtonIcon,
} from '@elastic/eui';
import React from 'react';
import { RecentTimelineHeader } from './header';
import { OnOpenTimeline, OpenTimelineResult } from '../open_timeline/types';
import { WithHoverActions } from '../with_hover_actions';
import { RecentTimelineCounts } from './counts';
import * as i18n from './translations';
export interface MeApiResponse {
username: string;
@ -34,19 +43,48 @@ export const RecentTimelines = React.memo<{
return (
<>
{timelines.map((t, i) => (
<div key={`${t.savedObjectId}-${i}`}>
<RecentTimelineHeader onOpenTimeline={onOpenTimeline} timeline={t} />
<RecentTimelineCounts timeline={t} />
{t.description && t.description.length && (
<>
<EuiSpacer size="s" />
<EuiText color="subdued" size="xs">
{t.description}
</EuiText>
</>
)}
{i !== timelines.length - 1 && <EuiSpacer size="l" />}
</div>
<React.Fragment key={`${t.savedObjectId}-${i}`}>
<WithHoverActions
render={showHoverContent => (
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<RecentTimelineHeader onOpenTimeline={onOpenTimeline} timeline={t} />
<RecentTimelineCounts timeline={t} />
{t.description && t.description.length && (
<>
<EuiSpacer size="s" />
<EuiText color="subdued" size="xs">
{t.description}
</EuiText>
</>
)}
</EuiFlexItem>
{showHoverContent && (
<EuiFlexItem grow={false}>
<EuiToolTip content={i18n.OPEN_AS_DUPLICATE}>
<EuiButtonIcon
aria-label={i18n.OPEN_AS_DUPLICATE}
data-test-subj="open-duplicate"
isDisabled={t.savedObjectId == null}
iconSize="s"
iconType="copy"
onClick={() =>
onOpenTimeline({
duplicate: true,
timelineId: `${t.savedObjectId}`,
})
}
size="s"
/>
</EuiToolTip>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
/>
<>{i !== timelines.length - 1 && <EuiSpacer size="l" />}</>
</React.Fragment>
))}
</>
);

View file

@ -24,6 +24,7 @@ import { UpdateDateRange } from '../../components/charts/common';
import { SetQuery } from '../../pages/hosts/navigation/types';
export interface OwnProps extends QueryTemplateProps {
chartHeight?: number;
dataKey: string | string[];
defaultStackByOption: MatrixHistogramOption;
errorMessage: string;
@ -37,6 +38,7 @@ export interface OwnProps extends QueryTemplateProps {
isEventsHistogram?: boolean;
legendPosition?: Position;
mapping?: MatrixHistogramMappingTypes;
panelHeight?: number;
query: Maybe<string>;
setQuery: SetQuery;
showLegend?: boolean;

View file

@ -41,34 +41,36 @@ export interface OverviewHostProps extends QueryTemplateProps {
}
const OverviewHostComponentQuery = React.memo<OverviewHostProps & OverviewHostReducer>(
({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => (
<Query<GetOverviewHostQuery.Query, GetOverviewHostQuery.Variables>
query={overviewHostQuery}
fetchPolicy={getDefaultFetchPolicy()}
variables={{
sourceId,
timerange: {
interval: '12h',
from: startDate,
to: endDate,
},
filterQuery: createFilter(filterQuery),
defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY),
inspect: isInspected,
}}
>
{({ data, loading, refetch }) => {
const overviewHost = getOr({}, `source.OverviewHost`, data);
return children({
id,
inspect: getOr(null, 'source.OverviewHost.inspect', data),
overviewHost,
loading,
refetch,
});
}}
</Query>
)
({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => {
return (
<Query<GetOverviewHostQuery.Query, GetOverviewHostQuery.Variables>
query={overviewHostQuery}
fetchPolicy={getDefaultFetchPolicy()}
variables={{
sourceId,
timerange: {
interval: '12h',
from: startDate,
to: endDate,
},
filterQuery: createFilter(filterQuery),
defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY),
inspect: isInspected,
}}
>
{({ data, loading, refetch }) => {
const overviewHost = getOr({}, `source.OverviewHost`, data);
return children({
id,
inspect: getOr(null, 'source.OverviewHost.inspect', data),
overviewHost,
loading,
refetch,
});
}}
</Query>
);
}
);
OverviewHostComponentQuery.displayName = 'OverviewHostComponentQuery';

View file

@ -114,13 +114,13 @@ export const signalsHeaders: ColumnHeader[] = [
columnHeaderType: defaultColumnHeaderType,
id: 'signal.rule.severity',
label: i18n.SIGNALS_HEADERS_SEVERITY,
width: 100,
width: 105,
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'signal.rule.risk_score',
label: i18n.SIGNALS_HEADERS_RISK_SCORE,
width: 120,
width: 115,
},
{
columnHeaderType: defaultColumnHeaderType,

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Position } from '@elastic/charts';
import { EuiButton, EuiSelect, EuiPanel } from '@elastic/eui';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSelect, EuiPanel } from '@elastic/eui';
import numeral from '@elastic/numeral';
import React, { memo, useCallback, useMemo, useState, useEffect } from 'react';
import styled from 'styled-components';
@ -12,8 +12,6 @@ import { isEmpty } from 'lodash/fp';
import { HeaderSection } from '../../../../components/header_section';
import { SignalsHistogram } from './signals_histogram';
import * as i18n from './translations';
import { Query } from '../../../../../../../../../src/plugins/data/common/query';
import { esFilters, esQuery } from '../../../../../../../../../src/plugins/data/common/es_query';
import { RegisterQuery, SignalsHistogramOption, SignalsAggregation, SignalsTotal } from './types';
@ -26,8 +24,14 @@ import { useQuerySignals } from '../../../../containers/detection_engine/signals
import { MatrixLoader } from '../../../../components/matrix_histogram/matrix_loader';
import { formatSignalsData, getSignalsHistogramQuery } from './helpers';
import * as i18n from './translations';
const StyledEuiPanel = styled(EuiPanel)`
const DEFAULT_PANEL_HEIGHT = 300;
const StyledEuiPanel = styled(EuiPanel)<{ height?: number }>`
display: flex;
flex-direction: column;
${({ height }) => (height != null ? `height: ${height}px;` : '')}
position: relative;
`;
@ -38,7 +42,12 @@ const defaultTotalSignalsObj: SignalsTotal = {
export const DETECTIONS_HISTOGRAM_ID = 'detections-histogram';
const ViewSignalsFlexItem = styled(EuiFlexItem)`
margin-left: 24px;
`;
interface SignalsHistogramPanelProps {
chartHeight?: number;
defaultStackByOption?: SignalsHistogramOption;
deleteQuery?: ({ id }: { id: string }) => void;
filters?: esFilters.Filter[];
@ -46,6 +55,7 @@ interface SignalsHistogramPanelProps {
query?: Query;
legendPosition?: Position;
loadingInitial?: boolean;
panelHeight?: number;
signalIndexName: string | null;
setQuery: (params: RegisterQuery) => void;
showLinkToSignals?: boolean;
@ -58,6 +68,7 @@ interface SignalsHistogramPanelProps {
export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>(
({
chartHeight,
defaultStackByOption = signalsHistogramOptions[0],
deleteQuery,
filters,
@ -65,6 +76,7 @@ export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>(
from,
legendPosition = 'right',
loadingInitial = false,
panelHeight = DEFAULT_PANEL_HEIGHT,
setQuery,
signalIndexName,
showLinkToSignals = false,
@ -171,7 +183,7 @@ export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>(
return (
<InspectButtonContainer show={!isInitialLoading}>
<StyledEuiPanel>
<StyledEuiPanel height={panelHeight}>
{isInitialLoading ? (
<>
<HeaderSection id={DETECTIONS_HISTOGRAM_ID} title={title} />
@ -184,26 +196,33 @@ export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>(
title={title}
subtitle={showTotalSignalsCount && totalSignals}
>
{stackByOptions && (
<EuiSelect
onChange={setSelectedOptionCallback}
options={stackByOptions}
prepend={i18n.STACK_BY_LABEL}
value={selectedStackByOption.value}
/>
)}
{showLinkToSignals && (
<EuiButton href={getDetectionEngineUrl()}>{i18n.VIEW_SIGNALS}</EuiButton>
)}
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
{stackByOptions && (
<EuiSelect
onChange={setSelectedOptionCallback}
options={stackByOptions}
prepend={i18n.STACK_BY_LABEL}
value={selectedStackByOption.value}
/>
)}
</EuiFlexItem>
{showLinkToSignals && (
<ViewSignalsFlexItem grow={false}>
<EuiButton href={getDetectionEngineUrl()}>{i18n.VIEW_SIGNALS}</EuiButton>
</ViewSignalsFlexItem>
)}
</EuiFlexGroup>
</HeaderSection>
<SignalsHistogram
chartHeight={chartHeight}
data={formattedSignalsData}
from={from}
legendPosition={legendPosition}
loading={isLoadingSignals}
to={to}
updateDateRange={updateDateRange}
loading={isLoadingSignals}
/>
</>
)}

View file

@ -19,7 +19,10 @@ import { useTheme } from '../../../../components/charts/common';
import { histogramDateTimeFormatter } from '../../../../components/utils';
import { HistogramData } from './types';
const DEFAULT_CHART_HEIGHT = 174;
interface HistogramSignalsProps {
chartHeight?: number;
from: number;
legendPosition?: Position;
loading: boolean;
@ -29,7 +32,15 @@ interface HistogramSignalsProps {
}
export const SignalsHistogram = React.memo<HistogramSignalsProps>(
({ to, from, legendPosition = 'right', data, updateDateRange, loading }) => {
({
chartHeight = DEFAULT_CHART_HEIGHT,
data,
from,
legendPosition = 'right',
loading,
to,
updateDateRange,
}) => {
const theme = useTheme();
return (
@ -43,7 +54,7 @@ export const SignalsHistogram = React.memo<HistogramSignalsProps>(
/>
)}
<Chart size={['100%', 324]}>
<Chart size={['100%', chartHeight]}>
<Settings
legendPosition={legendPosition}
onBrushEnd={updateDateRange}

View file

@ -173,8 +173,9 @@ const DetectionEnginePageComponent: React.FC<DetectionEnginePageComponentProps>
from={from}
loadingInitial={loading}
query={query}
signalIndexName={signalIndexName}
setQuery={setQuery}
showTotalSignalsCount={true}
signalIndexName={signalIndexName}
stackByOptions={signalsHistogramOptions}
to={to}
updateDateRange={updateDateRangeCallback}

View file

@ -36,7 +36,7 @@ import { HostsTabs } from './hosts_tabs';
import { navTabsHosts } from './nav_tabs';
import * as i18n from './translations';
import { HostsComponentProps, HostsComponentReduxProps } from './types';
import { filterAlertsHosts } from './navigation';
import { filterHostData } from './navigation';
import { HostsTableType } from '../../store/hosts/model';
const KpiHostsComponentManage = manageQuery(KpiHostsComponent);
@ -58,7 +58,7 @@ export const HostsComponent = React.memo<HostsComponentProps>(
const { tabName } = useParams();
const tabsFilters = React.useMemo(() => {
if (tabName === HostsTableType.alerts) {
return filters.length > 0 ? [...filters, ...filterAlertsHosts] : filterAlertsHosts;
return filters.length > 0 ? [...filters, ...filterHostData] : filterHostData;
}
return filters;
}, [tabName, filters]);

View file

@ -10,7 +10,7 @@ import { esFilters } from '../../../../../../../../src/plugins/data/common/es_qu
import { AlertsView } from '../../../components/alerts_viewer';
import { AlertsComponentQueryProps } from './types';
export const filterAlertsHosts: esFilters.Filter[] = [
export const filterHostData: esFilters.Filter[] = [
{
query: {
bool: {
@ -44,7 +44,7 @@ export const filterAlertsHosts: esFilters.Filter[] = [
export const HostAlertsQueryTabBody = React.memo((alertsProps: AlertsComponentQueryProps) => {
const { pageFilters, ...rest } = alertsProps;
const hostPageFilters = useMemo(
() => (pageFilters != null ? [...filterAlertsHosts, ...pageFilters] : filterAlertsHosts),
() => (pageFilters != null ? [...filterHostData, ...pageFilters] : filterHostData),
[pageFilters]
);

View file

@ -26,6 +26,10 @@ export const eventsStackByOptions: MatrixHistogramOption[] = [
text: 'event.dataset',
value: 'event.dataset',
},
{
text: 'event.module',
value: 'event.module',
},
];
export const EventsQueryTabBody = ({

View file

@ -10,7 +10,7 @@ import { esFilters } from '../../../../../../../../src/plugins/data/common/es_qu
import { AlertsView } from '../../../components/alerts_viewer';
import { NetworkComponentQueryProps } from './types';
export const filterAlertsNetwork: esFilters.Filter[] = [
export const filterNetworkData: esFilters.Filter[] = [
{
query: {
bool: {
@ -62,7 +62,7 @@ export const filterAlertsNetwork: esFilters.Filter[] = [
];
export const NetworkAlertsQueryTabBody = React.memo((alertsProps: NetworkComponentQueryProps) => (
<AlertsView {...alertsProps} pageFilters={filterAlertsNetwork} />
<AlertsView {...alertsProps} pageFilters={filterNetworkData} />
));
NetworkAlertsQueryTabBody.displayName = 'NetworkAlertsQueryTabBody';

View file

@ -29,7 +29,7 @@ import { networkModel, State, inputsSelectors } from '../../store';
import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions';
import { SpyRoute } from '../../utils/route/spy_routes';
import { navTabsNetwork, NetworkRoutes, NetworkRoutesLoading } from './navigation';
import { filterAlertsNetwork } from './navigation/alerts_query_tab_body';
import { filterNetworkData } from './navigation/alerts_query_tab_body';
import { NetworkEmptyPage } from './network_empty_page';
import * as i18n from './translations';
import { NetworkComponentProps } from './types';
@ -56,7 +56,7 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
const tabsFilters = useMemo(() => {
if (tabName === NetworkRouteType.alerts) {
return filters.length > 0 ? [...filters, ...filterAlertsNetwork] : filterAlertsNetwork;
return filters.length > 0 ? [...filters, ...filterNetworkData] : filterNetworkData;
}
return filters;
}, [tabName, filters]);

View file

@ -7,9 +7,8 @@
import { EuiButton } from '@elastic/eui';
import numeral from '@elastic/numeral';
import React, { useCallback, useEffect, useMemo } from 'react';
import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public';
import styled from 'styled-components';
import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants';
import {
ERROR_FETCHING_ALERTS_DATA,
SHOWING,
@ -22,10 +21,14 @@ import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/in
import { useKibana, useUiSetting$ } from '../../../lib/kibana';
import { convertToBuildEsQuery } from '../../../lib/keury';
import { SetAbsoluteRangeDatePicker } from '../../network/types';
import { esQuery } from '../../../../../../../../src/plugins/data/public';
import {
esFilters,
esQuery,
IIndexPattern,
Query,
} from '../../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../../store';
import { HostsType } from '../../../store/hosts/model';
import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants';
import * as i18n from '../translations';
@ -33,6 +36,7 @@ const ID = 'alertsByCategoryOverview';
const NO_FILTERS: esFilters.Filter[] = [];
const DEFAULT_QUERY: Query = { query: '', language: 'kuery' };
const DEFAULT_STACK_BY = 'event.module';
interface Props {
deleteQuery?: ({ id }: { id: string }) => void;
@ -51,80 +55,77 @@ interface Props {
to: number;
}
const ViewAlertsButton = styled(EuiButton)`
margin-left: 8px;
`;
const AlertsByCategoryComponent: React.FC<Props> = ({
deleteQuery,
filters = NO_FILTERS,
from,
hideHeaderChildren = false,
indexPattern,
query = DEFAULT_QUERY,
setAbsoluteRangeDatePicker,
setQuery,
to,
}) => {
useEffect(() => {
return () => {
if (deleteQuery) {
deleteQuery({ id: ID });
}
};
}, []);
export const AlertsByCategory = React.memo<Props>(
({
deleteQuery,
filters = NO_FILTERS,
from,
hideHeaderChildren = false,
indexPattern,
query = DEFAULT_QUERY,
setAbsoluteRangeDatePicker,
setQuery,
to,
}) => {
useEffect(() => {
return () => {
if (deleteQuery) {
deleteQuery({ id: ID });
}
};
}, []);
const kibana = useKibana();
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
const kibana = useKibana();
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
const updateDateRangeCallback = useCallback(
(min: number, max: number) => {
setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max });
},
[setAbsoluteRangeDatePicker]
);
const alertsCountViewAlertsButton = useMemo(
() => <EuiButton href={getDetectionEngineAlertUrl()}>{i18n.VIEW_ALERTS}</EuiButton>,
[]
);
const updateDateRangeCallback = useCallback(
(min: number, max: number) => {
setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max });
},
[setAbsoluteRangeDatePicker]
);
const alertsCountViewAlertsButton = useMemo(
() => (
<ViewAlertsButton href={getDetectionEngineAlertUrl()}>{i18n.VIEW_ALERTS}</ViewAlertsButton>
),
[]
);
const getSubtitle = useCallback(
(totalCount: number) =>
`${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`,
[]
);
const getSubtitle = useCallback(
(totalCount: number) =>
`${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`,
[]
);
const defaultStackByOption =
alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0];
return (
<MatrixHistogramContainer
dataKey="AlertsHistogram"
defaultStackByOption={alertsStackByOptions[0]}
endDate={to}
errorMessage={ERROR_FETCHING_ALERTS_DATA}
filterQuery={convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
})}
headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton}
id={ID}
isAlertsHistogram={true}
legendPosition={'right'}
query={MatrixHistogramGqlQuery}
setQuery={setQuery}
sourceId="default"
stackByOptions={alertsStackByOptions}
startDate={from}
title={i18n.ALERTS_GRAPH_TITLE}
subtitle={getSubtitle}
type={HostsType.page}
updateDateRange={updateDateRangeCallback}
/>
);
}
);
return (
<MatrixHistogramContainer
dataKey="AlertsHistogram"
defaultStackByOption={defaultStackByOption}
endDate={to}
errorMessage={ERROR_FETCHING_ALERTS_DATA}
filterQuery={convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
})}
headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton}
id={ID}
isAlertsHistogram={true}
legendPosition={'right'}
query={MatrixHistogramGqlQuery}
setQuery={setQuery}
sourceId="default"
stackByOptions={alertsStackByOptions}
startDate={from}
title={i18n.ALERTS_GRAPH_TITLE}
subtitle={getSubtitle}
type={HostsType.page}
updateDateRange={updateDateRangeCallback}
/>
);
};
AlertsByCategory.displayName = 'AlertsByCategory';
AlertsByCategoryComponent.displayName = 'AlertsByCategoryComponent';
export const AlertsByCategory = React.memo(AlertsByCategoryComponent);

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { mount } from 'enzyme';
import React from 'react';
import { OverviewHostProps } from '../../../components/page/overview/overview_host';
import { OverviewNetworkProps } from '../../../components/page/overview/overview_network';
import { mockIndexPattern, TestProviders } from '../../../mock';
import { EventCounts } from '.';
describe('EventCounts', () => {
const from = 1579553397080;
const to = 1579639797080;
test('it filters the `Host events` widget with a `host.name` `exists` filter', () => {
const wrapper = mount(
<TestProviders>
<EventCounts from={from} indexPattern={mockIndexPattern} setQuery={jest.fn()} to={to} />
</TestProviders>
);
expect(
(wrapper
.find('[data-test-subj="overview-host-query"]')
.first()
.props() as OverviewHostProps).filterQuery
).toContain('[{"bool":{"should":[{"exists":{"field":"host.name"}}]');
});
test('it filters the `Network events` widget with a `source.ip` or `destination.ip` `exists` filter', () => {
const wrapper = mount(
<TestProviders>
<EventCounts from={from} indexPattern={mockIndexPattern} setQuery={jest.fn()} to={to} />
</TestProviders>
);
expect(
(wrapper
.find('[data-test-subj="overview-network-query"]')
.first()
.props() as OverviewNetworkProps).filterQuery
).toContain(
'{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"exists":{"field":"source.ip"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"destination.ip"}}],"minimum_should_match":1}}],"minimum_should_match":1}}]}}]'
);
});
});

View file

@ -6,14 +6,20 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public';
import styled from 'styled-components';
import { OverviewHost } from '../../../components/page/overview/overview_host';
import { OverviewNetwork } from '../../../components/page/overview/overview_network';
import { filterHostData } from '../../hosts/navigation/alerts_query_tab_body';
import { useKibana } from '../../../lib/kibana';
import { convertToBuildEsQuery } from '../../../lib/keury';
import { esQuery } from '../../../../../../../../src/plugins/data/public';
import { filterNetworkData } from '../../network/navigation/alerts_query_tab_body';
import {
esFilters,
esQuery,
IIndexPattern,
Query,
} from '../../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../../store';
const HorizontalSpacer = styled(EuiFlexItem)`
@ -56,7 +62,7 @@ const EventCountsComponent: React.FC<Props> = ({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
filters: [...filters, ...filterHostData],
})}
startDate={from}
setQuery={setQuery}
@ -72,7 +78,7 @@ const EventCountsComponent: React.FC<Props> = ({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
filters: [...filters, ...filterNetworkData],
})}
startDate={from}
setQuery={setQuery}

View file

@ -7,8 +7,6 @@
import { EuiButton } from '@elastic/eui';
import numeral from '@elastic/numeral';
import React, { useCallback, useEffect, useMemo } from 'react';
import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public';
import styled from 'styled-components';
import {
ERROR_FETCHING_EVENTS_DATA,
@ -20,10 +18,14 @@ import { SetAbsoluteRangeDatePicker } from '../../network/types';
import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts';
import { MatrixHistogramContainer } from '../../../containers/matrix_histogram';
import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query';
import { MatrixHistogramOption } from '../../../components/matrix_histogram/types';
import { eventsStackByOptions } from '../../hosts/navigation';
import { useKibana, useUiSetting$ } from '../../../lib/kibana';
import { esQuery } from '../../../../../../../../src/plugins/data/public';
import {
esFilters,
esQuery,
IIndexPattern,
Query,
} from '../../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../../store';
import { HostsTableType, HostsType } from '../../../store/hosts/model';
import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants';
@ -32,6 +34,7 @@ import * as i18n from '../translations';
const NO_FILTERS: esFilters.Filter[] = [];
const DEFAULT_QUERY: Query = { query: '', language: 'kuery' };
const DEFAULT_STACK_BY = 'event.dataset';
const ID = 'eventsByDatasetOverview';
@ -51,85 +54,82 @@ interface Props {
to: number;
}
const ViewEventsButton = styled(EuiButton)`
margin-left: 8px;
`;
const EventsByDatasetComponent: React.FC<Props> = ({
deleteQuery,
filters = NO_FILTERS,
from,
indexPattern,
query = DEFAULT_QUERY,
setAbsoluteRangeDatePicker,
setQuery,
to,
}) => {
useEffect(() => {
return () => {
if (deleteQuery) {
deleteQuery({ id: ID });
}
};
}, []);
export const EventsByDataset = React.memo<Props>(
({
deleteQuery,
filters = NO_FILTERS,
from,
indexPattern,
query = DEFAULT_QUERY,
setAbsoluteRangeDatePicker,
setQuery,
to,
}) => {
useEffect(() => {
return () => {
if (deleteQuery) {
deleteQuery({ id: ID });
}
};
}, []);
const kibana = useKibana();
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
const kibana = useKibana();
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
const updateDateRangeCallback = useCallback(
(min: number, max: number) => {
setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max });
},
[setAbsoluteRangeDatePicker]
);
const eventsCountViewEventsButton = useMemo(
() => <EuiButton href={getTabsOnHostsUrl(HostsTableType.events)}>{i18n.VIEW_EVENTS}</EuiButton>,
[]
);
const updateDateRangeCallback = useCallback(
(min: number, max: number) => {
setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max });
},
[setAbsoluteRangeDatePicker]
);
const eventsCountViewEventsButton = useMemo(
() => (
<ViewEventsButton href={getTabsOnHostsUrl(HostsTableType.events)}>
{i18n.VIEW_EVENTS}
</ViewEventsButton>
),
[]
);
const getSubtitle = useCallback(
(totalCount: number) =>
`${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`,
[]
);
const getTitle = useCallback(
(option: MatrixHistogramOption) => i18n.EVENTS_COUNT_BY(option.text),
[]
);
const getSubtitle = useCallback(
(totalCount: number) =>
`${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`,
[]
);
const defaultStackByOption =
eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0];
return (
<MatrixHistogramContainer
dataKey="EventsHistogram"
defaultStackByOption={eventsStackByOptions[1]}
endDate={to}
errorMessage={ERROR_FETCHING_EVENTS_DATA}
filterQuery={convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
})}
headerChildren={eventsCountViewEventsButton}
id={ID}
isEventsHistogram={true}
legendPosition={'right'}
query={MatrixHistogramGqlQuery}
setQuery={setQuery}
sourceId="default"
stackByOptions={eventsStackByOptions}
startDate={from}
title={getTitle}
subtitle={getSubtitle}
type={HostsType.page}
updateDateRange={updateDateRangeCallback}
/>
);
}
);
const filterQuery = useMemo(
() =>
convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
indexPattern,
queries: [query],
filters,
}),
[kibana, indexPattern, query, filters]
);
EventsByDataset.displayName = 'EventsByDataset';
return (
<MatrixHistogramContainer
dataKey="EventsHistogram"
defaultStackByOption={defaultStackByOption}
endDate={to}
errorMessage={ERROR_FETCHING_EVENTS_DATA}
filterQuery={filterQuery}
headerChildren={eventsCountViewEventsButton}
id={ID}
isEventsHistogram={true}
legendPosition={'right'}
query={MatrixHistogramGqlQuery}
setQuery={setQuery}
sourceId="default"
stackByOptions={eventsStackByOptions}
startDate={from}
title={i18n.EVENTS}
subtitle={getSubtitle}
type={HostsType.page}
updateDateRange={updateDateRangeCallback}
/>
);
};
EventsByDatasetComponent.displayName = 'EventsByDatasetComponent';
export const EventsByDataset = React.memo(EventsByDatasetComponent);

View file

@ -64,51 +64,57 @@ const OverviewComponent: React.FC<OverviewComponentReduxProps> = ({
<EuiFlexItem grow={true}>
<GlobalTime>
{({ from, deleteQuery, setQuery, to }) => (
<>
<EventsByDataset
deleteQuery={deleteQuery}
filters={filters}
from={from}
indexPattern={indexPattern}
query={query}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!}
setQuery={setQuery}
to={to}
/>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<SignalsByCategory
filters={filters}
from={from}
indexPattern={indexPattern}
query={query}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!}
setQuery={setQuery}
to={to}
/>
<EuiSpacer size="l" />
</EuiFlexItem>
<EventCounts
filters={filters}
from={from}
indexPattern={indexPattern}
query={query}
setQuery={setQuery}
to={to}
/>
<EuiFlexItem grow={false}>
<AlertsByCategory
deleteQuery={deleteQuery}
filters={filters}
from={from}
indexPattern={indexPattern}
query={query}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!}
setQuery={setQuery}
to={to}
/>
</EuiFlexItem>
<EuiSpacer size="l" />
<EuiFlexItem grow={false}>
<EventsByDataset
deleteQuery={deleteQuery}
filters={filters}
from={from}
indexPattern={indexPattern}
query={query}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!}
setQuery={setQuery}
to={to}
/>
</EuiFlexItem>
<AlertsByCategory
deleteQuery={deleteQuery}
filters={filters}
from={from}
indexPattern={indexPattern}
query={query}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!}
setQuery={setQuery}
to={to}
/>
<SignalsByCategory
deleteQuery={deleteQuery}
filters={filters}
from={from}
indexPattern={indexPattern}
query={query}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!}
setQuery={setQuery}
to={to}
/>
</>
<EuiFlexItem grow={false}>
<EventCounts
filters={filters}
from={from}
indexPattern={indexPattern}
query={query}
setQuery={setQuery}
to={to}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</GlobalTime>
</EuiFlexItem>

View file

@ -13,7 +13,7 @@ import { useKibana } from '../../../lib/kibana';
const basePath = chrome.getBasePath();
export const OverviewEmpty = React.memo(() => {
const OverviewEmptyComponent: React.FC = () => {
const docLinks = useKibana().services.docLinks;
return (
@ -30,6 +30,8 @@ export const OverviewEmpty = React.memo(() => {
title={i18nCommon.EMPTY_TITLE}
/>
);
});
};
OverviewEmpty.displayName = 'OverviewEmpty';
OverviewEmptyComponent.displayName = 'OverviewEmptyComponent';
export const OverviewEmpty = React.memo(OverviewEmptyComponent);

View file

@ -5,16 +5,18 @@
*/
import React, { useCallback } from 'react';
import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public';
import { useSignalIndex } from '../../../containers/detection_engine/signals/use_signal_index';
import { SignalsHistogramPanel } from '../../detection_engine/components/signals_histogram_panel';
import { signalsHistogramOptions } from '../../detection_engine/components/signals_histogram_panel/config';
import { useSignalIndex } from '../../../containers/detection_engine/signals/use_signal_index';
import { SetAbsoluteRangeDatePicker } from '../../network/types';
import { esFilters, IIndexPattern, Query } from '../../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../../store';
import * as i18n from '../translations';
const NO_FILTERS: esFilters.Filter[] = [];
const DEFAULT_QUERY: Query = { query: '', language: 'kuery' };
const DEFAULT_STACK_BY = 'signal.rule.threat.tactic.name';
const NO_FILTERS: esFilters.Filter[] = [];
interface Props {
deleteQuery?: ({ id }: { id: string }) => void;
@ -32,47 +34,46 @@ interface Props {
to: number;
}
export const SignalsByCategory = React.memo<Props>(
({
deleteQuery,
filters = NO_FILTERS,
from,
query = DEFAULT_QUERY,
setAbsoluteRangeDatePicker,
setQuery,
to,
}) => {
const updateDateRangeCallback = useCallback(
(min: number, max: number) => {
setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max });
},
[setAbsoluteRangeDatePicker]
);
const defaultStackByOption = {
text: `${i18n.SIGNALS_BY_CATEGORY}`,
value: 'signal.rule.threat',
};
const SignalsByCategoryComponent: React.FC<Props> = ({
deleteQuery,
filters = NO_FILTERS,
from,
query = DEFAULT_QUERY,
setAbsoluteRangeDatePicker,
setQuery,
to,
}) => {
const { signalIndexName } = useSignalIndex();
const updateDateRangeCallback = useCallback(
(min: number, max: number) => {
setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max });
},
[setAbsoluteRangeDatePicker]
);
const { signalIndexName } = useSignalIndex();
const defaultStackByOption =
signalsHistogramOptions.find(o => o.text === DEFAULT_STACK_BY) ?? signalsHistogramOptions[0];
return (
<SignalsHistogramPanel
deleteQuery={deleteQuery}
filters={filters}
from={from}
query={query}
signalIndexName={signalIndexName}
setQuery={setQuery}
showTotalSignalsCount={true}
showLinkToSignals={true}
defaultStackByOption={defaultStackByOption}
legendPosition="right"
to={to}
title={i18n.SIGNALS_BY_CATEGORY}
updateDateRange={updateDateRangeCallback}
/>
);
}
);
return (
<SignalsHistogramPanel
deleteQuery={deleteQuery}
defaultStackByOption={defaultStackByOption}
filters={filters}
from={from}
query={query}
signalIndexName={signalIndexName}
setQuery={setQuery}
showTotalSignalsCount={true}
showLinkToSignals={true}
stackByOptions={signalsHistogramOptions}
legendPosition={'right'}
to={to}
title={i18n.SIGNAL_COUNT}
updateDateRange={updateDateRangeCallback}
/>
);
};
SignalsByCategory.displayName = 'SignalsByCategory';
SignalsByCategoryComponent.displayName = 'SignalsByCategoryComponent';
export const SignalsByCategory = React.memo(SignalsByCategoryComponent);

View file

@ -6,21 +6,13 @@
import { i18n } from '@kbn/i18n';
export const ALERTS_COUNT_BY = (groupByField: string) =>
i18n.translate('xpack.siem.overview.alertsCountByTitle', {
values: { groupByField },
defaultMessage: 'Alerts count by {groupByField}',
});
export const ALERTS_GRAPH_TITLE = i18n.translate('xpack.siem.overview.alertsGraphTitle', {
defaultMessage: 'External alerts count',
});
export const EVENTS_COUNT_BY = (groupByField: string) =>
i18n.translate('xpack.siem.overview.eventsCountByTitle', {
values: { groupByField },
defaultMessage: 'Events count by {groupByField}',
});
export const EVENTS = i18n.translate('xpack.siem.overview.eventsTitle', {
defaultMessage: 'Events count',
});
export const NEWS_FEED_TITLE = i18n.translate('xpack.siem.overview.newsFeedSidebarTitle', {
defaultMessage: 'Security news',
@ -38,8 +30,8 @@ export const RECENT_TIMELINES = i18n.translate('xpack.siem.overview.recentTimeli
defaultMessage: 'Recent timelines',
});
export const SIGNALS_BY_CATEGORY = i18n.translate('xpack.siem.overview.signalsByCategoryTitle', {
defaultMessage: 'Signals count by MITRE ATT&CK\\u2122 category',
export const SIGNAL_COUNT = i18n.translate('xpack.siem.overview.signalCountTitle', {
defaultMessage: 'Signals count',
});
export const VIEW_ALERTS = i18n.translate('xpack.siem.overview.viewAlertsButtonLabel', {

View file

@ -13323,7 +13323,6 @@
"xpack.siem.open.timeline.untitledTimelineLabel": "無題のタイムライン",
"xpack.siem.open.timeline.withLabel": "With",
"xpack.siem.open.timeline.zeroTimelinesMatchLabel": "0 件のタイムラインが検索条件に一致",
"xpack.siem.overview.alertsCountByTitle": "{groupByField}別アラートカウント",
"xpack.siem.overview.auditBeatAuditTitle": "監査",
"xpack.siem.overview.auditBeatFimTitle": "File Integrityモジュール",
"xpack.siem.overview.auditBeatLoginTitle": "ログイン",
@ -13338,7 +13337,6 @@
"xpack.siem.overview.endgameProcessTitle": "プロセス",
"xpack.siem.overview.endgameRegistryTitle": "レジストリ",
"xpack.siem.overview.endgameSecurityTitle": "セキュリティ",
"xpack.siem.overview.eventsCountByTitle": "{groupByField}別イベントカウント",
"xpack.siem.overview.feedbackText": "Elastic SIEM に関するご意見やご提案は、お気軽に {feedback}",
"xpack.siem.overview.feedbackText.feedbackLinkText": "フィードバックをオンラインで送信",
"xpack.siem.overview.feedbackTitle": "フィードバック",
@ -13367,7 +13365,6 @@
"xpack.siem.overview.pageSubtitle": "Elastic Stackによるセキュリティ情報とイベント管理",
"xpack.siem.overview.pageTitle": "SIEM",
"xpack.siem.overview.recentTimelinesSidebarTitle": "最近のタイムライン",
"xpack.siem.overview.signalsByCategoryTitle": "MITRE ATT&amp;CK\\u2122カテゴリー別シグナルカウント",
"xpack.siem.overview.startedText": "セキュリティ情報およびイベント管理SIEMへようこそ。はじめに{docs}や{data}をご参照ください。今後の機能に関する情報やチュートリアルは、{siemSolution} ページをお見逃しなく。",
"xpack.siem.overview.startedText.dataLinkText": "投入データ",
"xpack.siem.overview.startedText.docsLinkText": "ドキュメンテーション",

View file

@ -13324,7 +13324,6 @@
"xpack.siem.open.timeline.untitledTimelineLabel": "未命名时间线",
"xpack.siem.open.timeline.withLabel": "具有",
"xpack.siem.open.timeline.zeroTimelinesMatchLabel": "0 个时间线匹配搜索条件",
"xpack.siem.overview.alertsCountByTitle": "按 {groupByField} 划分的告警计数",
"xpack.siem.overview.auditBeatAuditTitle": "审计",
"xpack.siem.overview.auditBeatFimTitle": "文件完整性模块",
"xpack.siem.overview.auditBeatLoginTitle": "登录",
@ -13339,7 +13338,6 @@
"xpack.siem.overview.endgameProcessTitle": "进程",
"xpack.siem.overview.endgameRegistryTitle": "注册表",
"xpack.siem.overview.endgameSecurityTitle": "安全性",
"xpack.siem.overview.eventsCountByTitle": "按 {groupByField} 划分的事件计数",
"xpack.siem.overview.feedbackText": "如果您对 Elastic SIEM 体验有任何建议,请随时{feedback}。",
"xpack.siem.overview.feedbackText.feedbackLinkText": "在线提交反馈",
"xpack.siem.overview.feedbackTitle": "反馈",
@ -13368,7 +13366,6 @@
"xpack.siem.overview.pageSubtitle": "Elastic Stack 的安全信息和事件管理功能",
"xpack.siem.overview.pageTitle": "SIEM",
"xpack.siem.overview.recentTimelinesSidebarTitle": "最近的时间线",
"xpack.siem.overview.signalsByCategoryTitle": "按 MITRE ATT&amp;CK\\u2122 类别划分的信号计数",
"xpack.siem.overview.startedText": "欢迎使用安全信息和事件管理 (SIEM)。首先,查看我们的 {docs} 或 {data}。有关即将推出的功能和教程,确保查看我们的{siemSolution}页。",
"xpack.siem.overview.startedText.dataLinkText": "正在采集数据",
"xpack.siem.overview.startedText.docsLinkText": "文档",