[SIEM] Detection engine placeholders (#50220) (#50909)

* attempt at getting nav working

* fix detection-engine href redirect issue

* rough out basic page routing

* kql placeholder

* update page and panel headers

* rough out card table poc styles

* change HeaderPanel to HeaderSection

* cleanup and unit test updates

* rough out utilityBar poc

* clean up UtilityBar naming and styles

* support popovers in utility bar

* refactor icon side

* UtilityBar unit tests

* remove page tests for now

* adjust routes

* add comment

* cleanup chart

* open/closed signals content toggle

* remove EuiFilterButton icons

* fix misaligned popover button

* add split prop for HeaderSection

* fleshing out activity monitor table

* update global header to include logo

* fix tests

* correct table types; thanks Garrett!

* LinkIcon comp poc

* fix bugs, errors, tests

* rm import

* table cleanup

* correct merge errors

* switch All Rules to EuiBasicTable

* expand table types and values

* fleshing out all rules table

* rough out rule details

* move chart to separate comp

* update supplement layout

* example rule fail

* switch to new discover-style search

* add ProgressInline comp

* update unit tests and snapshots

* cleanup

* correct merge weirdness

* move text styles to all subtitle items

* correct invalid nav markup; update tests; cleanup

* fix console errors

* add empty page

* change to EuiButtonEmpty in HeaderGlobal

* overflow popover

* rough out edit layout

* new WrapperPage comp POC

* cleanup

* var for timeline gutter

* tests and snapshots update

* fix type + review + re-arrange code

* adding feature flag + fix route issue

* fix type with unit test

* Removing unused translation
This commit is contained in:
Xavier Mouligneau 2019-11-18 10:57:43 -05:00 committed by GitHub
parent 7927447c70
commit 14579af0c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
98 changed files with 4901 additions and 840 deletions

View file

@ -5,16 +5,16 @@
*/
/** Top-level (global) navigation link to the `Hosts` page */
export const NAVIGATION_HOSTS = '[data-test-subj="navigation-link-hosts"]';
export const NAVIGATION_HOSTS = '[data-test-subj="navigation-hosts"]';
/** Top-level (global) navigation link to the `Network` page */
export const NAVIGATION_NETWORK = '[data-test-subj="navigation-link-network"]';
export const NAVIGATION_NETWORK = '[data-test-subj="navigation-network"]';
/** Top-level (global) navigation link to the `Overview` page */
export const NAVIGATION_OVERVIEW = '[data-test-subj="navigation-link-overview"]';
export const NAVIGATION_OVERVIEW = '[data-test-subj="navigation-overview"]';
/** Top-level (global) navigation link to the `Timelines` page */
export const NAVIGATION_TIMELINES = '[data-test-subj="navigation-link-timelines"]';
export const NAVIGATION_TIMELINES = '[data-test-subj="navigation-timelines"]';
export const HOSTS_PAGE_TABS = {
allHosts: '[data-test-subj="navigation-allHosts"]',

View file

@ -0,0 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UtilityBar it renders 1`] = `
<Component>
<UtilityBar>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>
Test text
</UtilityBarText>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarAction
iconType=""
popoverContent={
<p>
Test popover
</p>
}
>
Test action
</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarAction
iconType="cross"
>
Test action
</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
</Component>
`;

View file

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UtilityBarAction it renders 1`] = `
<Component>
<UtilityBarAction
iconType="alert"
>
Test action
</UtilityBarAction>
</Component>
`;

View file

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UtilityBarGroup it renders 1`] = `
<Component>
<UtilityBarGroup>
<UtilityBarText>
Test text
</UtilityBarText>
</UtilityBarGroup>
</Component>
`;

View file

@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UtilityBarSection it renders 1`] = `
<Component>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>
Test text
</UtilityBarText>
</UtilityBarGroup>
</UtilityBarSection>
</Component>
`;

View file

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UtilityBarText it renders 1`] = `
<Component>
<UtilityBarText>
Test text
</UtilityBarText>
</Component>
`;

View file

@ -0,0 +1,11 @@
/*
* 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.
*/
export { UtilityBar } from './utility_bar';
export { UtilityBarAction } from './utility_bar_action';
export { UtilityBarGroup } from './utility_bar_group';
export { UtilityBarSection } from './utility_bar_section';
export { UtilityBarText } from './utility_bar_text';

View file

@ -0,0 +1,118 @@
/*
* 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 styled, { css } from 'styled-components';
/**
* UTILITY BAR
*/
export interface BarProps {
border?: boolean;
}
export const Bar = styled.aside.attrs({
className: 'siemUtilityBar',
})<BarProps>`
${({ border, theme }) => css`
${border &&
css`
border-bottom: ${theme.eui.euiBorderThin};
padding-bottom: ${theme.eui.paddingSizes.s};
`}
@media only screen and (min-width: ${theme.eui.euiBreakpoints.l}) {
display: flex;
justify-content: space-between;
}
`}
`;
Bar.displayName = 'Bar';
export const BarSection = styled.div.attrs({
className: 'siemUtilityBar__section',
})`
${({ theme }) => css`
& + & {
margin-top: ${theme.eui.euiSizeS};
}
@media only screen and (min-width: ${theme.eui.euiBreakpoints.m}) {
display: flex;
flex-wrap: wrap;
}
@media only screen and (min-width: ${theme.eui.euiBreakpoints.l}) {
& + & {
margin-top: 0;
margin-left: ${theme.eui.euiSize};
}
}
`}
`;
BarSection.displayName = 'BarSection';
export const BarGroup = styled.div.attrs({
className: 'siemUtilityBar__group',
})`
${({ theme }) => css`
align-items: flex-start;
display: flex;
flex-wrap: wrap;
& + & {
margin-top: ${theme.eui.euiSizeS};
}
@media only screen and (min-width: ${theme.eui.euiBreakpoints.m}) {
border-right: ${theme.eui.euiBorderThin};
flex-wrap: nowrap;
margin-right: ${theme.eui.paddingSizes.m};
padding-right: ${theme.eui.paddingSizes.m};
& + & {
margin-top: 0;
}
&:last-child {
border-right: none;
margin-right: 0;
padding-right: 0;
}
}
& > * {
margin-right: ${theme.eui.euiSize};
&:last-child {
margin-right: 0;
}
}
`}
`;
BarGroup.displayName = 'BarGroup';
export const BarText = styled.p.attrs({
className: 'siemUtilityBar__text',
})`
${({ theme }) => css`
color: ${theme.eui.textColors.subdued};
font-size: ${theme.eui.euiFontSizeXS};
line-height: ${theme.eui.euiLineHeight};
white-space: nowrap;
`}
`;
BarText.displayName = 'BarText';
export const BarAction = styled.div.attrs({
className: 'siemUtilityBar__action',
})`
${({ theme }) => css`
font-size: ${theme.eui.euiFontSizeXS};
line-height: ${theme.eui.euiLineHeight};
`}
`;
BarAction.displayName = 'BarAction';

View file

@ -0,0 +1,113 @@
/*
* 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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { mount, shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import 'jest-styled-components';
import React from 'react';
import '../../../mock/ui_settings';
import { TestProviders } from '../../../mock';
import {
UtilityBar,
UtilityBarAction,
UtilityBarGroup,
UtilityBarSection,
UtilityBarText,
} from './index';
jest.mock('../../../lib/settings/use_kibana_ui_setting');
describe('UtilityBar', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<UtilityBar>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{'Test text'}</UtilityBarText>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarAction iconType="" popoverContent={<p>{'Test popover'}</p>}>
{'Test action'}
</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarAction iconType="cross">{'Test action'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('it applies border styles when border is true', () => {
const wrapper = mount(
<TestProviders>
<UtilityBar border>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{'Test text'}</UtilityBarText>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarAction iconType="" popoverContent={<p>{'Test popover'}</p>}>
{'Test action'}
</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarAction iconType="cross">{'Test action'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
</TestProviders>
);
const siemUtilityBar = wrapper.find('.siemUtilityBar').first();
expect(siemUtilityBar).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin);
expect(siemUtilityBar).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.s);
});
test('it DOES NOT apply border styles when border is false', () => {
const wrapper = mount(
<TestProviders>
<UtilityBar>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{'Test text'}</UtilityBarText>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarAction iconType="" popoverContent={<p>{'Test popover'}</p>}>
{'Test action'}
</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarAction iconType="cross">{'Test action'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
</TestProviders>
);
const siemUtilityBar = wrapper.find('.siemUtilityBar').first();
expect(siemUtilityBar).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin);
expect(siemUtilityBar).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.s);
});
});

View file

@ -0,0 +1,18 @@
/*
* 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 React from 'react';
import { Bar, BarProps } from './styles';
export interface UtilityBarProps extends BarProps {
children: React.ReactNode;
}
export const UtilityBar = React.memo<UtilityBarProps>(({ border, children }) => (
<Bar border={border}>{children}</Bar>
));
UtilityBar.displayName = 'UtilityBar';

View file

@ -0,0 +1,45 @@
/*
* 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, shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import 'jest-styled-components';
import React from 'react';
import '../../../mock/ui_settings';
import { TestProviders } from '../../../mock';
import { UtilityBarAction } from './index';
jest.mock('../../../lib/settings/use_kibana_ui_setting');
describe('UtilityBarAction', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<UtilityBarAction iconType="alert">{'Test action'}</UtilityBarAction>
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('it renders a popover', () => {
const wrapper = mount(
<TestProviders>
<UtilityBarAction iconType="alert" popoverContent={<p>{'Test popover'}</p>}>
{'Test action'}
</UtilityBarAction>
</TestProviders>
);
expect(
wrapper
.find('.euiPopover')
.first()
.exists()
).toBe(true);
});
});

View file

@ -0,0 +1,72 @@
/*
* 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 { EuiPopover } from '@elastic/eui';
import React, { useState } from 'react';
import { LinkIcon, LinkIconProps } from '../../link_icon';
import { BarAction } from './styles';
const Popover = React.memo<UtilityBarActionProps>(
({ children, color, iconSide, iconSize, iconType, popoverContent }) => {
const [popoverState, setPopoverState] = useState(false);
return (
<EuiPopover
button={
<LinkIcon
color={color}
iconSide={iconSide}
iconSize={iconSize}
iconType={iconType}
onClick={() => setPopoverState(!popoverState)}
>
{children}
</LinkIcon>
}
closePopover={() => setPopoverState(false)}
isOpen={popoverState}
>
{popoverContent}
</EuiPopover>
);
}
);
Popover.displayName = 'Popover';
export interface UtilityBarActionProps extends LinkIconProps {
popoverContent?: React.ReactNode;
}
export const UtilityBarAction = React.memo<UtilityBarActionProps>(
({ children, color, href, iconSide, iconSize, iconType, onClick, popoverContent }) => (
<BarAction>
{popoverContent ? (
<Popover
color={color}
iconSide={iconSide}
iconSize={iconSize}
iconType={iconType}
popoverContent={popoverContent}
>
{children}
</Popover>
) : (
<LinkIcon
color={color}
href={href}
iconSide={iconSide}
iconSize={iconSize}
iconType={iconType}
onClick={onClick}
>
{children}
</LinkIcon>
)}
</BarAction>
)
);
UtilityBarAction.displayName = 'UtilityBarAction';

View file

@ -0,0 +1,29 @@
/*
* 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 { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import React from 'react';
import '../../../mock/ui_settings';
import { TestProviders } from '../../../mock';
import { UtilityBarGroup, UtilityBarText } from './index';
jest.mock('../../../lib/settings/use_kibana_ui_setting');
describe('UtilityBarGroup', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<UtilityBarGroup>
<UtilityBarText>{'Test text'}</UtilityBarText>
</UtilityBarGroup>
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
});

View file

@ -0,0 +1,18 @@
/*
* 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 React from 'react';
import { BarGroup } from './styles';
export interface UtilityBarGroupProps {
children: React.ReactNode;
}
export const UtilityBarGroup = React.memo<UtilityBarGroupProps>(({ children }) => (
<BarGroup>{children}</BarGroup>
));
UtilityBarGroup.displayName = 'UtilityBarGroup';

View file

@ -0,0 +1,31 @@
/*
* 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 { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import React from 'react';
import '../../../mock/ui_settings';
import { TestProviders } from '../../../mock';
import { UtilityBarGroup, UtilityBarSection, UtilityBarText } from './index';
jest.mock('../../../lib/settings/use_kibana_ui_setting');
describe('UtilityBarSection', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{'Test text'}</UtilityBarText>
</UtilityBarGroup>
</UtilityBarSection>
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
});

View file

@ -0,0 +1,18 @@
/*
* 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 React from 'react';
import { BarSection } from './styles';
export interface UtilityBarSectionProps {
children: React.ReactNode;
}
export const UtilityBarSection = React.memo<UtilityBarSectionProps>(({ children }) => (
<BarSection>{children}</BarSection>
));
UtilityBarSection.displayName = 'UtilityBarSection';

View file

@ -0,0 +1,27 @@
/*
* 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 { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import React from 'react';
import '../../../mock/ui_settings';
import { TestProviders } from '../../../mock';
import { UtilityBarText } from './index';
jest.mock('../../../lib/settings/use_kibana_ui_setting');
describe('UtilityBarText', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<UtilityBarText>{'Test text'}</UtilityBarText>
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
});

View file

@ -0,0 +1,18 @@
/*
* 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 React from 'react';
import { BarText } from './styles';
export interface UtilityBarTextProps {
children: string;
}
export const UtilityBarText = React.memo<UtilityBarTextProps>(({ children }) => (
<BarText>{children}</BarText>
));
UtilityBarText.displayName = 'UtilityBarText';

View file

@ -52,7 +52,7 @@ describe('EventsViewer', () => {
expect(
wrapper
.find(`[data-test-subj="header-panel-subtitle"]`)
.find(`[data-test-subj="header-section-subtitle"]`)
.first()
.text()
).toEqual('Showing: 12 events');

View file

@ -16,7 +16,7 @@ import { Direction } from '../../graphql/types';
import { useKibanaCore } from '../../lib/compose/kibana_core';
import { KqlMode } from '../../store/timeline/model';
import { AutoSizer } from '../auto_sizer';
import { HeaderPanel } from '../header_panel';
import { HeaderSection } from '../header_section';
import { ColumnHeader } from '../timeline/body/column_headers/column_header';
import { defaultHeaders } from '../timeline/body/column_headers/default_headers';
import { Sort } from '../timeline/body/sort';
@ -130,7 +130,7 @@ export const EventsViewer = React.memo<Props>(
totalCount = 0,
}) => (
<>
<HeaderPanel
<HeaderSection
id={id}
showInspect={showInspect}
subtitle={`${i18n.SHOWING}: ${totalCount.toLocaleString()} ${i18n.UNIT(

View file

@ -10,23 +10,22 @@ import { Sticky } from 'react-sticky';
import { pure } from 'recompose';
import styled, { css } from 'styled-components';
import { gutterTimeline } from '../../lib/helpers';
const offsetChrome = 49;
const gutterTimeline = '70px'; // Temporary until timeline is moved - MichaelMarcialis
const disableSticky = 'screen and (max-width: ' + euiLightVars.euiBreakpoints.s + ')';
const disableStickyMq = window.matchMedia(disableSticky);
const Aside = styled.aside<{ isSticky?: boolean }>`
const Wrapper = styled.aside<{ isSticky?: boolean }>`
${props => css`
position: relative;
z-index: ${props.theme.eui.euiZNavigation};
background: ${props.theme.eui.euiColorEmptyShade};
border-bottom: ${props.theme.eui.euiBorderThin};
box-sizing: content-box;
margin: 0 -${gutterTimeline} 0 -${props.theme.eui.euiSizeL};
padding: ${props.theme.eui.euiSize} ${gutterTimeline} ${props.theme.eui.euiSize} ${
props.theme.eui.euiSizeL
};
padding: ${props.theme.eui.paddingSizes.m} ${gutterTimeline} ${
props.theme.eui.paddingSizes.m
} ${props.theme.eui.paddingSizes.l};
${props.isSticky &&
`
@ -39,8 +38,7 @@ const Aside = styled.aside<{ isSticky?: boolean }>`
}
`}
`;
Aside.displayName = 'Aside';
Wrapper.displayName = 'Wrapper';
export interface FiltersGlobalProps {
children: React.ReactNode;
@ -49,11 +47,10 @@ export interface FiltersGlobalProps {
export const FiltersGlobal = pure<FiltersGlobalProps>(({ children }) => (
<Sticky disableCompensation={disableStickyMq.matches} topOffset={-offsetChrome}>
{({ style, isSticky }) => (
<Aside isSticky={isSticky} style={style}>
<Wrapper className="siemFiltersGlobal" isSticky={isSticky} style={style}>
{children}
</Aside>
</Wrapper>
)}
</Sticky>
));
FiltersGlobal.displayName = 'FiltersGlobal';

View file

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HeaderGlobal it renders 1`] = `
<Component>
<HeaderGlobal />
</Component>
`;

View file

@ -0,0 +1,34 @@
/*
* 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 { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import React from 'react';
import { TestProviders } from '../../mock';
import '../../mock/match_media';
import '../../mock/ui_settings';
import { HeaderGlobal } from './index';
jest.mock('../../lib/settings/use_kibana_ui_setting');
// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar
jest.mock('../search_bar', () => ({
SiemSearchBar: () => null,
}));
describe('HeaderGlobal', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<HeaderGlobal />
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
});

View file

@ -0,0 +1,82 @@
/*
* 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui';
import { pickBy } from 'lodash/fp';
import React from 'react';
import styled, { css } from 'styled-components';
import { gutterTimeline } from '../../lib/helpers';
import { navTabs } from '../../pages/home/home_navigations';
import { SiemPageName } from '../../pages/home/types';
import { getOverviewUrl } from '../link_to';
import { MlPopover } from '../ml_popover/ml_popover';
import { SiemNavigation } from '../navigation';
import * as i18n from './translations';
const Wrapper = styled.header`
${({ theme }) => css`
background: ${theme.eui.euiColorEmptyShade};
border-bottom: ${theme.eui.euiBorderThin};
padding: ${theme.eui.paddingSizes.m} ${gutterTimeline} ${theme.eui.paddingSizes.m}
${theme.eui.paddingSizes.l};
`}
`;
Wrapper.displayName = 'Wrapper';
const FlexItem = styled(EuiFlexItem)`
min-width: 0;
`;
FlexItem.displayName = 'FlexItem';
interface HeaderGlobalProps {
hideDetectionEngine?: boolean;
}
export const HeaderGlobal = React.memo<HeaderGlobalProps>(({ hideDetectionEngine = true }) => (
<Wrapper className="siemHeaderGlobal">
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" wrap>
<FlexItem>
<EuiFlexGroup alignItems="center" responsive={false}>
<FlexItem grow={false}>
<EuiLink href={getOverviewUrl()}>
<EuiIcon aria-label={i18n.SIEM} type="securityAnalyticsApp" size="l" />
</EuiLink>
</FlexItem>
<FlexItem component="nav">
<SiemNavigation
display="condensed"
navTabs={
hideDetectionEngine
? pickBy((value, key) => key !== SiemPageName.detectionEngine, navTabs)
: navTabs
}
/>
</FlexItem>
</EuiFlexGroup>
</FlexItem>
<FlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap>
<FlexItem grow={false}>
<MlPopover />
</FlexItem>
<FlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="add-data"
href="kibana#home/tutorial_directory/siem"
iconType="plusInCircle"
>
{i18n.BUTTON_ADD_DATA}
</EuiButtonEmpty>
</FlexItem>
</EuiFlexGroup>
</FlexItem>
</EuiFlexGroup>
</Wrapper>
));
HeaderGlobal.displayName = 'HeaderGlobal';

View file

@ -0,0 +1,15 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const SIEM = i18n.translate('xpack.siem.headerGlobal.siem', {
defaultMessage: 'SIEM',
});
export const BUTTON_ADD_DATA = i18n.translate('xpack.siem.headerGlobal.buttonAddData', {
defaultMessage: 'Add data',
});

View file

@ -1,14 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rendering renders correctly 1`] = `
<Component
badgeLabel="Beta"
badgeTooltip="My test tooltip."
subtitle="My Test Subtitle"
title="My Test Title"
>
<p>
My test supplement.
</p>
</Component>
`;

View file

@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HeaderPage it renders 1`] = `
<Component>
<HeaderPage
badgeOptions={
Object {
"beta": true,
"text": "Beta",
"tooltip": "Test tooltip",
}
}
border={true}
subtitle="Test subtitle"
subtitle2="Test subtitle 2"
title="Test title"
>
<p>
Test supplement
</p>
</HeaderPage>
</Component>
`;

View file

@ -1,42 +0,0 @@
/*
* 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 { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import React from 'react';
import { HeaderPage } from './index';
describe('rendering', () => {
test('renders correctly', () => {
const wrapper = shallow(
<HeaderPage
badgeLabel="Beta"
badgeTooltip="My test tooltip."
subtitle="My Test Subtitle"
title="My Test Title"
>
<p>{'My test supplement.'}</p>
</HeaderPage>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('renders as a draggable when provided arguments', () => {
const wrapper = shallow(
<HeaderPage
badgeLabel="Beta"
badgeTooltip="My test tooltip."
subtitle="My Test Subtitle"
title="My Test Title"
draggableArguments={{ field: 'neat', value: 'cool' }}
>
<p>{'My test supplement.'}</p>
</HeaderPage>
);
const draggableHeader = wrapper.dive().find('[data-test-subj="page_headline_draggable"]');
expect(draggableHeader.exists()).toBeTruthy();
});
});

View file

@ -1,78 +0,0 @@
/*
* 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 { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
import { DefaultDraggable } from '../draggables';
const Header = styled.header`
${({ theme }) => `
border-bottom: ${theme.eui.euiBorderThin};
padding-bottom: ${theme.eui.euiSizeL};
margin: ${theme.eui.euiSizeL} 0;
`}
`;
Header.displayName = 'Header';
interface DraggableArguments {
field: string;
value: string;
}
export interface HeaderPageProps {
badgeLabel?: string;
badgeTooltip?: string;
children?: React.ReactNode;
draggableArguments?: DraggableArguments;
subtitle?: string | React.ReactNode;
title: string | React.ReactNode;
}
export const HeaderPage = pure<HeaderPageProps>(
({ badgeLabel, badgeTooltip, children, draggableArguments, subtitle, title, ...rest }) => (
<Header {...rest}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle size="l">
<h1 data-test-subj="page_headline_title">
{!draggableArguments ? (
title
) : (
<DefaultDraggable
data-test-subj="page_headline_draggable"
id={`header-page-draggable-${draggableArguments.field}-${draggableArguments.value}`}
field={draggableArguments.field}
value={`${draggableArguments.value}`}
/>
)}
{badgeLabel && (
<>
{' '}
<EuiBetaBadge
label={badgeLabel}
tooltipContent={badgeTooltip}
tooltipPosition="bottom"
/>
</>
)}
</h1>
</EuiTitle>
<EuiText color="subdued" size="xs">
{subtitle}
</EuiText>
</EuiFlexItem>
{children && <EuiFlexItem grow={false}>{children}</EuiFlexItem>}
</EuiFlexGroup>
</Header>
)
);
HeaderPage.displayName = 'HeaderPage';

View file

@ -0,0 +1,228 @@
/*
* 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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { mount, shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import 'jest-styled-components';
import React from 'react';
import { TestProviders } from '../../mock';
import '../../mock/ui_settings';
import { HeaderPage } from './index';
jest.mock('../../lib/settings/use_kibana_ui_setting');
describe('HeaderPage', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<HeaderPage
badgeOptions={{ beta: true, text: 'Beta', tooltip: 'Test tooltip' }}
border
subtitle="Test subtitle"
subtitle2="Test subtitle 2"
title="Test title"
>
<p>{'Test supplement'}</p>
</HeaderPage>
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('it renders the title', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage title="Test title" />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-page-title"]')
.first()
.exists()
).toBe(true);
});
test('it renders the back link when provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage backOptions={{ href: '#', text: 'Test link' }} title="Test title" />
</TestProviders>
);
expect(
wrapper
.find('.siemHeaderPage__linkBack')
.first()
.exists()
).toBe(true);
});
test('it DOES NOT render the back link when not provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage title="Test title" />
</TestProviders>
);
expect(
wrapper
.find('.siemHeaderPage__linkBack')
.first()
.exists()
).toBe(false);
});
test('it renders the first subtitle when provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage subtitle="Test subtitle" title="Test title" />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-page-subtitle"]')
.first()
.exists()
).toBe(true);
});
test('it DOES NOT render the first subtitle when not provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage title="Test title" />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-section-subtitle"]')
.first()
.exists()
).toBe(false);
});
test('it renders the second subtitle when provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage subtitle2="Test subtitle 2" title="Test title" />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-page-subtitle-2"]')
.first()
.exists()
).toBe(true);
});
test('it DOES NOT render the second subtitle when not provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage title="Test title" />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-section-subtitle-2"]')
.first()
.exists()
).toBe(false);
});
test('it renders supplements when children provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage title="Test title">
<p>{'Test supplement'}</p>
</HeaderPage>
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-page-supplements"]')
.first()
.exists()
).toBe(true);
});
test('it DOES NOT render supplements when children not provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage title="Test title" />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-page-supplements"]')
.first()
.exists()
).toBe(false);
});
test('it applies border styles when border is true', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage border title="Test title" />
</TestProviders>
);
const siemHeaderPage = wrapper.find('.siemHeaderPage').first();
expect(siemHeaderPage).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin);
expect(siemHeaderPage).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l);
});
test('it DOES NOT apply border styles when border is false', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage title="Test title" />
</TestProviders>
);
const siemHeaderPage = wrapper.find('.siemHeaderPage').first();
expect(siemHeaderPage).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin);
expect(siemHeaderPage).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l);
});
test('it renders as a draggable when arguments provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage draggableArguments={{ field: 'neat', value: 'cool' }} title="Test title" />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-page-draggable"]')
.first()
.exists()
).toBe(true);
});
test('it DOES NOT render as a draggable when arguments not provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPage title="Test title" />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-page-draggable"]')
.first()
.exists()
).toBe(false);
});
});

View file

@ -4,4 +4,143 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { HeaderPage } from './header_page';
import { EuiBetaBadge, EuiBadge, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import React from 'react';
import styled, { css } from 'styled-components';
import { DefaultDraggable } from '../draggables';
import { LinkIcon, LinkIconProps } from '../link_icon';
import { Subtitle, SubtitleProps } from '../subtitle';
interface HeaderProps {
border?: boolean;
}
const Header = styled.header.attrs({
className: 'siemHeaderPage',
})<HeaderProps>`
${({ border, theme }) => css`
margin-bottom: ${theme.eui.euiSizeL};
${border &&
css`
border-bottom: ${theme.eui.euiBorderThin};
padding-bottom: ${theme.eui.paddingSizes.l};
`}
`}
`;
Header.displayName = 'Header';
const FlexItem = styled(EuiFlexItem)`
display: block;
`;
FlexItem.displayName = 'FlexItem';
const LinkBack = styled.div.attrs({
className: 'siemHeaderPage__linkBack',
})`
${({ theme }) => css`
font-size: ${theme.eui.euiFontSizeXS};
line-height: ${theme.eui.euiLineHeight};
margin-bottom: ${theme.eui.euiSizeS};
`}
`;
LinkBack.displayName = 'LinkBack';
const Badge = styled(EuiBadge)`
letter-spacing: 0;
`;
Badge.displayName = 'Badge';
interface BackOptions {
href: LinkIconProps['href'];
text: LinkIconProps['children'];
}
interface BadgeOptions {
beta?: boolean;
text: string;
tooltip?: string;
}
interface DraggableArguments {
field: string;
value: string;
}
export interface HeaderPageProps extends HeaderProps {
backOptions?: BackOptions;
badgeOptions?: BadgeOptions;
children?: React.ReactNode;
draggableArguments?: DraggableArguments;
subtitle?: SubtitleProps['items'];
subtitle2?: SubtitleProps['items'];
title: string | React.ReactNode;
}
export const HeaderPage = React.memo<HeaderPageProps>(
({
backOptions,
badgeOptions,
border,
children,
draggableArguments,
subtitle,
subtitle2,
title,
...rest
}) => (
<Header border={border} {...rest}>
<EuiFlexGroup alignItems="center">
<FlexItem>
{backOptions && (
<LinkBack>
<LinkIcon href={backOptions.href} iconType="arrowLeft">
{backOptions.text}
</LinkIcon>
</LinkBack>
)}
<EuiTitle size="l">
<h1 data-test-subj="header-page-title">
{!draggableArguments ? (
title
) : (
<DefaultDraggable
data-test-subj="header-page-draggable"
id={`header-page-draggable-${draggableArguments.field}-${draggableArguments.value}`}
field={draggableArguments.field}
value={`${draggableArguments.value}`}
/>
)}
{badgeOptions && (
<>
{' '}
{badgeOptions.beta ? (
<EuiBetaBadge
label={badgeOptions.text}
tooltipContent={badgeOptions.tooltip}
tooltipPosition="bottom"
/>
) : (
<Badge color="hollow">{badgeOptions.text}</Badge>
)}
</>
)}
</h1>
</EuiTitle>
{subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />}
{subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />}
</FlexItem>
{children && (
<FlexItem data-test-subj="header-page-supplements" grow={false}>
{children}
</FlexItem>
)}
</EuiFlexGroup>
</Header>
)
);
HeaderPage.displayName = 'HeaderPage';

View file

@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HeaderPanel it renders 1`] = `
exports[`HeaderSection it renders 1`] = `
<Component>
<HeaderPanel
<HeaderSection
title="Test title"
/>
</Component>

View file

@ -10,17 +10,17 @@ import toJson from 'enzyme-to-json';
import 'jest-styled-components';
import React from 'react';
import '../../mock/ui_settings';
import { TestProviders } from '../../mock';
import { HeaderPanel } from './index';
import '../../mock/ui_settings';
import { HeaderSection } from './index';
jest.mock('../../lib/settings/use_kibana_ui_setting');
describe('HeaderPanel', () => {
describe('HeaderSection', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<HeaderPanel title="Test title" />
<HeaderSection title="Test title" />
</TestProviders>
);
@ -30,13 +30,13 @@ describe('HeaderPanel', () => {
test('it renders the title', () => {
const wrapper = mount(
<TestProviders>
<HeaderPanel title="Test title" />
<HeaderSection title="Test title" />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-panel-title"]')
.find('[data-test-subj="header-section-title"]')
.first()
.exists()
).toBe(true);
@ -45,13 +45,13 @@ describe('HeaderPanel', () => {
test('it renders the subtitle when provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPanel subtitle="Test subtitle" title="Test title" />
<HeaderSection subtitle="Test subtitle" title="Test title" />
</TestProviders>
);
expect(
wrapper
.find(`[data-test-subj="header-panel-subtitle"]`)
.find('[data-test-subj="header-section-subtitle"]')
.first()
.exists()
).toBe(true);
@ -60,13 +60,13 @@ describe('HeaderPanel', () => {
test('it DOES NOT render the subtitle when not provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPanel title="Test title" />
<HeaderSection title="Test title" />
</TestProviders>
);
expect(
wrapper
.find(`[data-test-subj="header-panel-subtitle"]`)
.find('[data-test-subj="header-section-subtitle"]')
.first()
.exists()
).toBe(false);
@ -75,13 +75,13 @@ describe('HeaderPanel', () => {
test('it renders a transparent inspect button when showInspect is false', () => {
const wrapper = mount(
<TestProviders>
<HeaderPanel title="Test title" id="test" showInspect={false} />
<HeaderSection title="Test title" id="test" showInspect={false} />
</TestProviders>
);
expect(
wrapper
.find(`[data-test-subj="transparent-inspect-container"]`)
.find('[data-test-subj="transparent-inspect-container"]')
.first()
.exists()
).toBe(true);
@ -90,13 +90,13 @@ describe('HeaderPanel', () => {
test('it renders an opaque inspect button when showInspect is true', () => {
const wrapper = mount(
<TestProviders>
<HeaderPanel title="Test title" id="test" showInspect={true} />
<HeaderSection title="Test title" id="test" showInspect={true} />
</TestProviders>
);
expect(
wrapper
.find(`[data-test-subj="opaque-inspect-container"]`)
.find('[data-test-subj="opaque-inspect-container"]')
.first()
.exists()
).toBe(true);
@ -105,15 +105,15 @@ describe('HeaderPanel', () => {
test('it renders supplements when children provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPanel title="Test title">
<HeaderSection title="Test title">
<p>{'Test children'}</p>
</HeaderPanel>
</HeaderSection>
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-panel-supplements"]')
.find('[data-test-subj="header-section-supplements"]')
.first()
.exists()
).toBe(true);
@ -122,13 +122,13 @@ describe('HeaderPanel', () => {
test('it DOES NOT render supplements when children not provided', () => {
const wrapper = mount(
<TestProviders>
<HeaderPanel title="Test title" />
<HeaderSection title="Test title" />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="header-panel-supplements"]')
.find('[data-test-subj="header-section-supplements"]')
.first()
.exists()
).toBe(false);
@ -137,24 +137,58 @@ describe('HeaderPanel', () => {
test('it applies border styles when border is true', () => {
const wrapper = mount(
<TestProviders>
<HeaderPanel border title="Test title" />
<HeaderSection border title="Test title" />
</TestProviders>
);
const siemHeaderPanel = wrapper.find('.siemHeaderPanel').first();
const siemHeaderSection = wrapper.find('.siemHeaderSection').first();
expect(siemHeaderPanel).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin);
expect(siemHeaderPanel).toHaveStyleRule('padding-bottom', euiDarkVars.euiSizeL);
expect(siemHeaderSection).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin);
expect(siemHeaderSection).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l);
});
test('it DOES NOT apply border styles when border is false', () => {
const wrapper = mount(
<TestProviders>
<HeaderPanel title="Test title" />
<HeaderSection title="Test title" />
</TestProviders>
);
const siemHeaderPanel = wrapper.find('.siemHeaderPanel').first();
const siemHeaderSection = wrapper.find('.siemHeaderSection').first();
expect(siemHeaderPanel).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin);
expect(siemHeaderPanel).not.toHaveStyleRule('padding-bottom', euiDarkVars.euiSizeL);
expect(siemHeaderSection).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin);
expect(siemHeaderSection).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l);
});
test('it splits the title and supplement areas evenly when split is true', () => {
const wrapper = mount(
<TestProviders>
<HeaderSection split title="Test title">
<p>{'Test children'}</p>
</HeaderSection>
</TestProviders>
);
expect(
wrapper
.find('.euiFlexItem--flexGrowZero[data-test-subj="header-section-supplements"]')
.first()
.exists()
).toBe(false);
});
test('it DOES NOT split the title and supplement areas evenly when split is false', () => {
const wrapper = mount(
<TestProviders>
<HeaderSection title="Test title">
<p>{'Test children'}</p>
</HeaderSection>
</TestProviders>
);
expect(
wrapper
.find('.euiFlexItem--flexGrowZero[data-test-subj="header-section-supplements"]')
.first()
.exists()
).toBe(true);
});
});

View file

@ -4,51 +4,52 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiText, EuiTitle } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle } from '@elastic/eui';
import React from 'react';
import styled, { css } from 'styled-components';
import { InspectButton } from '../inspect';
import { Subtitle } from '../subtitle';
interface HeaderProps {
border?: boolean;
}
const Header = styled.header.attrs({
className: 'siemHeaderPanel',
className: 'siemHeaderSection',
})<HeaderProps>`
${props => css`
margin-bottom: ${props.theme.eui.euiSizeL};
${({ border, theme }) => css`
margin-bottom: ${theme.eui.euiSizeL};
user-select: text;
${props.border &&
`
border-bottom: ${props.theme.eui.euiBorderThin};
padding-bottom: ${props.theme.eui.euiSizeL};
`}
${border &&
css`
border-bottom: ${theme.eui.euiBorderThin};
padding-bottom: ${theme.eui.paddingSizes.l};
`}
`}
`;
Header.displayName = 'Header';
export interface HeaderPanelProps extends HeaderProps {
export interface HeaderSectionProps extends HeaderProps {
children?: React.ReactNode;
id?: string;
split?: boolean;
subtitle?: string | React.ReactNode;
showInspect?: boolean;
title: string | React.ReactNode;
tooltip?: string;
}
export const HeaderPanel = React.memo<HeaderPanelProps>(
({ border, children, id, showInspect = false, subtitle, title, tooltip }) => (
export const HeaderSection = React.memo<HeaderSectionProps>(
({ border, children, id, showInspect = false, split, subtitle, title, tooltip }) => (
<Header border={border}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiFlexGroup alignItems="center" responsive={false}>
<EuiFlexItem>
<EuiTitle>
<h2 data-test-subj="header-panel-title">
<h2 data-test-subj="header-section-title">
{title}
{tooltip && (
<>
@ -59,11 +60,7 @@ export const HeaderPanel = React.memo<HeaderPanelProps>(
</h2>
</EuiTitle>
{subtitle && (
<EuiText color="subdued" data-test-subj="header-panel-subtitle" size="xs">
<p>{subtitle}</p>
</EuiText>
)}
{subtitle && <Subtitle data-test-subj="header-section-subtitle" items={subtitle} />}
</EuiFlexItem>
{id && (
@ -75,7 +72,7 @@ export const HeaderPanel = React.memo<HeaderPanelProps>(
</EuiFlexItem>
{children && (
<EuiFlexItem data-test-subj="header-panel-supplements" grow={false}>
<EuiFlexItem data-test-subj="header-section-supplements" grow={split ? true : false}>
{children}
</EuiFlexItem>
)}
@ -83,5 +80,4 @@ export const HeaderPanel = React.memo<HeaderPanelProps>(
</Header>
)
);
HeaderPanel.displayName = 'HeaderPanel';
HeaderSection.displayName = 'HeaderSection';

View file

@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LinkIcon it renders 1`] = `
<Component>
<LinkIcon
href="#"
iconSide="right"
iconSize="xxl"
iconType="alert"
>
Test link
</LinkIcon>
</Component>
`;

View file

@ -0,0 +1,121 @@
/*
* 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, shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import 'jest-styled-components';
import React from 'react';
import { TestProviders } from '../../mock';
import '../../mock/ui_settings';
import { LinkIcon } from './index';
jest.mock('../../lib/settings/use_kibana_ui_setting');
describe('LinkIcon', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<LinkIcon href="#" iconSide="right" iconSize="xxl" iconType="alert">
{'Test link'}
</LinkIcon>
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('it renders an action button when onClick is provided', () => {
const wrapper = mount(
<TestProviders>
<LinkIcon iconType="alert" onClick={() => alert('Test alert')}>
{'Test link'}
</LinkIcon>
</TestProviders>
);
expect(
wrapper
.find('button')
.first()
.exists()
).toBe(true);
});
test('it renders an action link when href is provided', () => {
const wrapper = mount(
<TestProviders>
<LinkIcon href="#" iconType="alert">
{'Test link'}
</LinkIcon>
</TestProviders>
);
expect(
wrapper
.find('a')
.first()
.exists()
).toBe(true);
});
test('it renders an icon', () => {
const wrapper = mount(
<TestProviders>
<LinkIcon iconType="alert">{'Test link'}</LinkIcon>
</TestProviders>
);
expect(
wrapper
.find('.euiIcon')
.first()
.exists()
).toBe(true);
});
test('it positions the icon to the right when iconSide is right', () => {
const wrapper = mount(
<TestProviders>
<LinkIcon iconSide="right" iconType="alert">
{'Test link'}
</LinkIcon>
</TestProviders>
);
expect(wrapper.find('.siemLinkIcon').first()).toHaveStyleRule('flex-direction', 'row-reverse');
});
test('it positions the icon to the left when iconSide is left (or not provided)', () => {
const wrapper = mount(
<TestProviders>
<LinkIcon iconSide="left" iconType="alert">
{'Test link'}
</LinkIcon>
</TestProviders>
);
expect(wrapper.find('.siemLinkIcon').first()).not.toHaveStyleRule(
'flex-direction',
'row-reverse'
);
});
test('it renders a label', () => {
const wrapper = mount(
<TestProviders>
<LinkIcon iconType="alert">{'Test link'}</LinkIcon>
</TestProviders>
);
expect(
wrapper
.find('.siemLinkIcon__label')
.first()
.exists()
).toBe(true);
});
});

View file

@ -0,0 +1,61 @@
/*
* 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 { EuiIcon, EuiLink, IconSize, IconType } from '@elastic/eui';
import { LinkAnchorProps } from '@elastic/eui/src/components/link/link';
import React from 'react';
import styled, { css } from 'styled-components';
interface LinkProps {
color?: LinkAnchorProps['color'];
href?: string;
iconSide?: 'left' | 'right';
onClick?: Function;
}
const Link = styled(({ iconSide, children, ...rest }) => <EuiLink {...rest}>{children}</EuiLink>)<
LinkProps
>`
${({ iconSide, theme }) => css`
align-items: center;
display: inline-flex;
vertical-align: top;
white-space: nowrap;
${iconSide === 'left' &&
css`
.euiIcon {
margin-right: ${theme.eui.euiSizeXS};
}
`}
${iconSide === 'right' &&
css`
flex-direction: row-reverse;
.euiIcon {
margin-left: ${theme.eui.euiSizeXS};
}
`}
`}
`;
Link.displayName = 'Link';
export interface LinkIconProps extends LinkProps {
children: string;
iconSize?: IconSize;
iconType: IconType;
}
export const LinkIcon = React.memo<LinkIconProps>(
({ children, color, href, iconSide = 'left', iconSize = 's', iconType, onClick }) => (
<Link className="siemLinkIcon" color={color} href={href} iconSide={iconSide} onClick={onClick}>
<EuiIcon size={iconSize} type={iconType} />
<span className="siemLinkIcon__label">{children}</span>
</Link>
)
);
LinkIcon.displayName = 'LinkIcon';

View file

@ -5,6 +5,10 @@
*/
export { LinkToPage } from './link_to';
export {
getDetectionEngineUrl,
RedirectToDetectionEnginePage,
} from './redirect_to_detection_engine';
export { getOverviewUrl, RedirectToOverviewPage } from './redirect_to_overview';
export { getHostsUrl, getHostDetailsUrl } from './redirect_to_hosts';
export { getNetworkUrl, getIPDetailsUrl, RedirectToNetworkPage } from './redirect_to_network';

View file

@ -8,12 +8,19 @@ import React from 'react';
import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom';
import { pure } from 'recompose';
import { SiemPageName } from '../../pages/home/types';
import { HostsTableType } from '../../store/hosts/model';
import {
RedirectToCreateRulePage,
RedirectToDetectionEnginePage,
RedirectToEditRulePage,
RedirectToRuleDetailsPage,
RedirectToRulesPage,
} from './redirect_to_detection_engine';
import { RedirectToHostsPage, RedirectToHostDetailsPage } from './redirect_to_hosts';
import { RedirectToNetworkPage } from './redirect_to_network';
import { RedirectToOverviewPage } from './redirect_to_overview';
import { RedirectToTimelinesPage } from './redirect_to_timelines';
import { HostsTableType } from '../../store/hosts/model';
import { SiemPageName } from '../../pages/home/types';
interface LinkToPageProps {
match: RouteMatch<{}>;
@ -22,39 +29,62 @@ interface LinkToPageProps {
export const LinkToPage = pure<LinkToPageProps>(({ match }) => (
<Switch>
<Route
path={`${match.url}/:pageName(${SiemPageName.overview})`}
component={RedirectToOverviewPage}
path={`${match.url}/:pageName(${SiemPageName.overview})`}
/>
<Route
component={RedirectToHostsPage}
exact
path={`${match.url}/:pageName(${SiemPageName.hosts})`}
component={RedirectToHostsPage}
/>
<Route
component={RedirectToHostsPage}
path={`${match.url}/:pageName(${SiemPageName.hosts})/:tabName(${HostsTableType.hosts}|${HostsTableType.authentications}|${HostsTableType.uncommonProcesses}|${HostsTableType.anomalies}|${HostsTableType.events})`}
component={RedirectToHostsPage}
/>
<Route
component={RedirectToHostDetailsPage}
path={`${match.url}/:pageName(${SiemPageName.hosts})/:detailName/:tabName(${HostsTableType.authentications}|${HostsTableType.uncommonProcesses}|${HostsTableType.anomalies}|${HostsTableType.events})`}
component={RedirectToHostDetailsPage}
/>
<Route
component={RedirectToHostDetailsPage}
path={`${match.url}/:pageName(${SiemPageName.hosts})/:detailName`}
component={RedirectToHostDetailsPage}
/>
<Route
component={RedirectToNetworkPage}
exact
path={`${match.url}/:pageName(${SiemPageName.network})`}
component={RedirectToNetworkPage}
/>
<Route
component={RedirectToNetworkPage}
path={`${match.url}/:pageName(${SiemPageName.network})/ip/:detailName`}
component={RedirectToNetworkPage}
/>
<Route
path={`${match.url}/:pageName(${SiemPageName.timelines})`}
component={RedirectToDetectionEnginePage}
exact
path={`${match.url}/:pageName(${SiemPageName.detectionEngine})`}
strict
/>
<Route
component={RedirectToRulesPage}
exact
path={`${match.url}/:pageName(${SiemPageName.detectionEngine})/rules`}
/>
<Route
component={RedirectToCreateRulePage}
path={`${match.url}/:pageName(${SiemPageName.detectionEngine})/rules/create-rule`}
/>
<Route
component={RedirectToRuleDetailsPage}
exact
path={`${match.url}/:pageName(${SiemPageName.detectionEngine})/rules/rule-details`}
/>
<Route
component={RedirectToEditRulePage}
path={`${match.url}/:pageName(${SiemPageName.detectionEngine})/rules/rule-details/edit-rule`}
/>
<Route
component={RedirectToTimelinesPage}
path={`${match.url}/:pageName(${SiemPageName.timelines})`}
/>
<Redirect to="/" />
</Switch>

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { RedirectWrapper } from './redirect_wrapper';
export type DetectionEngineComponentProps = RouteComponentProps<{
search: string;
}>;
export const DETECTION_ENGINE_PAGE_NAME = 'detection-engine';
export const RedirectToDetectionEnginePage = ({
location: { search },
}: DetectionEngineComponentProps) => (
<RedirectWrapper to={`/${DETECTION_ENGINE_PAGE_NAME}${search}`} />
);
export const RedirectToRulesPage = ({ location: { search } }: DetectionEngineComponentProps) => {
return <RedirectWrapper to={`/${DETECTION_ENGINE_PAGE_NAME}/rules${search}`} />;
};
export const RedirectToCreateRulePage = ({
location: { search },
}: DetectionEngineComponentProps) => {
return <RedirectWrapper to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/create-rule${search}`} />;
};
export const RedirectToRuleDetailsPage = ({
location: { search },
}: DetectionEngineComponentProps) => {
return <RedirectWrapper to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/rule-details${search}`} />;
};
export const RedirectToEditRulePage = ({ location: { search } }: DetectionEngineComponentProps) => {
return (
<RedirectWrapper to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/rule-details/edit-rule${search}`} />
);
};
export const getDetectionEngineUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`;
export const getRulesUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules`;
export const getCreateRuleUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/create-rule`;
export const getRuleDetailsUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/rule-details`;
export const getEditRuleUrl = () =>
`#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/rule-details/edit-rule`;

View file

@ -26,9 +26,9 @@ jest.mock('../../lib/settings/use_kibana_ui_setting', () => {
return { useKibanaUiSetting: () => [false] };
});
jest.mock('../header_panel', () => {
jest.mock('../header_section', () => {
return {
HeaderPanel: () => <div className="headerPanel" />,
HeaderSection: () => <div className="headerSection" />,
};
});

View file

@ -12,7 +12,7 @@ import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import { EuiLoadingContent } from '@elastic/eui';
import { BarChart } from '../charts/barchart';
import { HeaderPanel } from '../header_panel';
import { HeaderSection } from '../header_section';
import { ChartSeriesData, UpdateDateRange } from '../charts/common';
import { MatrixOverTimeHistogramData } from '../../graphql/types';
import { DEFAULT_DARK_MODE } from '../../../common/constants';
@ -113,7 +113,7 @@ export const MatrixOverTimeHistogram = ({
onMouseEnter={() => setShowInspect(true)}
onMouseLeave={() => setShowInspect(false)}
>
<HeaderPanel
<HeaderSection
id={id}
title={title}
showInspect={!loadingInitial && showInspect}

View file

@ -7,7 +7,7 @@
import React, { useContext } from 'react';
import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data';
import { HeaderPanel } from '../../header_panel';
import { HeaderSection } from '../../header_section';
import * as i18n from './translations';
import { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_columns';
@ -62,7 +62,7 @@ export const AnomaliesHostTable = React.memo<AnomaliesHostTableProps>(
} else {
return (
<Panel loading={loading}>
<HeaderPanel
<HeaderSection
subtitle={`${i18n.SHOWING}: ${pagination.totalItemCount.toLocaleString()} ${i18n.UNIT(
pagination.totalItemCount
)}`}

View file

@ -6,7 +6,7 @@
import React, { useContext } from 'react';
import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data';
import { HeaderPanel } from '../../header_panel';
import { HeaderSection } from '../../header_section';
import * as i18n from './translations';
import { convertAnomaliesToNetwork } from './convert_anomalies_to_network';
@ -60,7 +60,7 @@ export const AnomaliesNetworkTable = React.memo<AnomaliesNetworkTableProps>(
} else {
return (
<Panel loading={loading}>
<HeaderPanel
<HeaderSection
subtitle={`${i18n.SHOWING}: ${pagination.totalItemCount.toLocaleString()} ${i18n.UNIT(
pagination.totalItemCount
)}`}

View file

@ -4,29 +4,30 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, EuiCallOut, EuiPopover, EuiPopoverTitle, EuiSpacer } from '@elastic/eui';
import { EuiButtonEmpty, EuiCallOut, EuiPopover, EuiPopoverTitle, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
import React, { useContext, useReducer, useState } from 'react';
import styled from 'styled-components';
import moment from 'moment';
import { FormattedMessage } from '@kbn/i18n/react';
import { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } from 'ui/documentation_links';
import * as i18n from './translations';
import { JobsFilters, JobSummary, SiemJob } from './types';
import { DEFAULT_KBN_VERSION } from '../../../common/constants';
import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting';
import { METRIC_TYPE, TELEMETRY_EVENT, trackUiAction as track } from '../../lib/track_usage';
import { errorToToaster } from '../ml/api/error_to_toaster';
import { hasMlAdminPermissions } from '../ml/permissions/has_ml_admin_permissions';
import { MlCapabilitiesContext } from '../ml/permissions/ml_capabilities_provider';
import { JobsTable } from './jobs_table/jobs_table';
import { useStateToaster } from '../toasters';
import { setupMlJob, startDatafeeds, stopDatafeeds } from './api';
import { UpgradeContents } from './upgrade_contents';
import { filterJobs } from './helpers';
import { useSiemJobs } from './hooks/use_siem_jobs';
import { JobsTableFilters } from './jobs_table/filters/jobs_table_filters';
import { JobsTable } from './jobs_table/jobs_table';
import { ShowingCount } from './jobs_table/showing_count';
import { PopoverDescription } from './popover_description';
import { useStateToaster } from '../toasters';
import { errorToToaster } from '../ml/api/error_to_toaster';
import { METRIC_TYPE, TELEMETRY_EVENT, trackUiAction as track } from '../../lib/track_usage';
import { useSiemJobs } from './hooks/use_siem_jobs';
import { filterJobs } from './helpers';
import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting';
import { DEFAULT_KBN_VERSION } from '../../../common/constants';
import * as i18n from './translations';
import { JobsFilters, JobSummary, SiemJob } from './types';
import { UpgradeContents } from './upgrade_contents';
const PopoverContentsDiv = styled.div`
max-width: 684px;
@ -161,14 +162,14 @@ export const MlPopover = React.memo(() => {
anchorPosition="downRight"
id="integrations-popover"
button={
<EuiButton
<EuiButtonEmpty
data-test-subj="integrations-button"
iconType="arrowDown"
iconSide="right"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
>
{i18n.ANOMALY_DETECTION}
</EuiButton>
</EuiButtonEmpty>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(!isPopoverOpen)}
@ -183,7 +184,7 @@ export const MlPopover = React.memo(() => {
anchorPosition="downRight"
id="integrations-popover"
button={
<EuiButton
<EuiButtonEmpty
data-test-subj="integrations-button"
iconType="arrowDown"
iconSide="right"
@ -193,7 +194,7 @@ export const MlPopover = React.memo(() => {
}}
>
{i18n.ANOMALY_DETECTION}
</EuiButton>
</EuiButtonEmpty>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(!isPopoverOpen)}

View file

@ -61,6 +61,13 @@ describe('SIEM Navigation', () => {
expect(setBreadcrumbs).toHaveBeenNthCalledWith(1, {
detailName: undefined,
navTabs: {
'detection-engine': {
disabled: false,
href: '#/link-to/detection-engine',
id: 'detection-engine',
name: 'Detection engine',
urlKey: 'detection-engine',
},
hosts: {
disabled: false,
href: '#/link-to/hosts',
@ -132,9 +139,17 @@ describe('SIEM Navigation', () => {
tabName: undefined,
});
wrapper.update();
expect(setBreadcrumbs).toHaveBeenNthCalledWith(2, {
expect(setBreadcrumbs).toHaveBeenNthCalledWith(1, {
detailName: undefined,
filters: [],
navTabs: {
'detection-engine': {
disabled: false,
href: '#/link-to/detection-engine',
id: 'detection-engine',
name: 'Detection engine',
urlKey: 'detection-engine',
},
hosts: {
disabled: false,
href: '#/link-to/hosts',
@ -164,17 +179,13 @@ describe('SIEM Navigation', () => {
urlKey: 'timeline',
},
},
pageName: 'network',
pathName: '/network',
search: '',
tabName: undefined,
query: { query: '', language: 'kuery' },
filters: [],
pageName: 'hosts',
pathName: '/hosts',
query: { language: 'kuery', query: '' },
savedQuery: undefined,
timeline: {
id: '',
isOpen: false,
},
search: '',
tabName: 'authentications',
timeline: { id: '', isOpen: false },
timerange: {
global: {
linkTo: ['timeline'],

View file

@ -6,17 +6,16 @@
import { isEqual } from 'lodash/fp';
import React, { useEffect } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { RouteSpyState } from '../../utils/route/types';
import { useRouteSpy } from '../../utils/route/use_route_spy';
import { makeMapStateToProps } from '../url_state/helpers';
import { setBreadcrumbs } from './breadcrumbs';
import { TabNavigation } from './tab_navigation';
import { TabNavigationProps } from './tab_navigation/types';
import { SiemNavigationComponentProps } from './types';
import { makeMapStateToProps } from '../url_state/helpers';
export const SiemNavigationComponent = React.memo<TabNavigationProps & RouteSpyState>(
({
@ -29,7 +28,6 @@ export const SiemNavigationComponent = React.memo<TabNavigationProps & RouteSpyS
pathName,
savedQuery,
search,
showBorder,
tabName,
timeline,
timerange,
@ -61,7 +59,6 @@ export const SiemNavigationComponent = React.memo<TabNavigationProps & RouteSpyS
pageName={pageName}
pathName={pathName}
savedQuery={savedQuery}
showBorder={showBorder}
tabName={tabName}
timeline={timeline}
timerange={timerange}

View file

@ -7,14 +7,14 @@
import { mount, shallow } from 'enzyme';
import * as React from 'react';
import { TabNavigation } from './';
import { TabNavigationProps } from './types';
import { navTabs } from '../../../pages/home/home_navigations';
import { SiemPageName } from '../../../pages/home/types';
import { HostsTableType } from '../../../store/hosts/model';
import { navTabsHostDetails } from '../../../pages/hosts/details/nav_tabs';
import { CONSTANTS } from '../../url_state/constants';
import { HostsTableType } from '../../../store/hosts/model';
import { RouteSpyState } from '../../../utils/route/types';
import { CONSTANTS } from '../../url_state/constants';
import { TabNavigation } from './';
import { TabNavigationProps } from './types';
describe('Tab Navigation', () => {
const pageName = SiemPageName.hosts;
@ -78,7 +78,7 @@ describe('Tab Navigation', () => {
});
test('it carries the url state in the link', () => {
const wrapper = shallow(<TabNavigation {...mockProps} />);
const firstTab = wrapper.find('[data-test-subj="navigation-link-network"]');
const firstTab = wrapper.find('[data-test-subj="navigation-network"]');
expect(firstTab.props().href).toBe(
"#/link-to/network?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))"
);
@ -147,7 +147,7 @@ describe('Tab Navigation', () => {
test('it carries the url state in the link', () => {
const wrapper = shallow(<TabNavigation {...mockProps} />);
const firstTab = wrapper.find(
`[data-test-subj="navigation-link-${HostsTableType.authentications}"]`
`[data-test-subj="navigation-${HostsTableType.authentications}"]`
);
expect(firstTab.props().href).toBe(
`#/${pageName}/${hostName}/${HostsTableType.authentications}?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`

View file

@ -3,40 +3,17 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiTab, EuiTabs, EuiLink } from '@elastic/eui';
import { EuiTab, EuiTabs } from '@elastic/eui';
import { getOr } from 'lodash/fp';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import classnames from 'classnames';
import { trackUiAction as track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../lib/track_usage';
import { getSearch } from '../helpers';
import { TabNavigationProps } from './types';
const TabContainer = styled.div`
.euiLink {
color: inherit !important;
&:focus {
outline: 0;
background: none;
}
.euiTab.euiTab-isSelected {
cursor: pointer;
}
}
&.showBorder {
padding: 8px 8px 0;
}
`;
TabContainer.displayName = 'TabContainer';
export const TabNavigation = React.memo<TabNavigationProps>(props => {
const { display = 'condensed', navTabs, pageName, showBorder, tabName } = props;
const { display, navTabs, pageName, tabName } = props;
const mapLocationToTab = (): string => {
return getOr(
'',
@ -44,6 +21,7 @@ export const TabNavigation = React.memo<TabNavigationProps>(props => {
Object.values(navTabs).find(item => tabName === item.id || pageName === item.id)
);
};
const [selectedTabId, setSelectedTabId] = useState(mapLocationToTab());
useEffect(() => {
const currentTabSelected = mapLocationToTab();
@ -57,31 +35,21 @@ export const TabNavigation = React.memo<TabNavigationProps>(props => {
const renderTabs = (): JSX.Element[] =>
Object.values(navTabs).map(tab => (
<TabContainer
className={classnames({ euiTab: true, showBorder })}
<EuiTab
data-href={tab.href}
data-test-subj={`navigation-${tab.id}`}
disabled={tab.disabled}
href={tab.href + getSearch(tab, props)}
isSelected={selectedTabId === tab.id}
key={`navigation-${tab.id}`}
onClick={() => {
track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.TAB_CLICKED}${tab.id}`);
}}
>
<EuiLink
data-test-subj={`navigation-link-${tab.id}`}
href={tab.href + getSearch(tab, props)}
>
<EuiTab
data-href={tab.href}
data-test-subj={`navigation-${tab.id}`}
disabled={tab.disabled}
isSelected={selectedTabId === tab.id}
onClick={() => {
track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.TAB_CLICKED}${tab.id}`);
}}
>
{tab.name}
</EuiTab>
</EuiLink>
</TabContainer>
{tab.name}
</EuiTab>
));
return (
<EuiTabs display={display} size="m">
{renderTabs()}
</EuiTabs>
);
return <EuiTabs display={display}>{renderTabs()}</EuiTabs>;
});
TabNavigation.displayName = 'TabNavigation';

View file

@ -9,7 +9,6 @@ import { UrlStateType } from '../url_state/constants';
export interface SiemNavigationComponentProps {
display?: 'default' | 'condensed';
navTabs: Record<string, NavTab>;
showBorder?: boolean;
}
export type SearchNavTab = NavTab | { urlKey: UrlStateType; isDetailPage: boolean };

View file

@ -492,7 +492,7 @@ describe('StatefulOpenTimeline', () => {
expect(
wrapper
.find('[data-test-subj="header-panel-title"]')
.find('[data-test-subj="header-section-title"]')
.first()
.text()
).toEqual(title);

View file

@ -30,7 +30,7 @@ describe('TitleRow', () => {
expect(
wrapper
.find('[data-test-subj="header-panel-title"]')
.find('[data-test-subj="header-section-title"]')
.first()
.text()
).toEqual(title);

View file

@ -10,7 +10,7 @@ import { pure } from 'recompose';
import * as i18n from '../translations';
import { OpenTimelineProps } from '../types';
import { HeaderPanel } from '../../header_panel';
import { HeaderSection } from '../../header_section';
type Props = Pick<OpenTimelineProps, 'onAddTimelinesToFavorites' | 'onDeleteSelected' | 'title'> & {
/** The number of timelines currently selected */
@ -23,7 +23,7 @@ type Props = Pick<OpenTimelineProps, 'onAddTimelinesToFavorites' | 'onDeleteSele
*/
export const TitleRow = pure<Props>(
({ onAddTimelinesToFavorites, onDeleteSelected, selectedTimelinesCount, title }) => (
<HeaderPanel title={title}>
<HeaderSection title={title}>
{(onAddTimelinesToFavorites || onDeleteSelected) && (
<EuiFlexGroup gutterSize="s" responsive={false}>
{onAddTimelinesToFavorites && (
@ -55,7 +55,7 @@ export const TitleRow = pure<Props>(
)}
</EuiFlexGroup>
)}
</HeaderPanel>
</HeaderSection>
)
);

View file

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HistogramSignals it renders 1`] = `
<Component>
<HistogramSignals />
</Component>
`;

View file

@ -0,0 +1,27 @@
/*
* 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 { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import React from 'react';
import '../../../../mock/ui_settings';
import { TestProviders } from '../../../../mock';
import { HistogramSignals } from './index';
jest.mock('../../../../lib/settings/use_kibana_ui_setting');
describe('HistogramSignals', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<HistogramSignals />
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
});

View file

@ -0,0 +1,85 @@
/*
* 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 {
Axis,
Chart,
HistogramBarSeries,
Settings,
getAxisId,
getSpecId,
niceTimeFormatByDay,
timeFormatter,
} from '@elastic/charts';
import React from 'react';
import { npStart } from 'ui/new_platform';
export const HistogramSignals = React.memo(() => {
const sampleChartData = [
{ x: 1571090784000, y: 2, a: 'a' },
{ x: 1571090784000, y: 2, b: 'b' },
{ x: 1571093484000, y: 7, a: 'a' },
{ x: 1571096184000, y: 3, a: 'a' },
{ x: 1571098884000, y: 2, a: 'a' },
{ x: 1571101584000, y: 7, a: 'a' },
{ x: 1571104284000, y: 3, a: 'a' },
{ x: 1571106984000, y: 2, a: 'a' },
{ x: 1571109684000, y: 7, a: 'a' },
{ x: 1571112384000, y: 3, a: 'a' },
{ x: 1571115084000, y: 2, a: 'a' },
{ x: 1571117784000, y: 7, a: 'a' },
{ x: 1571120484000, y: 3, a: 'a' },
{ x: 1571123184000, y: 2, a: 'a' },
{ x: 1571125884000, y: 7, a: 'a' },
{ x: 1571128584000, y: 3, a: 'a' },
{ x: 1571131284000, y: 2, a: 'a' },
{ x: 1571133984000, y: 7, a: 'a' },
{ x: 1571136684000, y: 3, a: 'a' },
{ x: 1571139384000, y: 2, a: 'a' },
{ x: 1571142084000, y: 7, a: 'a' },
{ x: 1571144784000, y: 3, a: 'a' },
{ x: 1571147484000, y: 2, a: 'a' },
{ x: 1571150184000, y: 7, a: 'a' },
{ x: 1571152884000, y: 3, a: 'a' },
{ x: 1571155584000, y: 2, a: 'a' },
{ x: 1571158284000, y: 7, a: 'a' },
{ x: 1571160984000, y: 3, a: 'a' },
{ x: 1571163684000, y: 2, a: 'a' },
{ x: 1571166384000, y: 7, a: 'a' },
{ x: 1571169084000, y: 3, a: 'a' },
{ x: 1571171784000, y: 2, a: 'a' },
{ x: 1571174484000, y: 7, a: 'a' },
];
return (
<Chart size={['100%', 259]}>
<Settings
legendPosition="bottom"
showLegend
theme={npStart.plugins.eui_utils.useChartsTheme()}
/>
<Axis
id={getAxisId('signalAxisX')}
position="bottom"
tickFormat={timeFormatter(niceTimeFormatByDay(1))}
/>
<Axis id={getAxisId('signalAxisY')} position="left" />
<HistogramBarSeries
id={getSpecId('signalBar')}
xScaleType="time"
yScaleType="linear"
xAccessor="x"
yAccessors={['y']}
splitSeriesAccessors={['a', 'b']}
data={sampleChartData}
/>
</Chart>
);
});
HistogramSignals.displayName = 'HistogramSignals';

View file

@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState } from 'react';
import { pure } from 'recompose';
import { HeaderPanel } from '../../../header_panel';
import { HeaderSection } from '../../../header_section';
import { manageQuery } from '../../../page/manage_query';
import {
ID as OverviewHostQueryId,
@ -42,7 +42,7 @@ export const OverviewHost = pure<OverviewHostProps>(({ endDate, startDate, setQu
return (
<EuiFlexItem>
<EuiPanel onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
<HeaderPanel
<HeaderSection
border
id={OverviewHostQueryId}
showInspect={isHover}
@ -59,7 +59,7 @@ export const OverviewHost = pure<OverviewHostProps>(({ endDate, startDate, setQu
<EuiButton href={getHostsUrl()}>
<FormattedMessage id="xpack.siem.overview.hostsAction" defaultMessage="View hosts" />
</EuiButton>
</HeaderPanel>
</HeaderSection>
<OverviewHostQuery endDate={endDate} sourceId="default" startDate={startDate}>
{({ overviewHost, loading, id, inspect, refetch }) => (

View file

@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState } from 'react';
import { pure } from 'recompose';
import { HeaderPanel } from '../../../header_panel';
import { HeaderSection } from '../../../header_section';
import { manageQuery } from '../../../page/manage_query';
import {
ID as OverviewNetworkQueryId,
@ -42,7 +42,7 @@ export const OverviewNetwork = pure<OwnProps>(({ endDate, startDate, setQuery })
return (
<EuiFlexItem>
<EuiPanel onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
<HeaderPanel
<HeaderSection
border
id={OverviewNetworkQueryId}
showInspect={isHover}
@ -65,7 +65,7 @@ export const OverviewNetwork = pure<OwnProps>(({ endDate, startDate, setQuery })
defaultMessage="View network"
/>
</EuiButton>
</HeaderPanel>
</HeaderSection>
<OverviewNetworkQuery endDate={endDate} sourceId="default" startDate={startDate}>
{({ overviewNetwork, loading, id, inspect, refetch }) => (

View file

@ -35,7 +35,7 @@ import {
import { TlsColumns } from '../page/network/tls_table/columns';
import { UncommonProcessTableColumns } from '../page/hosts/uncommon_process_table';
import { UsersColumns } from '../page/network/users_table/columns';
import { HeaderPanel } from '../header_panel';
import { HeaderSection } from '../header_section';
import { Loader } from '../loader';
import { useStateToaster } from '../toasters';
import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants';
@ -234,7 +234,7 @@ export const PaginatedTable = memo<SiemTables>(
onMouseEnter={() => setShowInspect(true)}
onMouseLeave={() => setShowInspect(false)}
>
<HeaderPanel
<HeaderSection
id={id}
showInspect={!loadingInitial && showInspect}
subtitle={
@ -245,7 +245,7 @@ export const PaginatedTable = memo<SiemTables>(
tooltip={headerTooltip}
>
{!loadingInitial && headerSupplement}
</HeaderPanel>
</HeaderSection>
{loadingInitial ? (
<EuiLoadingContent data-test-subj="initialLoadingPanelPaginatedTable" lines={10} />

View file

@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProgressInline it renders 1`] = `
<Component>
<ProgressInline
current={50}
max={100}
unit="tests"
>
Test progress
</ProgressInline>
</Component>
`;

View file

@ -0,0 +1,29 @@
/*
* 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 { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import React from 'react';
import { TestProviders } from '../../mock';
import '../../mock/ui_settings';
import { ProgressInline } from './index';
jest.mock('../../lib/settings/use_kibana_ui_setting');
describe('ProgressInline', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<ProgressInline current={50} max={100} unit="tests">
{'Test progress'}
</ProgressInline>
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
});

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiProgress } from '@elastic/eui';
import React from 'react';
import styled, { css } from 'styled-components';
const Wrapper = styled.dl`
${({ theme }) => css`
align-items: center;
display: inline-flex;
& > * + * {
margin-left: ${theme.eui.euiSizeS};
}
.siemProgressInline__bar {
width: 100px;
}
`}
`;
Wrapper.displayName = 'Wrapper';
export interface ProgressInlineProps {
children: string;
current: number;
max: number;
unit: string;
}
export const ProgressInline = React.memo<ProgressInlineProps>(
({ children, current, max, unit }) => (
<Wrapper className="siemProgressInline">
<dt className="siemProgressInline__title">{children}</dt>
<dd className="siemProgressInline__bar">
<EuiProgress color="secondary" max={max} value={current} />
</dd>
<dd className="siemProgressInline__ratio">
{current.toLocaleString()}
{'/'}
{max.toLocaleString()} {unit}
</dd>
</Wrapper>
)
);
ProgressInline.displayName = 'ProgressInline';

View file

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Subtitle it renders 1`] = `
<Component>
<Subtitle
items="Test subtitle"
/>
</Component>
`;

View file

@ -0,0 +1,77 @@
/*
* 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, shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import React from 'react';
import '../../mock/ui_settings';
import { TestProviders } from '../../mock';
import { Subtitle } from './index';
jest.mock('../../lib/settings/use_kibana_ui_setting');
describe('Subtitle', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<Subtitle items="Test subtitle" />
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('it renders one subtitle string item', () => {
const wrapper = mount(
<TestProviders>
<Subtitle items="Test subtitle" />
</TestProviders>
);
expect(wrapper.find('.siemSubtitle__item--text').length).toEqual(1);
});
test('it renders multiple subtitle string items', () => {
const wrapper = mount(
<TestProviders>
<Subtitle items={['Test subtitle 1', 'Test subtitle 2']} />
</TestProviders>
);
expect(wrapper.find('.siemSubtitle__item--text').length).toEqual(2);
});
test('it renders one subtitle React.ReactNode item', () => {
const wrapper = mount(
<TestProviders>
<Subtitle items={<span>{'Test subtitle'}</span>} />
</TestProviders>
);
expect(wrapper.find('.siemSubtitle__item--node').length).toEqual(1);
});
test('it renders multiple subtitle React.ReactNode items', () => {
const wrapper = mount(
<TestProviders>
<Subtitle items={[<span>{'Test subtitle 1'}</span>, <span>{'Test subtitle 2'}</span>]} />
</TestProviders>
);
expect(wrapper.find('.siemSubtitle__item--node').length).toEqual(2);
});
test('it renders multiple subtitle items of mixed type', () => {
const wrapper = mount(
<TestProviders>
<Subtitle items={['Test subtitle 1', <span>{'Test subtitle 2'}</span>]} />
</TestProviders>
);
expect(wrapper.find('.siemSubtitle__item').length).toEqual(2);
});
});

View file

@ -0,0 +1,60 @@
/*
* 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 React from 'react';
import styled, { css } from 'styled-components';
const Wrapper = styled.div`
${({ theme }) => css`
margin-top: ${theme.eui.euiSizeS};
.siemSubtitle__item {
color: ${theme.eui.textColors.subdued};
font-size: ${theme.eui.euiFontSizeXS};
line-height: ${theme.eui.euiLineHeight};
@media only screen and (min-width: ${theme.eui.euiBreakpoints.s}) {
display: inline-block;
margin-right: ${theme.eui.euiSize};
&:last-child {
margin-right: 0;
}
}
}
`}
`;
Wrapper.displayName = 'Wrapper';
interface SubtitleItemProps {
children: string | React.ReactNode;
}
const SubtitleItem = React.memo<SubtitleItemProps>(({ children }) => {
if (typeof children === 'string') {
return <p className="siemSubtitle__item siemSubtitle__item--text">{children}</p>;
} else {
return <div className="siemSubtitle__item siemSubtitle__item--node">{children}</div>;
}
});
SubtitleItem.displayName = 'SubtitleItem';
export interface SubtitleProps {
items: string | React.ReactNode | Array<string | React.ReactNode>;
}
export const Subtitle = React.memo<SubtitleProps>(({ items }) => {
return (
<Wrapper className="siemSubtitle">
{Array.isArray(items) ? (
items.map((item, i) => <SubtitleItem key={i}>{item}</SubtitleItem>)
) : (
<SubtitleItem>{items}</SubtitleItem>
)}
</Wrapper>
);
});
Subtitle.displayName = 'Subtitle';

View file

@ -6,17 +6,18 @@
export enum CONSTANTS {
appQuery = 'query',
detectionEnginePage = 'detectionEngine.page',
filters = 'filters',
savedQuery = 'savedQuery',
hostsDetails = 'hosts.details',
hostsPage = 'hosts.page',
networkDetails = 'network.details',
networkPage = 'network.page',
overviewPage = 'overview.page',
savedQuery = 'savedQuery',
timelinePage = 'timeline.page',
timerange = 'timerange',
timeline = 'timeline',
unknown = 'unknown',
}
export type UrlStateType = 'host' | 'network' | 'overview' | 'timeline';
export type UrlStateType = 'detection-engine' | 'host' | 'network' | 'overview' | 'timeline';

View file

@ -72,12 +72,14 @@ export const replaceQueryStringInLocation = (location: Location, queryString: st
};
export const getUrlType = (pageName: string): UrlStateType => {
if (pageName === SiemPageName.hosts) {
if (pageName === SiemPageName.overview) {
return 'overview';
} else if (pageName === SiemPageName.hosts) {
return 'host';
} else if (pageName === SiemPageName.network) {
return 'network';
} else if (pageName === SiemPageName.overview) {
return 'overview';
} else if (pageName === SiemPageName.detectionEngine) {
return 'detection-engine';
} else if (pageName === SiemPageName.timelines) {
return 'timeline';
}
@ -97,7 +99,9 @@ export const getCurrentLocation = (
pageName: string,
detailName: string | undefined
): LocationTypes => {
if (pageName === SiemPageName.hosts) {
if (pageName === SiemPageName.overview) {
return CONSTANTS.overviewPage;
} else if (pageName === SiemPageName.hosts) {
if (detailName != null) {
return CONSTANTS.hostsDetails;
}
@ -107,8 +111,8 @@ export const getCurrentLocation = (
return CONSTANTS.networkDetails;
}
return CONSTANTS.networkPage;
} else if (pageName === SiemPageName.overview) {
return CONSTANTS.overviewPage;
} else if (pageName === SiemPageName.detectionEngine) {
return CONSTANTS.detectionEnginePage;
} else if (pageName === SiemPageName.timelines) {
return CONSTANTS.timelinePage;
}

View file

@ -25,6 +25,7 @@ export const ALL_URL_STATE_KEYS: KeyUrlState[] = [
];
export const URL_STATE_KEYS: Record<UrlStateType, KeyUrlState[]> = {
'detection-engine': [],
host: [
CONSTANTS.appQuery,
CONSTANTS.filters,
@ -39,15 +40,16 @@ export const URL_STATE_KEYS: Record<UrlStateType, KeyUrlState[]> = {
CONSTANTS.timerange,
CONSTANTS.timeline,
],
timeline: [CONSTANTS.timeline, CONSTANTS.timerange],
overview: [CONSTANTS.timeline, CONSTANTS.timerange],
timeline: [CONSTANTS.timeline, CONSTANTS.timerange],
};
export type LocationTypes =
| CONSTANTS.networkDetails
| CONSTANTS.networkPage
| CONSTANTS.detectionEnginePage
| CONSTANTS.hostsDetails
| CONSTANTS.hostsPage
| CONSTANTS.networkDetails
| CONSTANTS.networkPage
| CONSTANTS.overviewPage
| CONSTANTS.timelinePage
| CONSTANTS.unknown;

View file

@ -0,0 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WrapperPage it renders 1`] = `
<Component>
<WrapperPage>
<p>
Test page
</p>
</WrapperPage>
</Component>
`;
exports[`WrapperPage restrict width custom max width when restrictWidth is number 1`] = `
<Component>
<WrapperPage
restrictWidth={600}
>
<p>
Test page
</p>
</WrapperPage>
</Component>
`;
exports[`WrapperPage restrict width custom max width when restrictWidth is string 1`] = `
<Component>
<WrapperPage
restrictWidth="600px"
>
<p>
Test page
</p>
</WrapperPage>
</Component>
`;
exports[`WrapperPage restrict width default max width when restrictWidth is true 1`] = `
<Component>
<WrapperPage
restrictWidth={true}
>
<p>
Test page
</p>
</WrapperPage>
</Component>
`;

View file

@ -0,0 +1,67 @@
/*
* 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 { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import React from 'react';
import { TestProviders } from '../../mock';
import '../../mock/ui_settings';
import { WrapperPage } from './index';
jest.mock('../../lib/settings/use_kibana_ui_setting');
describe('WrapperPage', () => {
test('it renders', () => {
const wrapper = shallow(
<TestProviders>
<WrapperPage>
<p>{'Test page'}</p>
</WrapperPage>
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
describe('restrict width', () => {
test('default max width when restrictWidth is true', () => {
const wrapper = shallow(
<TestProviders>
<WrapperPage restrictWidth>
<p>{'Test page'}</p>
</WrapperPage>
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('custom max width when restrictWidth is number', () => {
const wrapper = shallow(
<TestProviders>
<WrapperPage restrictWidth={600}>
<p>{'Test page'}</p>
</WrapperPage>
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('custom max width when restrictWidth is string', () => {
const wrapper = shallow(
<TestProviders>
<WrapperPage restrictWidth="600px">
<p>{'Test page'}</p>
</WrapperPage>
</TestProviders>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,61 @@
/*
* 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 classNames from 'classnames';
import React from 'react';
import styled, { css } from 'styled-components';
import { gutterTimeline } from '../../lib/helpers';
const Wrapper = styled.div`
${({ theme }) => css`
padding: ${theme.eui.paddingSizes.l} ${gutterTimeline} ${theme.eui.paddingSizes.l}
${theme.eui.paddingSizes.l};
&.siemWrapperPage--restrictWidthDefault,
&.siemWrapperPage--restrictWidthCustom {
box-sizing: content-box;
margin: 0 auto;
}
&.siemWrapperPage--restrictWidthDefault {
max-width: 1000px;
}
`}
`;
Wrapper.displayName = 'Wrapper';
export interface WrapperPageProps {
children: React.ReactNode;
className?: string;
restrictWidth?: boolean | number | string;
style?: Record<string, string>;
}
export const WrapperPage = React.memo<WrapperPageProps>(
({ children, className, restrictWidth, style }) => {
const classes = classNames(className, {
siemWrapperPage: true,
'siemWrapperPage--restrictWidthDefault':
restrictWidth && typeof restrictWidth === 'boolean' && restrictWidth === true,
'siemWrapperPage--restrictWidthCustom': restrictWidth && typeof restrictWidth !== 'boolean',
});
let customStyle: WrapperPageProps['style'];
if (restrictWidth && typeof restrictWidth !== 'boolean') {
const value = typeof restrictWidth === 'number' ? `${restrictWidth}px` : restrictWidth;
customStyle = { ...style, maxWidth: value };
}
return (
<Wrapper className={classes} style={customStyle || style}>
{children}
</Wrapper>
);
}
);
WrapperPage.displayName = 'WrapperPage';

View file

@ -42,3 +42,9 @@ export const assertUnreachable = (
): never => {
throw new Error(`${message}: ${x}`);
};
/**
* Global variables
*/
export const gutterTimeline = '70px'; // Michael: Temporary until timeline is moved.

View file

@ -8,13 +8,14 @@ import React from 'react';
import { pure } from 'recompose';
import { FormattedMessage } from '@kbn/i18n/react';
import { WrapperPage } from '../components/wrapper_page';
export const NotFoundPage = pure(() => (
<div>
<WrapperPage>
<FormattedMessage
id="xpack.siem.pages.fourohfour.noContentFoundDescription"
defaultMessage="No content found"
/>
</div>
</WrapperPage>
));
NotFoundPage.displayName = 'NotFoundPage';

View file

@ -0,0 +1,29 @@
/*
* 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 React from 'react';
import { HeaderPage } from '../../../components/header_page';
import { WrapperPage } from '../../../components/wrapper_page';
import { SpyRoute } from '../../../utils/route/spy_routes';
import * as i18n from './translations';
export const CreateRuleComponent = React.memo(() => {
return (
<>
<WrapperPage restrictWidth>
<HeaderPage
backOptions={{ href: '#detection-engine/rules', text: 'Back to rules' }}
border
title={i18n.PAGE_TITLE}
/>
</WrapperPage>
<SpyRoute />
</>
);
});
CreateRuleComponent.displayName = 'CreateRuleComponent';

View file

@ -0,0 +1,11 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.createRule.pageTitle', {
defaultMessage: 'Create new rule',
});

View file

@ -0,0 +1,205 @@
/*
* 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 {
EuiButton,
EuiFilterButton,
EuiFilterGroup,
EuiPanel,
EuiSelect,
EuiSpacer,
} from '@elastic/eui';
import React, { useState } from 'react';
import { StickyContainer } from 'react-sticky';
import { FiltersGlobal } from '../../components/filters_global';
import { HeaderPage } from '../../components/header_page';
import { HeaderSection } from '../../components/header_section';
import { HistogramSignals } from '../../components/page/detection_engine/histogram_signals';
import { SiemSearchBar } from '../../components/search_bar';
import {
UtilityBar,
UtilityBarAction,
UtilityBarGroup,
UtilityBarSection,
UtilityBarText,
} from '../../components/detection_engine/utility_bar';
import { WrapperPage } from '../../components/wrapper_page';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
import { SpyRoute } from '../../utils/route/spy_routes';
import { DetectionEngineEmptyPage } from './detection_engine_empty_page';
import * as i18n from './translations';
const OpenSignals = React.memo(() => {
return (
<>
<UtilityBar>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{`${i18n.PANEL_SUBTITLE_SHOWING}: 7,712 signals`}</UtilityBarText>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarText>{'Selected: 20 signals'}</UtilityBarText>
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={<p>{'Batch actions context menu here.'}</p>}
>
{'Batch actions'}
</UtilityBarAction>
<UtilityBarAction iconType="listAdd">
{'Select all signals on all pages'}
</UtilityBarAction>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarAction iconType="cross">{'Clear 7 filters'}</UtilityBarAction>
<UtilityBarAction iconType="cross">{'Clear aggregation'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={<p>{'Customize columns context menu here.'}</p>}
>
{'Customize columns'}
</UtilityBarAction>
<UtilityBarAction iconType="indexMapping">{'Aggregate data'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
{/* Michael: Open signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */}
</>
);
});
const ClosedSignals = React.memo(() => {
return (
<>
<UtilityBar>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{`${i18n.PANEL_SUBTITLE_SHOWING}: 7,712 signals`}</UtilityBarText>
</UtilityBarGroup>
</UtilityBarSection>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={<p>{'Customize columns context menu here.'}</p>}
>
{'Customize columns'}
</UtilityBarAction>
<UtilityBarAction iconType="indexMapping">{'Aggregate data'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
{/* Michael: Closed signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */}
</>
);
});
export const DetectionEngineComponent = React.memo(() => {
const sampleChartOptions = [
{ text: 'Risk scores', value: 'risk_scores' },
{ text: 'Severities', value: 'severities' },
{ text: 'Top destination IPs', value: 'destination_ips' },
{ text: 'Top event actions', value: 'event_actions' },
{ text: 'Top event categories', value: 'event_categories' },
{ text: 'Top host names', value: 'host_names' },
{ text: 'Top rule types', value: 'rule_types' },
{ text: 'Top rules', value: 'rules' },
{ text: 'Top source IPs', value: 'source_ips' },
{ text: 'Top users', value: 'users' },
];
const filterGroupOptions = ['open', 'closed'];
const [filterGroupState, setFilterGroupState] = useState(filterGroupOptions[0]);
return (
<>
<WithSource sourceId="default">
{({ indicesExist, indexPattern }) => {
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<StickyContainer>
<FiltersGlobal>
<SiemSearchBar id="global" indexPattern={indexPattern} />
</FiltersGlobal>
<WrapperPage>
<HeaderPage border subtitle={i18n.PAGE_SUBTITLE} title={i18n.PAGE_TITLE}>
<EuiButton fill href="#/detection-engine/rules" iconType="gear">
{i18n.BUTTON_MANAGE_RULES}
</EuiButton>
</HeaderPage>
<EuiPanel>
<HeaderSection title="Signal detection frequency">
<EuiSelect
options={sampleChartOptions}
onChange={() => {}}
prepend="Stack by"
value={sampleChartOptions[0].value}
/>
</HeaderSection>
<HistogramSignals />
</EuiPanel>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="All signals">
<EuiFilterGroup>
<EuiFilterButton
hasActiveFilters={filterGroupState === filterGroupOptions[0]}
onClick={() => setFilterGroupState(filterGroupOptions[0])}
withNext
>
{'Open signals'}
</EuiFilterButton>
<EuiFilterButton
hasActiveFilters={filterGroupState === filterGroupOptions[1]}
onClick={() => setFilterGroupState(filterGroupOptions[1])}
>
{'Closed signals'}
</EuiFilterButton>
</EuiFilterGroup>
</HeaderSection>
{filterGroupState === filterGroupOptions[0] ? <OpenSignals /> : <ClosedSignals />}
</EuiPanel>
</WrapperPage>
</StickyContainer>
) : (
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<DetectionEngineEmptyPage />
</WrapperPage>
);
}}
</WithSource>
<SpyRoute />
</>
);
});
DetectionEngineComponent.displayName = 'DetectionEngineComponent';

View file

@ -0,0 +1,29 @@
/*
* 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 React from 'react';
import chrome from 'ui/chrome';
import { documentationLinks } from 'ui/documentation_links';
import { EmptyPage } from '../../components/empty_page';
import * as i18n from './translations';
const basePath = chrome.getBasePath();
export const DetectionEngineEmptyPage = React.memo(() => (
<EmptyPage
actionPrimaryIcon="gear"
actionPrimaryLabel={i18n.EMPTY_ACTION_PRIMARY}
actionPrimaryUrl={`${basePath}/app/kibana#/home/tutorial_directory/siem`}
actionSecondaryIcon="popout"
actionSecondaryLabel={i18n.EMPTY_ACTION_SECONDARY}
actionSecondaryTarget="_blank"
actionSecondaryUrl={documentationLinks.siem}
data-test-subj="empty-page"
title={i18n.EMPTY_TITLE}
/>
));
DetectionEngineEmptyPage.displayName = 'DetectionEngineEmptyPage';

View file

@ -0,0 +1,128 @@
/*
* 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 {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiTabbedContent,
} from '@elastic/eui';
import React from 'react';
import { HeaderPage } from '../../../components/header_page';
import { HeaderSection } from '../../../components/header_section';
import { WrapperPage } from '../../../components/wrapper_page';
import { SpyRoute } from '../../../utils/route/spy_routes';
import * as i18n from './translations';
const Define = React.memo(() => (
<>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="Define rule" />
</EuiPanel>
</>
));
Define.displayName = 'Define';
const About = React.memo(() => (
<>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="About rule" />
</EuiPanel>
</>
));
About.displayName = 'About';
const Schedule = React.memo(() => (
<>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="Schedule rule" />
</EuiPanel>
</>
));
Schedule.displayName = 'Schedule';
export const EditRuleComponent = React.memo(() => {
return (
<>
<WrapperPage restrictWidth>
<HeaderPage
backOptions={{
href: '#detection-engine/rules/rule-details',
text: 'Back to automated exfiltration',
}}
title={i18n.PAGE_TITLE}
>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButton href="#/detection-engine/rules/rule-details" iconType="cross">
{'Cancel'}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill href="#/detection-engine/rules/rule-details" iconType="save">
{'Save changes'}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</HeaderPage>
<EuiTabbedContent
tabs={[
{
id: 'tabDefine',
name: 'Define',
content: <Define />,
},
{
id: 'tabAbout',
name: 'About',
content: <About />,
},
{
id: 'tabSchedule',
name: 'Schedule',
content: <Schedule />,
},
]}
/>
<EuiSpacer />
<EuiFlexGroup
alignItems="center"
gutterSize="s"
justifyContent="flexEnd"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton href="#/detection-engine/rules/rule-details" iconType="cross">
{'Cancel'}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill href="#/detection-engine/rules/rule-details" iconType="save">
{'Save changes'}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</WrapperPage>
<SpyRoute />
</>
);
});
EditRuleComponent.displayName = 'EditRuleComponent';

View file

@ -0,0 +1,11 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.editRule.pageTitle', {
defaultMessage: 'Edit rule settings',
});

View file

@ -0,0 +1,45 @@
/*
* 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 React from 'react';
import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom';
import { CreateRuleComponent } from './create_rule';
import { DetectionEngineComponent } from './detection_engine';
import { EditRuleComponent } from './edit_rule';
import { RuleDetailsComponent } from './rule_details';
import { RulesComponent } from './rules';
const detectionEnginePath = `/:pageName(detection-engine)`;
type Props = Partial<RouteComponentProps<{}>> & { url: string };
export const DetectionEngineContainer = React.memo<Props>(() => (
<Switch>
<Route exact path={detectionEnginePath} render={() => <DetectionEngineComponent />} strict />
<Route exact path={`${detectionEnginePath}/rules`} render={() => <RulesComponent />} />
<Route
path={`${detectionEnginePath}/rules/create-rule`}
render={() => <CreateRuleComponent />}
/>
<Route
exact
path={`${detectionEnginePath}/rules/rule-details`}
render={() => <RuleDetailsComponent />}
/>
<Route
path={`${detectionEnginePath}/rules/rule-details/edit-rule`}
render={() => <EditRuleComponent />}
/>
<Route
path="/detection-engine/"
render={({ location: { search = '' } }) => (
<Redirect from="/detection-engine/" to={`/detection-engine${search}`} />
)}
/>
</Switch>
));
DetectionEngineContainer.displayName = 'DetectionEngineContainer';

View file

@ -0,0 +1,660 @@
/*
* 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 {
EuiBasicTable,
EuiButton,
EuiButtonIcon,
EuiCallOut,
EuiFilterButton,
EuiFilterGroup,
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
EuiPanel,
EuiPopover,
EuiSelect,
EuiSpacer,
EuiSwitch,
EuiTabbedContent,
EuiTextColor,
} from '@elastic/eui';
import moment from 'moment';
import React, { useState } from 'react';
import { StickyContainer } from 'react-sticky';
import { getEmptyTagValue } from '../../../components/empty_value';
import { FiltersGlobal } from '../../../components/filters_global';
import { HeaderPage } from '../../../components/header_page';
import { HeaderSection } from '../../../components/header_section';
import { HistogramSignals } from '../../../components/page/detection_engine/histogram_signals';
import { ProgressInline } from '../../../components/progress_inline';
import { SiemSearchBar } from '../../../components/search_bar';
import {
UtilityBar,
UtilityBarAction,
UtilityBarGroup,
UtilityBarSection,
UtilityBarText,
} from '../../../components/detection_engine/utility_bar';
import { WrapperPage } from '../../../components/wrapper_page';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source';
import { SpyRoute } from '../../../utils/route/spy_routes';
import { DetectionEngineEmptyPage } from '../detection_engine_empty_page';
import * as i18n from './translations';
// Michael: Will need to change this to get the current datetime format from Kibana settings.
const dateTimeFormat = (value: string) => {
return moment(value).format('M/D/YYYY, h:mm A');
};
const OpenSignals = React.memo(() => {
return (
<>
<UtilityBar>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{'Showing: 439 signals'}</UtilityBarText>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarText>{'Selected: 20 signals'}</UtilityBarText>
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={<p>{'Batch actions context menu here.'}</p>}
>
{'Batch actions'}
</UtilityBarAction>
<UtilityBarAction iconType="listAdd">
{'Select all signals on all pages'}
</UtilityBarAction>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarAction iconType="cross">{'Clear 7 filters'}</UtilityBarAction>
<UtilityBarAction iconType="cross">{'Clear aggregation'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={<p>{'Customize columns context menu here.'}</p>}
>
{'Customize columns'}
</UtilityBarAction>
<UtilityBarAction iconType="indexMapping">{'Aggregate data'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
{/* Michael: Open signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */}
</>
);
});
const ClosedSignals = React.memo(() => {
return (
<>
<UtilityBar>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{'Showing: 439 signals'}</UtilityBarText>
</UtilityBarGroup>
</UtilityBarSection>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={<p>{'Customize columns context menu here.'}</p>}
>
{'Customize columns'}
</UtilityBarAction>
<UtilityBarAction iconType="indexMapping">{'Aggregate data'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
{/* Michael: Closed signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */}
</>
);
});
const Signals = React.memo(() => {
const sampleChartOptions = [
{ text: 'Risk scores', value: 'risk_scores' },
{ text: 'Severities', value: 'severities' },
{ text: 'Top destination IPs', value: 'destination_ips' },
{ text: 'Top event actions', value: 'event_actions' },
{ text: 'Top event categories', value: 'event_categories' },
{ text: 'Top host names', value: 'host_names' },
{ text: 'Top source IPs', value: 'source_ips' },
{ text: 'Top users', value: 'users' },
];
const filterGroupOptions = ['open', 'closed'];
const [filterGroupState, setFilterGroupState] = useState(filterGroupOptions[0]);
return (
<>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="Signal detection frequency">
<EuiSelect
options={sampleChartOptions}
onChange={() => {}}
prepend="Stack by"
value={sampleChartOptions[0].value}
/>
</HeaderSection>
<HistogramSignals />
</EuiPanel>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="Signals">
<EuiFilterGroup>
<EuiFilterButton
hasActiveFilters={filterGroupState === filterGroupOptions[0]}
onClick={() => setFilterGroupState(filterGroupOptions[0])}
withNext
>
{'Open signals'}
</EuiFilterButton>
<EuiFilterButton
hasActiveFilters={filterGroupState === filterGroupOptions[1]}
onClick={() => setFilterGroupState(filterGroupOptions[1])}
>
{'Closed signals'}
</EuiFilterButton>
</EuiFilterGroup>
</HeaderSection>
{filterGroupState === filterGroupOptions[0] ? <OpenSignals /> : <ClosedSignals />}
</EuiPanel>
</>
);
});
Signals.displayName = 'Signals';
const ActivityMonitor = React.memo(() => {
interface ColumnTypes {
id: number;
ran: string;
lookedBackTo: string;
status: string;
response: string | undefined;
}
interface PageTypes {
index: number;
size: number;
}
interface SortTypes {
field: string;
direction: string;
}
const actions = [
{
available: (item: ColumnTypes) => item.status === 'Running',
description: 'Stop',
icon: 'stop',
isPrimary: true,
name: 'Stop',
onClick: () => {},
type: 'icon',
},
{
available: (item: ColumnTypes) => item.status === 'Stopped',
description: 'Resume',
icon: 'play',
isPrimary: true,
name: 'Resume',
onClick: () => {},
type: 'icon',
},
];
// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes?
const columns = [
{
field: 'ran',
name: 'Ran',
render: (value: ColumnTypes['ran']) => <time dateTime={value}>{dateTimeFormat(value)}</time>,
sortable: true,
truncateText: true,
},
{
field: 'lookedBackTo',
name: 'Looked back to',
render: (value: ColumnTypes['lookedBackTo']) => (
<time dateTime={value}>{dateTimeFormat(value)}</time>
),
sortable: true,
truncateText: true,
},
{
field: 'status',
name: 'Status',
sortable: true,
truncateText: true,
},
{
field: 'response',
name: 'Response',
render: (value: ColumnTypes['response']) => {
return value === undefined ? (
getEmptyTagValue()
) : (
<>
{value === 'Fail' ? (
<EuiTextColor color="danger">
{value} <EuiIconTip content="Full fail message here." type="iInCircle" />
</EuiTextColor>
) : (
<EuiTextColor color="secondary">{value}</EuiTextColor>
)}
</>
);
},
sortable: true,
truncateText: true,
},
{
actions,
width: '40px',
},
];
const sampleTableData = [
{
id: 1,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Running',
},
{
id: 2,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Stopped',
},
{
id: 3,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Fail',
},
{
id: 4,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 5,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 6,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 7,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 8,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 9,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 10,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 11,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 12,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 13,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 14,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 15,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 16,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 17,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 18,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 19,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 20,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
{
id: 21,
ran: '2019-12-28 00:00:00.000-05:00',
lookedBackTo: '2019-12-28 00:00:00.000-05:00',
status: 'Completed',
response: 'Success',
},
];
const [itemsTotalState] = useState<number>(sampleTableData.length);
const [pageState, setPageState] = useState<PageTypes>({ index: 0, size: 20 });
// const [selectedState, setSelectedState] = useState<ColumnTypes[]>([]);
const [sortState, setSortState] = useState<SortTypes>({ field: 'ran', direction: 'desc' });
return (
<>
<EuiSpacer />
<EuiPanel>
<HeaderSection title="Activity monitor" />
<UtilityBar border>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText>{'Showing: 39 activites'}</UtilityBarText>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarText>{'Selected: 2 activities'}</UtilityBarText>
<UtilityBarAction iconType="stop">{'Stop selected'}</UtilityBarAction>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarAction iconType="cross">{'Clear 7 filters'}</UtilityBarAction>
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
<EuiBasicTable
columns={columns}
isSelectable
itemId="id"
items={sampleTableData}
onChange={({ page, sort }: { page: PageTypes; sort: SortTypes }) => {
setPageState(page);
setSortState(sort);
}}
pagination={{
pageIndex: pageState.index,
pageSize: pageState.size,
totalItemCount: itemsTotalState,
pageSizeOptions: [5, 10, 20],
}}
selection={{
selectable: (item: ColumnTypes) => item.status !== 'Completed',
selectableMessage: (selectable: boolean) =>
selectable ? undefined : 'Completed runs cannot be acted upon',
onSelectionChange: (selectedItems: ColumnTypes[]) => {
// setSelectedState(selectedItems);
},
}}
sorting={{
sort: sortState,
}}
/>
</EuiPanel>
</>
);
});
ActivityMonitor.displayName = 'ActivityMonitor';
export const RuleDetailsComponent = React.memo(() => {
const [popoverState, setPopoverState] = useState(false);
return (
<>
<WithSource sourceId="default">
{({ indicesExist, indexPattern }) => {
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<StickyContainer>
<FiltersGlobal>
<SiemSearchBar id="global" indexPattern={indexPattern} />
</FiltersGlobal>
<WrapperPage>
<HeaderPage
backOptions={{ href: '#detection-engine/rules', text: 'Back to rules' }}
badgeOptions={{ text: 'Experimental' }}
border
subtitle={[
'Created by: mmarcialis on 12/28/2019, 12:00 PM',
'Updated by: agoldstein on 12/28/2019, 12:00 PM',
]}
subtitle2={[
'Last signal: 23 minutes ago',
<ProgressInline current={95000} max={105000} unit="events">
{'Status: Running'}
</ProgressInline>,
]}
title="Automated exfiltration"
>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiSwitch checked={true} label="Activate rule" onChange={() => {}} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButton
href="#detection-engine/rules/rule-details/edit-rule"
iconType="visControls"
>
{'Edit rule settings'}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButtonIcon
aria-label="Additional actions"
iconType="boxesHorizontal"
onClick={() => setPopoverState(!popoverState)}
/>
}
closePopover={() => setPopoverState(false)}
isOpen={popoverState}
>
<p>{'Overflow context menu here.'}</p>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</HeaderPage>
<EuiCallOut
color="danger"
iconType="alert"
size="s"
title="Rule failed to run on 12/28/2019, 12:00 PM"
>
<p>{'Full fail message here.'}</p>
</EuiCallOut>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem component="section" grow={1}>
<EuiPanel>
<HeaderSection title="Definition" />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem component="section" grow={2}>
<EuiPanel>
<HeaderSection title="About" />
{/* <p>{'Description'}</p> */}
{/* <EuiFlexGrid columns={2}>
<EuiFlexItem style={{ flex: '0 0 calc(100% - 24px)' }}>
<p>{'Description'}</p>
</EuiFlexItem>
<EuiFlexItem>
<p>{'Severity'}</p>
</EuiFlexItem>
<EuiFlexItem>
<p>{'Risk score boost'}</p>
</EuiFlexItem>
<EuiFlexItem>
<p>{'References'}</p>
</EuiFlexItem>
<EuiFlexItem>
<p>{'False positives'}</p>
</EuiFlexItem>
<EuiFlexItem>
<p>{'Mitre ATT&CK types'}</p>
</EuiFlexItem>
<EuiFlexItem>
<p>{'Tags'}</p>
</EuiFlexItem>
</EuiFlexGrid> */}
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem component="section" grow={1}>
<EuiPanel>
<HeaderSection title="Schedule" />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiTabbedContent
tabs={[
{
id: 'tabSignals',
name: 'Signals',
content: <Signals />,
},
{
id: 'tabActivityMonitor',
name: 'Activity monitor',
content: <ActivityMonitor />,
},
]}
/>
</WrapperPage>
</StickyContainer>
) : (
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<DetectionEngineEmptyPage />
</WrapperPage>
);
}}
</WithSource>
<SpyRoute />
</>
);
});
RuleDetailsComponent.displayName = 'RuleDetailsComponent';

View file

@ -0,0 +1,11 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.ruleDetails.pageTitle', {
defaultMessage: 'Rule details',
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageTitle', {
defaultMessage: 'Rules',
});

View file

@ -0,0 +1,45 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle', {
defaultMessage: 'Detection engine',
});
export const PAGE_SUBTITLE = i18n.translate('xpack.siem.detectionEngine.pageSubtitle', {
defaultMessage: 'Last signal: X minutes ago',
});
export const BUTTON_MANAGE_RULES = i18n.translate('xpack.siem.detectionEngine.buttonManageRules', {
defaultMessage: 'Manage rules',
});
export const PANEL_SUBTITLE_SHOWING = i18n.translate(
'xpack.siem.detectionEngine.panelSubtitleShowing',
{
defaultMessage: 'Showing',
}
);
export const EMPTY_TITLE = i18n.translate('xpack.siem.detectionEngine.emptyTitle', {
defaultMessage:
'It looks like you dont have any indices relevant to the detction engine in the SIEM application',
});
export const EMPTY_ACTION_PRIMARY = i18n.translate(
'xpack.siem.detectionEngine.emptyActionPrimary',
{
defaultMessage: 'View setup instructions',
}
);
export const EMPTY_ACTION_SECONDARY = i18n.translate(
'xpack.siem.detectionEngine.emptyActionSecondary',
{
defaultMessage: 'Go to documentation',
}
);

View file

@ -3,14 +3,16 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as i18n from './translations';
import { SiemPageName, SiemNavTab } from './types';
import {
getDetectionEngineUrl,
getOverviewUrl,
getNetworkUrl,
getTimelinesUrl,
getHostsUrl,
} from '../../components/link_to';
import * as i18n from './translations';
import { SiemPageName, SiemNavTab } from './types';
export const navTabs: SiemNavTab = {
[SiemPageName.overview]: {
@ -34,6 +36,13 @@ export const navTabs: SiemNavTab = {
disabled: false,
urlKey: 'network',
},
[SiemPageName.detectionEngine]: {
id: SiemPageName.detectionEngine,
name: i18n.DETECTION_ENGINE,
href: getDetectionEngineUrl(),
disabled: false,
urlKey: 'detection-engine',
},
[SiemPageName.timelines]: {
id: SiemPageName.timelines,
name: i18n.TIMELINES,

View file

@ -4,35 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPage, EuiPageBody } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import * as React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { pure } from 'recompose';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { AutoSizer } from '../../components/auto_sizer';
import { DragDropContextWrapper } from '../../components/drag_and_drop/drag_drop_context_wrapper';
import { Flyout, flyoutHeaderHeight } from '../../components/flyout';
import { HeaderGlobal } from '../../components/header_global';
import { HelpMenu } from '../../components/help_menu';
import { LinkToPage } from '../../components/link_to';
import { SiemNavigation } from '../../components/navigation';
import { MlHostConditionalContainer } from '../../components/ml/conditional_links/ml_host_conditional_container';
import { MlNetworkConditionalContainer } from '../../components/ml/conditional_links/ml_network_conditional_container';
import { StatefulTimeline } from '../../components/timeline';
import { AutoSaveWarningMsg } from '../../components/timeline/auto_save_warning';
import { UseUrlState } from '../../components/url_state';
import { WithSource } from '../../containers/source';
import { SpyRoute } from '../../utils/route/spy_routes';
import { NotFoundPage } from '../404';
import { DetectionEngineContainer } from '../detection_engine';
import { HostsContainer } from '../hosts';
import { NetworkContainer } from '../network';
import { Overview } from '../overview';
import { Timelines } from '../timelines';
import { WithSource } from '../../containers/source';
import { MlPopover } from '../../components/ml_popover/ml_popover';
import { MlHostConditionalContainer } from '../../components/ml/conditional_links/ml_host_conditional_container';
import { MlNetworkConditionalContainer } from '../../components/ml/conditional_links/ml_network_conditional_container';
import { navTabs } from './home_navigations';
import { SiemPageName } from './types';
import { UseUrlState } from '../../components/url_state';
import { SpyRoute } from '../../utils/route/spy_routes';
/*
* This is import is important to keep because if we do not have it
@ -44,30 +41,8 @@ import 'uiExports/embeddableFactories';
const WrappedByAutoSizer = styled.div`
height: 100%;
`;
WrappedByAutoSizer.displayName = 'WrappedByAutoSizer';
const gutterTimeline = '70px'; // Temporary until timeline is moved - MichaelMarcialis
const Page = styled(EuiPage)`
${({ theme }) => `
padding: 0 ${gutterTimeline} ${theme.eui.euiSizeL} ${theme.eui.euiSizeL};
`}
`;
Page.displayName = 'Page';
const NavGlobal = styled.nav`
${({ theme }) => `
background: ${theme.eui.euiColorEmptyShade};
border-bottom: ${theme.eui.euiBorderThin};
margin: 0 -${gutterTimeline} 0 -${theme.eui.euiSizeL};
padding: ${theme.eui.euiSize} ${gutterTimeline} ${theme.eui.euiSize} ${theme.eui.euiSizeL};
`}
`;
NavGlobal.displayName = 'NavGlobal';
const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance)
/** the global Kibana navigation at the top of every page */
@ -85,8 +60,9 @@ export const HomePage = pure(() => (
<AutoSizer detectAnyWindowResize={true} content>
{({ measureRef, windowMeasurement: { height: windowHeight = 0 } }) => (
<WrappedByAutoSizer data-test-subj="wrapped-by-auto-sizer" innerRef={measureRef}>
<Page data-test-subj="pageContainer">
<HelpMenu />
<HeaderGlobal />
<main data-test-subj="pageContainer">
<WithSource sourceId="default">
{({ browserFields, indexPattern }) => (
<DragDropContextWrapper browserFields={browserFields}>
@ -111,90 +87,59 @@ export const HomePage = pure(() => (
/>
</Flyout>
<EuiPageBody>
<NavGlobal
aria-label={i18n.translate('xpack.siem.global.navigationLabel', {
defaultMessage: 'SIEM app',
})}
>
<EuiFlexGroup alignItems="center" gutterSize="m" justifyContent="spaceBetween">
<EuiFlexItem>
<SiemNavigation navTabs={navTabs} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup
alignItems="center"
gutterSize="m"
responsive={false}
wrap={true}
>
<EuiFlexItem grow={false}>
<MlPopover />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="add-data"
href="kibana#home/tutorial_directory/siem"
iconType="plusInCircle"
>
<FormattedMessage
id="xpack.siem.global.addData"
defaultMessage="Add data"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</NavGlobal>
<Switch>
<Redirect from="/" exact={true} to={`/${SiemPageName.overview}`} />
<Route
path={`/:pageName(${SiemPageName.overview})`}
render={() => <Overview />}
/>
<Route
path={`/:pageName(${SiemPageName.hosts})`}
render={({ match, location }) => (
<HostsContainer url={match.url} location={location} />
)}
/>
<Route
path={`/:pageName(${SiemPageName.network})`}
render={({ match, location }) => (
<NetworkContainer url={match.url} location={location} />
)}
/>
<Route
path={`/:pageName(${SiemPageName.timelines})`}
render={() => <Timelines />}
/>
<Route path="/link-to" component={LinkToPage} />
<Route
path="/ml-hosts"
render={({ match, location }) => (
<MlHostConditionalContainer url={match.url} location={location} />
)}
/>
<Route
path="/ml-network"
render={({ match, location }) => (
<MlNetworkConditionalContainer url={match.url} location={location} />
)}
/>
<Route component={NotFoundPage} />
</Switch>
</EuiPageBody>
<Switch>
<Redirect exact from="/" to={`/${SiemPageName.overview}`} />
<Route
path={`/:pageName(${SiemPageName.overview})`}
render={() => <Overview />}
/>
<Route
path={`/:pageName(${SiemPageName.hosts})`}
render={({ location, match }) => (
<HostsContainer location={location} url={match.url} />
)}
/>
<Route
path={`/:pageName(${SiemPageName.network})`}
render={({ location, match }) => (
<NetworkContainer location={location} url={match.url} />
)}
/>
<Route
path={`/:pageName(${SiemPageName.detectionEngine})`}
render={({ location, match }) => (
<DetectionEngineContainer location={location} url={match.url} />
)}
/>
<Route
path={`/:pageName(${SiemPageName.timelines})`}
render={() => <Timelines />}
/>
<Route path="/link-to" component={LinkToPage} />
<Route
path="/ml-hosts"
render={({ location, match }) => (
<MlHostConditionalContainer location={location} url={match.url} />
)}
/>
<Route
path="/ml-network"
render={({ location, match }) => (
<MlNetworkConditionalContainer location={location} url={match.url} />
)}
/>
<Route component={NotFoundPage} />
</Switch>
</DragDropContextWrapper>
)}
</WithSource>
</Page>
</main>
<HelpMenu />
<SpyRoute />
</WrappedByAutoSizer>
)}
</AutoSizer>
));
HomePage.displayName = 'HomePage';

View file

@ -18,6 +18,10 @@ export const NETWORK = i18n.translate('xpack.siem.navigation.network', {
defaultMessage: 'Network',
});
export const DETECTION_ENGINE = i18n.translate('xpack.siem.navigation.detectionEngine', {
defaultMessage: 'Detection engine',
});
export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', {
defaultMessage: 'Timelines',
});

View file

@ -10,6 +10,7 @@ export enum SiemPageName {
overview = 'overview',
hosts = 'hosts',
network = 'network',
detectionEngine = 'detection-engine',
timelines = 'timelines',
}
@ -17,6 +18,7 @@ export type SiemNavTabKey =
| SiemPageName.overview
| SiemPageName.hosts
| SiemPageName.network
| SiemPageName.detectionEngine
| SiemPageName.timelines;
export type SiemNavTab = Record<SiemNavTabKey, NavTab>;

View file

@ -6,39 +6,40 @@
import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import React, { useContext, useEffect } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { StickyContainer } from 'react-sticky';
import { compose } from 'redux';
import { inputsSelectors, State } from '../../../store';
import { FiltersGlobal } from '../../../components/filters_global';
import { HeaderPage } from '../../../components/header_page';
import { KpiHostDetailsQuery } from '../../../containers/kpi_host_details';
import { LastEventTime } from '../../../components/last_event_time';
import { hostToCriteria } from '../../../components/ml/criteria/host_to_criteria';
import { MlCapabilitiesContext } from '../../../components/ml/permissions/ml_capabilities_provider';
import { hasMlUserPermissions } from '../../../components/ml/permissions/has_ml_user_permissions';
import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider';
import { hostToCriteria } from '../../../components/ml/criteria/host_to_criteria';
import { hasMlUserPermissions } from '../../../components/ml/permissions/has_ml_user_permissions';
import { MlCapabilitiesContext } from '../../../components/ml/permissions/ml_capabilities_provider';
import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime';
import { setHostDetailsTablesActivePageToZero as dispatchHostDetailsTablesActivePageToZero } from '../../../store/hosts/actions';
import { SiemNavigation } from '../../../components/navigation';
import { manageQuery } from '../../../components/page/manage_query';
import { HostOverview } from '../../../components/page/hosts/host_overview';
import { KpiHostsComponent } from '../../../components/page/hosts';
import { HostOverview } from '../../../components/page/hosts/host_overview';
import { manageQuery } from '../../../components/page/manage_query';
import { SiemSearchBar } from '../../../components/search_bar';
import { WrapperPage } from '../../../components/wrapper_page';
import { HostOverviewByNameQuery } from '../../../containers/hosts/overview';
import { KpiHostDetailsQuery } from '../../../containers/kpi_host_details';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source';
import { LastEventIndexKey } from '../../../graphql/types';
import { useKibanaCore } from '../../../lib/compose/kibana_core';
import { convertToBuildEsQuery } from '../../../lib/keury';
import { inputsSelectors, State } from '../../../store';
import { setHostDetailsTablesActivePageToZero as dispatchHostDetailsTablesActivePageToZero } from '../../../store/hosts/actions';
import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions';
import { SpyRoute } from '../../../utils/route/spy_routes';
import { useKibanaCore } from '../../../lib/compose/kibana_core';
import { esQuery } from '../../../../../../../../src/plugins/data/public';
import { HostsEmptyPage } from '../hosts_empty_page';
import { HostDetailsTabs } from './details_tabs';
import { navTabsHostDetails } from './nav_tabs';
import { HostDetailsComponentProps, HostDetailsProps } from './types';
import { HostDetailsTabs } from './details_tabs';
import { type } from './utils';
const HostOverviewManage = manageQuery(HostOverview);
@ -63,6 +64,7 @@ const HostDetailsComponent = React.memo<HostDetailsComponentProps>(
}, [detailName]);
const capabilities = useContext(MlCapabilitiesContext);
const core = useKibanaCore();
return (
<>
<WithSource sourceId="default">
@ -96,14 +98,16 @@ const HostDetailsComponent = React.memo<HostDetailsComponentProps>(
...filters,
],
});
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<>
<StickyContainer>
<FiltersGlobal>
<SiemSearchBar indexPattern={indexPattern} id="global" />
</FiltersGlobal>
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<StickyContainer>
<FiltersGlobal>
<SiemSearchBar indexPattern={indexPattern} id="global" />
</FiltersGlobal>
<WrapperPage>
<HeaderPage
border
subtitle={
<LastEventTime
indexKey={LastEventIndexKey.hostDetails}
@ -112,6 +116,7 @@ const HostDetailsComponent = React.memo<HostDetailsComponentProps>(
}
title={detailName}
/>
<HostOverviewByNameQuery
sourceId="default"
hostName={detailName}
@ -182,40 +187,40 @@ const HostDetailsComponent = React.memo<HostDetailsComponentProps>(
<SiemNavigation
navTabs={navTabsHostDetails(detailName, hasMlUserPermissions(capabilities))}
display="default"
showBorder={true}
/>
<EuiSpacer />
</StickyContainer>
<HostDetailsTabs
isInitializing={isInitializing}
deleteQuery={deleteQuery}
to={to}
from={from}
detailName={detailName}
type={type}
setQuery={setQuery}
filterQuery={filterQuery}
hostDetailsPagePath={hostDetailsPagePath}
indexPattern={indexPattern}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker}
/>
</>
<HostDetailsTabs
isInitializing={isInitializing}
deleteQuery={deleteQuery}
to={to}
from={from}
detailName={detailName}
type={type}
setQuery={setQuery}
filterQuery={filterQuery}
hostDetailsPagePath={hostDetailsPagePath}
indexPattern={indexPattern}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker}
/>
</WrapperPage>
</StickyContainer>
) : (
<>
<HeaderPage title={detailName} />
<WrapperPage>
<HeaderPage border title={detailName} />
<HostsEmptyPage />
</>
</WrapperPage>
);
}}
</WithSource>
<SpyRoute />
</>
);
}
);
HostDetailsComponent.displayName = 'HostDetailsComponent';
export const makeMapStateToProps = () => {

View file

@ -6,35 +6,35 @@
import { EuiSpacer } from '@elastic/eui';
import * as React from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { StickyContainer } from 'react-sticky';
import { compose } from 'redux';
import { FiltersGlobal } from '../../components/filters_global';
import { GlobalTimeArgs } from '../../containers/global_time';
import { HeaderPage } from '../../components/header_page';
import { KpiHostsQuery } from '../../containers/kpi_hosts';
import { LastEventTime } from '../../components/last_event_time';
import { hasMlUserPermissions } from '../../components/ml/permissions/has_ml_user_permissions';
import { MlCapabilitiesContext } from '../../components/ml/permissions/ml_capabilities_provider';
import { SiemNavigation } from '../../components/navigation';
import { KpiHostsComponent } from '../../components/page/hosts';
import { manageQuery } from '../../components/page/manage_query';
import { hasMlUserPermissions } from '../../components/ml/permissions/has_ml_user_permissions';
import { MlCapabilitiesContext } from '../../components/ml/permissions/ml_capabilities_provider';
import { SiemSearchBar } from '../../components/search_bar';
import { WrapperPage } from '../../components/wrapper_page';
import { GlobalTimeArgs } from '../../containers/global_time';
import { KpiHostsQuery } from '../../containers/kpi_hosts';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
import { LastEventIndexKey } from '../../graphql/types';
import { useKibanaCore } from '../../lib/compose/kibana_core';
import { convertToBuildEsQuery } from '../../lib/keury';
import { inputsSelectors, State, hostsModel } from '../../store';
import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions';
import { SpyRoute } from '../../utils/route/spy_routes';
import { useKibanaCore } from '../../lib/compose/kibana_core';
import { esQuery } from '../../../../../../../src/plugins/data/public';
import { HostsEmptyPage } from './hosts_empty_page';
import { HostsTabs } from './hosts_tabs';
import { navTabsHosts } from './nav_tabs';
import * as i18n from './translations';
import { HostsComponentProps, HostsComponentReduxProps } from './types';
import { HostsTabs } from './hosts_tabs';
const KpiHostsComponentManage = manageQuery(KpiHostsComponent);
@ -64,76 +64,77 @@ const HostsComponent = React.memo<HostsComponentProps>(
filters,
});
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<>
<StickyContainer>
<FiltersGlobal>
<SiemSearchBar indexPattern={indexPattern} id="global" />
</FiltersGlobal>
<StickyContainer>
<FiltersGlobal>
<SiemSearchBar indexPattern={indexPattern} id="global" />
</FiltersGlobal>
<WrapperPage>
<HeaderPage
border
subtitle={<LastEventTime indexKey={LastEventIndexKey.hosts} />}
title={i18n.PAGE_TITLE}
/>
<>
<KpiHostsQuery
endDate={to}
filterQuery={filterQuery}
skip={isInitializing}
sourceId="default"
startDate={from}
>
{({ kpiHosts, loading, id, inspect, refetch }) => (
<KpiHostsComponentManage
data={kpiHosts}
from={from}
id={id}
inspect={inspect}
loading={loading}
refetch={refetch}
setQuery={setQuery}
to={to}
narrowDateRange={(min: number, max: number) => {
setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
}}
/>
)}
</KpiHostsQuery>
<EuiSpacer />
<SiemNavigation
navTabs={navTabsHosts(hasMlUserPermissions(capabilities))}
display="default"
showBorder={true}
/>
<EuiSpacer />
</>
</StickyContainer>
<HostsTabs
deleteQuery={deleteQuery}
to={to}
filterQuery={filterQuery}
isInitializing={isInitializing}
setQuery={setQuery}
from={from}
type={hostsModel.HostsType.page}
indexPattern={indexPattern}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker}
hostsPagePath={hostsPagePath}
/>
</>
<KpiHostsQuery
endDate={to}
filterQuery={filterQuery}
skip={isInitializing}
sourceId="default"
startDate={from}
>
{({ kpiHosts, loading, id, inspect, refetch }) => (
<KpiHostsComponentManage
data={kpiHosts}
from={from}
id={id}
inspect={inspect}
loading={loading}
refetch={refetch}
setQuery={setQuery}
to={to}
narrowDateRange={(min: number, max: number) => {
setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
}}
/>
)}
</KpiHostsQuery>
<EuiSpacer />
<SiemNavigation navTabs={navTabsHosts(hasMlUserPermissions(capabilities))} />
<EuiSpacer />
<HostsTabs
deleteQuery={deleteQuery}
to={to}
filterQuery={filterQuery}
isInitializing={isInitializing}
setQuery={setQuery}
from={from}
type={hostsModel.HostsType.page}
indexPattern={indexPattern}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker}
hostsPagePath={hostsPagePath}
/>
</WrapperPage>
</StickyContainer>
) : (
<>
<HeaderPage title={i18n.PAGE_TITLE} />
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<HostsEmptyPage />
</>
</WrapperPage>
);
}}
</WithSource>
<SpyRoute />
</>
);
}
);
HostsComponent.displayName = 'HostsComponent';
const makeMapStateToProps = () => {

View file

@ -163,7 +163,7 @@ describe('Ip Details', () => {
wrapper.update();
expect(
wrapper
.find('[data-test-subj="ip-details-headline"] [data-test-subj="page_headline_title"]')
.find('[data-test-subj="ip-details-headline"] [data-test-subj="header-page-title"]')
.text()
).toEqual('fe80::24ce:f7ff:fede:a571');
});

View file

@ -20,9 +20,11 @@ import { manageQuery } from '../../../components/page/manage_query';
import { FlowTargetSelectConnected } from '../../../components/page/network/flow_target_select_connected';
import { IpOverview } from '../../../components/page/network/ip_overview';
import { SiemSearchBar } from '../../../components/search_bar';
import { WrapperPage } from '../../../components/wrapper_page';
import { IpOverviewQuery } from '../../../containers/ip_overview';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source';
import { FlowTargetSourceDest, LastEventIndexKey } from '../../../graphql/types';
import { useKibanaCore } from '../../../lib/compose/kibana_core';
import { decodeIpv6 } from '../../../lib/helpers';
import { convertToBuildEsQuery } from '../../../lib/keury';
import { ConditionalFlexGroup } from '../../../pages/network/navigation/conditional_flex_group';
@ -31,16 +33,15 @@ import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '.
import { setIpDetailsTablesActivePageToZero as dispatchIpDetailsTablesActivePageToZero } from '../../../store/network/actions';
import { SpyRoute } from '../../../utils/route/spy_routes';
import { NetworkEmptyPage } from '../network_empty_page';
import { IPDetailsComponentProps } from './types';
export { getBreadcrumbs } from './utils';
import { TlsQueryTable } from './tls_query_table';
import { UsersQueryTable } from './users_query_table';
import { NetworkTopNFlowQueryTable } from './network_top_n_flow_query_table';
import { NetworkHttpQueryTable } from './network_http_query_table';
import { NetworkTopCountriesQueryTable } from './network_top_countries_query_table';
import { NetworkTopNFlowQueryTable } from './network_top_n_flow_query_table';
import { TlsQueryTable } from './tls_query_table';
import { IPDetailsComponentProps } from './types';
import { UsersQueryTable } from './users_query_table';
import { esQuery } from '../../../../../../../../src/plugins/data/public';
import { useKibanaCore } from '../../../lib/compose/kibana_core';
export { getBreadcrumbs } from './utils';
const IpOverviewManage = manageQuery(IpOverview);
@ -85,193 +86,197 @@ export const IPDetailsComponent = React.memo<IPDetailsComponentProps>(
queries: [query],
filters,
});
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<StickyContainer>
<FiltersGlobal>
<SiemSearchBar indexPattern={indexPattern} id="global" />
</FiltersGlobal>
<HeaderPage
data-test-subj="ip-details-headline"
subtitle={<LastEventTime indexKey={LastEventIndexKey.ipDetails} ip={ip} />}
title={ip}
draggableArguments={{ field: `${flowTarget}.ip`, value: ip }}
>
<FlowTargetSelectConnected />
</HeaderPage>
<WrapperPage>
<HeaderPage
border
data-test-subj="ip-details-headline"
draggableArguments={{ field: `${flowTarget}.ip`, value: ip }}
subtitle={<LastEventTime indexKey={LastEventIndexKey.ipDetails} ip={ip} />}
title={ip}
>
<FlowTargetSelectConnected />
</HeaderPage>
<IpOverviewQuery
skip={isInitializing}
sourceId="default"
filterQuery={filterQuery}
type={networkModel.NetworkType.details}
ip={ip}
>
{({ id, inspect, ipOverviewData, loading, refetch }) => (
<AnomalyTableProvider
criteriaFields={networkToCriteria(detailName, flowTarget)}
startDate={from}
endDate={to}
skip={isInitializing}
>
{({ isLoadingAnomaliesData, anomaliesData }) => (
<IpOverviewManage
id={id}
inspect={inspect}
ip={ip}
data={ipOverviewData}
anomaliesData={anomaliesData}
loading={loading}
isLoadingAnomaliesData={isLoadingAnomaliesData}
type={networkModel.NetworkType.details}
flowTarget={flowTarget}
refetch={refetch}
setQuery={setQuery}
startDate={from}
endDate={to}
narrowDateRange={(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
setAbsoluteRangeDatePicker({
id: 'global',
from: fromTo.from,
to: fromTo.to,
});
}}
/>
)}
</AnomalyTableProvider>
)}
</IpOverviewQuery>
<IpOverviewQuery
skip={isInitializing}
sourceId="default"
filterQuery={filterQuery}
type={networkModel.NetworkType.details}
ip={ip}
>
{({ id, inspect, ipOverviewData, loading, refetch }) => (
<AnomalyTableProvider
criteriaFields={networkToCriteria(detailName, flowTarget)}
startDate={from}
endDate={to}
skip={isInitializing}
>
{({ isLoadingAnomaliesData, anomaliesData }) => (
<IpOverviewManage
id={id}
inspect={inspect}
ip={ip}
data={ipOverviewData}
anomaliesData={anomaliesData}
loading={loading}
isLoadingAnomaliesData={isLoadingAnomaliesData}
type={networkModel.NetworkType.details}
flowTarget={flowTarget}
refetch={refetch}
setQuery={setQuery}
startDate={from}
endDate={to}
narrowDateRange={(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
setAbsoluteRangeDatePicker({
id: 'global',
from: fromTo.from,
to: fromTo.to,
});
}}
/>
)}
</AnomalyTableProvider>
)}
</IpOverviewQuery>
<EuiHorizontalRule />
<EuiHorizontalRule />
<ConditionalFlexGroup direction="column">
<EuiFlexItem>
<NetworkTopNFlowQueryTable
endDate={to}
filterQuery={filterQuery}
flowTarget={FlowTargetSourceDest.source}
ip={ip}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
setQuery={setQuery}
indexPattern={indexPattern}
/>
</EuiFlexItem>
<ConditionalFlexGroup direction="column">
<EuiFlexItem>
<NetworkTopNFlowQueryTable
endDate={to}
filterQuery={filterQuery}
flowTarget={FlowTargetSourceDest.source}
ip={ip}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
setQuery={setQuery}
indexPattern={indexPattern}
/>
</EuiFlexItem>
<EuiFlexItem>
<NetworkTopNFlowQueryTable
endDate={to}
flowTarget={FlowTargetSourceDest.destination}
filterQuery={filterQuery}
ip={ip}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
setQuery={setQuery}
indexPattern={indexPattern}
/>
</EuiFlexItem>
</ConditionalFlexGroup>
<EuiFlexItem>
<NetworkTopNFlowQueryTable
endDate={to}
flowTarget={FlowTargetSourceDest.destination}
filterQuery={filterQuery}
ip={ip}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
setQuery={setQuery}
indexPattern={indexPattern}
/>
</EuiFlexItem>
</ConditionalFlexGroup>
<EuiSpacer />
<EuiSpacer />
<ConditionalFlexGroup direction="column">
<EuiFlexItem>
<NetworkTopCountriesQueryTable
endDate={to}
filterQuery={filterQuery}
flowTarget={FlowTargetSourceDest.source}
ip={ip}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
setQuery={setQuery}
indexPattern={indexPattern}
/>
</EuiFlexItem>
<ConditionalFlexGroup direction="column">
<EuiFlexItem>
<NetworkTopCountriesQueryTable
endDate={to}
filterQuery={filterQuery}
flowTarget={FlowTargetSourceDest.source}
ip={ip}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
setQuery={setQuery}
indexPattern={indexPattern}
/>
</EuiFlexItem>
<EuiFlexItem>
<NetworkTopCountriesQueryTable
endDate={to}
flowTarget={FlowTargetSourceDest.destination}
filterQuery={filterQuery}
ip={ip}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
setQuery={setQuery}
indexPattern={indexPattern}
/>
</EuiFlexItem>
</ConditionalFlexGroup>
<EuiFlexItem>
<NetworkTopCountriesQueryTable
endDate={to}
flowTarget={FlowTargetSourceDest.destination}
filterQuery={filterQuery}
ip={ip}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
setQuery={setQuery}
indexPattern={indexPattern}
/>
</EuiFlexItem>
</ConditionalFlexGroup>
<EuiSpacer />
<EuiSpacer />
<UsersQueryTable
endDate={to}
filterQuery={filterQuery}
flowTarget={flowTarget}
ip={ip}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
setQuery={setQuery}
/>
<UsersQueryTable
endDate={to}
filterQuery={filterQuery}
flowTarget={flowTarget}
ip={ip}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
setQuery={setQuery}
/>
<EuiSpacer />
<EuiSpacer />
<NetworkHttpQueryTable
endDate={to}
filterQuery={filterQuery}
ip={ip}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
setQuery={setQuery}
/>
<NetworkHttpQueryTable
endDate={to}
filterQuery={filterQuery}
ip={ip}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
setQuery={setQuery}
/>
<EuiSpacer />
<EuiSpacer />
<TlsQueryTable
endDate={to}
filterQuery={filterQuery}
flowTarget={(flowTarget as unknown) as FlowTargetSourceDest}
ip={ip}
setQuery={setQuery}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
/>
<TlsQueryTable
endDate={to}
filterQuery={filterQuery}
flowTarget={(flowTarget as unknown) as FlowTargetSourceDest}
ip={ip}
setQuery={setQuery}
skip={isInitializing}
startDate={from}
type={networkModel.NetworkType.details}
/>
<EuiSpacer />
<EuiSpacer />
<AnomaliesNetworkTable
startDate={from}
endDate={to}
skip={isInitializing}
ip={ip}
type={networkModel.NetworkType.details}
flowTarget={flowTarget}
narrowDateRange={narrowDateRange}
/>
<AnomaliesNetworkTable
startDate={from}
endDate={to}
skip={isInitializing}
ip={ip}
type={networkModel.NetworkType.details}
flowTarget={flowTarget}
narrowDateRange={narrowDateRange}
/>
</WrapperPage>
</StickyContainer>
) : (
<>
<HeaderPage title={ip} />
<WrapperPage>
<HeaderPage border title={ip} />
<NetworkEmptyPage />
</>
</WrapperPage>
);
}}
</WithSource>
<SpyRoute />
</>
);
}
);
IPDetailsComponent.displayName = 'IPDetailsComponent';
const makeMapStateToProps = () => {

View file

@ -17,6 +17,7 @@ import { SiemNavigation } from '../../components/navigation';
import { manageQuery } from '../../components/page/manage_query';
import { KpiNetworkComponent } from '../../components/page/network';
import { SiemSearchBar } from '../../components/search_bar';
import { WrapperPage } from '../../components/wrapper_page';
import { KpiNetworkQuery } from '../../containers/kpi_network';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
import { LastEventIndexKey } from '../../graphql/types';
@ -48,6 +49,7 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
capabilitiesFetched,
}) => {
const core = useKibanaCore();
return (
<>
<WithSource sourceId={sourceId}>
@ -58,95 +60,95 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
queries: [query],
filters,
});
return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<StickyContainer>
<FiltersGlobal>
<SiemSearchBar indexPattern={indexPattern} id="global" />
</FiltersGlobal>
<HeaderPage
subtitle={<LastEventTime indexKey={LastEventIndexKey.network} />}
title={i18n.PAGE_TITLE}
/>
<WrapperPage>
<HeaderPage
border
subtitle={<LastEventTime indexKey={LastEventIndexKey.network} />}
title={i18n.PAGE_TITLE}
/>
<EmbeddedMap
query={query}
filters={filters}
startDate={from}
endDate={to}
setQuery={setQuery}
/>
<EmbeddedMap
query={query}
filters={filters}
startDate={from}
endDate={to}
setQuery={setQuery}
/>
<EuiSpacer />
<EuiSpacer />
<KpiNetworkQuery
endDate={to}
filterQuery={filterQuery}
skip={isInitializing}
sourceId={sourceId}
startDate={from}
>
{({ kpiNetwork, loading, id, inspect, refetch }) => (
<KpiNetworkComponentManage
id={id}
inspect={inspect}
setQuery={setQuery}
refetch={refetch}
data={kpiNetwork}
loading={loading}
from={from}
to={to}
narrowDateRange={(min: number, max: number) => {
setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
}}
/>
<KpiNetworkQuery
endDate={to}
filterQuery={filterQuery}
skip={isInitializing}
sourceId={sourceId}
startDate={from}
>
{({ kpiNetwork, loading, id, inspect, refetch }) => (
<KpiNetworkComponentManage
id={id}
inspect={inspect}
setQuery={setQuery}
refetch={refetch}
data={kpiNetwork}
loading={loading}
from={from}
to={to}
narrowDateRange={(min: number, max: number) => {
setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
}}
/>
)}
</KpiNetworkQuery>
{capabilitiesFetched && !isInitializing ? (
<>
<EuiSpacer />
<SiemNavigation navTabs={navTabsNetwork(hasMlUserPermissions)} />
<EuiSpacer />
<NetworkRoutes
to={to}
filterQuery={filterQuery}
isInitializing={isInitializing}
from={from}
type={networkModel.NetworkType.page}
indexPattern={indexPattern}
setQuery={setQuery}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker}
networkPagePath={networkPagePath}
/>
</>
) : (
<NetworkRoutesLoading />
)}
</KpiNetworkQuery>
{capabilitiesFetched && !isInitializing ? (
<>
<EuiSpacer />
<SiemNavigation
navTabs={navTabsNetwork(hasMlUserPermissions)}
display={sourceId}
showBorder={true}
/>
<EuiSpacer />
<NetworkRoutes
to={to}
filterQuery={filterQuery}
isInitializing={isInitializing}
from={from}
type={networkModel.NetworkType.page}
indexPattern={indexPattern}
setQuery={setQuery}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker}
networkPagePath={networkPagePath}
/>
</>
) : (
<NetworkRoutesLoading />
)}
<EuiSpacer />
<EuiSpacer />
</WrapperPage>
</StickyContainer>
) : (
<>
<HeaderPage title={i18n.PAGE_TITLE} />
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<NetworkEmptyPage />
</>
</WrapperPage>
);
}}
</WithSource>
<SpyRoute />
</>
);
}
);
NetworkComponent.displayName = 'NetworkComponent';
const makeMapStateToProps = () => {

View file

@ -7,68 +7,72 @@
import { EuiFlexGroup } from '@elastic/eui';
import moment from 'moment';
import React from 'react';
import { pure } from 'recompose';
import chrome from 'ui/chrome';
import { documentationLinks } from 'ui/documentation_links';
import { EmptyPage } from '../../components/empty_page';
import { HeaderPage } from '../../components/header_page';
import { OverviewHost } from '../../components/page/overview/overview_host';
import { OverviewNetwork } from '../../components/page/overview/overview_network';
import { WrapperPage } from '../../components/wrapper_page';
import { GlobalTime } from '../../containers/global_time';
import { Summary } from './summary';
import { EmptyPage } from '../../components/empty_page';
import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source';
import { SpyRoute } from '../../utils/route/spy_routes';
import { Summary } from './summary';
import * as i18n from './translations';
const basePath = chrome.getBasePath();
export const OverviewComponent = pure(() => {
export const OverviewComponent = React.memo(() => {
const dateEnd = Date.now();
const dateRange = moment.duration(24, 'hours').asMilliseconds();
const dateStart = dateEnd - dateRange;
return (
<>
<HeaderPage
badgeLabel={i18n.PAGE_BADGE_LABEL}
badgeTooltip={i18n.PAGE_BADGE_TOOLTIP}
subtitle={i18n.PAGE_SUBTITLE}
title={i18n.PAGE_TITLE}
/>
<WrapperPage>
<HeaderPage
badgeOptions={{
beta: true,
text: i18n.PAGE_BADGE_LABEL,
tooltip: i18n.PAGE_BADGE_TOOLTIP,
}}
border
subtitle={i18n.PAGE_SUBTITLE}
title={i18n.PAGE_TITLE}
/>
<WithSource sourceId="default">
{({ indicesExist }) =>
indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<GlobalTime>
{({ setQuery }) => (
<EuiFlexGroup>
<Summary />
<OverviewHost endDate={dateEnd} startDate={dateStart} setQuery={setQuery} />
<OverviewNetwork endDate={dateEnd} startDate={dateStart} setQuery={setQuery} />
</EuiFlexGroup>
)}
</GlobalTime>
) : (
<EmptyPage
actionPrimaryIcon="gear"
actionPrimaryLabel={i18n.EMPTY_ACTION_PRIMARY}
actionPrimaryUrl={`${basePath}/app/kibana#/home/tutorial_directory/siem`}
actionSecondaryIcon="popout"
actionSecondaryLabel={i18n.EMPTY_ACTION_SECONDARY}
actionSecondaryTarget="_blank"
actionSecondaryUrl={documentationLinks.siem}
data-test-subj="empty-page"
title={i18n.EMPTY_TITLE}
/>
)
}
</WithSource>
</WrapperPage>
<WithSource sourceId="default">
{({ indicesExist }) =>
indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<GlobalTime>
{({ setQuery }) => (
<EuiFlexGroup>
<Summary />
<OverviewHost endDate={dateEnd} startDate={dateStart} setQuery={setQuery} />
<OverviewNetwork endDate={dateEnd} startDate={dateStart} setQuery={setQuery} />
</EuiFlexGroup>
)}
</GlobalTime>
) : (
<EmptyPage
actionPrimaryIcon="gear"
actionPrimaryLabel={i18n.EMPTY_ACTION_PRIMARY}
actionPrimaryUrl={`${basePath}/app/kibana#/home/tutorial_directory/siem`}
actionSecondaryIcon="popout"
actionSecondaryLabel={i18n.EMPTY_ACTION_SECONDARY}
actionSecondaryTarget="_blank"
actionSecondaryUrl={documentationLinks.siem}
data-test-subj="empty-page"
title={i18n.EMPTY_TITLE}
/>
)
}
</WithSource>
<SpyRoute />
</>
);
});
OverviewComponent.displayName = 'OverviewComponent';

View file

@ -10,14 +10,13 @@ import styled from 'styled-components';
import { HeaderPage } from '../../components/header_page';
import { StatefulOpenTimeline } from '../../components/open_timeline';
import { WrapperPage } from '../../components/wrapper_page';
import { SpyRoute } from '../../utils/route/spy_routes';
import * as i18n from './translations';
const TimelinesContainer = styled.div`
width: 100%:
`;
TimelinesContainer.displayName = 'TimelinesContainer';
interface TimelinesProps<TCache = object> {
@ -30,16 +29,19 @@ export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10;
export const TimelinesPage = React.memo<OwnProps>(({ apolloClient }) => (
<>
<HeaderPage title={i18n.PAGE_TITLE} />
<WrapperPage>
<HeaderPage border title={i18n.PAGE_TITLE} />
<TimelinesContainer>
<StatefulOpenTimeline
apolloClient={apolloClient}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
isModal={false}
title={i18n.ALL_TIMELINES_PANEL_TITLE}
/>
</TimelinesContainer>
</WrapperPage>
<TimelinesContainer>
<StatefulOpenTimeline
apolloClient={apolloClient}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
isModal={false}
title={i18n.ALL_TIMELINES_PANEL_TITLE}
/>
</TimelinesContainer>
<SpyRoute />
</>
));

View file

@ -10511,7 +10511,6 @@
"xpack.siem.formatted.duration.noDurationTooltip": "期間がありません",
"xpack.siem.formatted.duration.zeroNanosecondsTooltip": "0ナ秒",
"xpack.siem.formattedDuration.tooltipLabel": "生",
"xpack.siem.global.addData": "データの投入",
"xpack.siem.headerPage.pageSubtitle": "前回のイベント: {beat}",
"xpack.siem.host.details.architectureLabel": "アーキテクチャー",
"xpack.siem.host.details.firstSeenTitle": "初回の認識",

View file

@ -10601,7 +10601,6 @@
"xpack.siem.formatted.duration.noDurationTooltip": "无持续时间",
"xpack.siem.formatted.duration.zeroNanosecondsTooltip": "零纳秒",
"xpack.siem.formattedDuration.tooltipLabel": "原始",
"xpack.siem.global.addData": "添加数据",
"xpack.siem.headerPage.pageSubtitle": "最后事件:{beat}",
"xpack.siem.host.details.architectureLabel": "架构",
"xpack.siem.host.details.firstSeenTitle": "首次看到时间",