mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* [SIEM] Overview page feedback (#56261) ## [SIEM] Overview page feedback Implements feedback and fixes to the Overview page ### Overview (default theme)  ### Overview (dark theme)  ## 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  * 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:
parent
63e435c866
commit
ff7aa450d9
42 changed files with 1863 additions and 1399 deletions
|
@ -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" />
|
||||
</>
|
||||
|
|
|
@ -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 = () => (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]: [
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
||||
|
|
|
@ -26,6 +26,10 @@ export const eventsStackByOptions: MatrixHistogramOption[] = [
|
|||
text: 'event.dataset',
|
||||
value: 'event.dataset',
|
||||
},
|
||||
{
|
||||
text: 'event.module',
|
||||
value: 'event.module',
|
||||
},
|
||||
];
|
||||
|
||||
export const EventsQueryTabBody = ({
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}}]}}]'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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&CK\\u2122カテゴリー別シグナルカウント",
|
||||
"xpack.siem.overview.startedText": "セキュリティ情報およびイベント管理(SIEM)へようこそ。はじめに{docs}や{data}をご参照ください。今後の機能に関する情報やチュートリアルは、{siemSolution} ページをお見逃しなく。",
|
||||
"xpack.siem.overview.startedText.dataLinkText": "投入データ",
|
||||
"xpack.siem.overview.startedText.docsLinkText": "ドキュメンテーション",
|
||||
|
|
|
@ -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&CK\\u2122 类别划分的信号计数",
|
||||
"xpack.siem.overview.startedText": "欢迎使用安全信息和事件管理 (SIEM)。首先,查看我们的 {docs} 或 {data}。有关即将推出的功能和教程,确保查看我们的{siemSolution}页。",
|
||||
"xpack.siem.overview.startedText.dataLinkText": "正在采集数据",
|
||||
"xpack.siem.overview.startedText.docsLinkText": "文档",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue