Siem Global Navigation Updates (#36372)

* use new condensed tabs

* pull button out of settings menu

* clean up global nav styles

* account for dark mode

* remove unnecessary styles

* move kql bar to relevant location

* tweak page header styles

* add border prop to HeaderPanel

* placeholder text updates

* fiddling with direction control visuals

* move kql back above kpis, per tudor

* change host table name to “all hosts”

* change direction select and tests (TY Garrett)

* rm console.log

* rm `EuiHorizontalRule` import

* sticky poc

* apply poc to all relevant pages

* general cleanup

* update snapshots

* Override EuiSuperDatePicker spacing bug

* restrict overview beats data to the last 24 hours

* garrett & tudor’s suggested changes

* overview data refresh on render, per garrett

* update tests and snapshots

* remove sticky at small bp and update tests
This commit is contained in:
Michael Marcialis 2019-05-10 19:28:16 -04:00 committed by GitHub
parent ee1335b464
commit f7519aaf95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 882 additions and 1147 deletions

View file

@ -12,14 +12,16 @@
"@types/color": "^3.0.0",
"@types/lodash": "^4.14.110",
"@types/memoize-one": "^4.1.0",
"@types/react-beautiful-dnd": "^10.0.1"
"@types/react-beautiful-dnd": "^10.0.1",
"@types/react-sticky": "^6.0.3"
},
"dependencies": {
"react-beautiful-dnd": "^10.0.1",
"react-markdown": "^4.0.6",
"apollo-link-error": "^1.1.7",
"lodash": "^4.17.10",
"memoize-one": "^5.0.0",
"apollo-link-error": "^1.1.7",
"react-beautiful-dnd": "^10.0.1",
"react-markdown": "^4.0.6",
"react-sticky": "^6.0.3",
"suricata-sid-db": "^1.0.2"
}
}

View file

@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AppSettingsPopover rendering it renders against snapshot 1`] = `
<Component
onClick={[Function]}
onClose={[Function]}
showPopover={false}
/>
`;

View file

@ -1,49 +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 { mount, shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import { noop } from 'lodash/fp';
import * as React from 'react';
import { AppSettingsPopover } from './app_settings_popover';
describe('AppSettingsPopover', () => {
describe('rendering', () => {
test('it renders against snapshot', () => {
const wrapper = shallow(
<AppSettingsPopover onClick={noop} onClose={noop} showPopover={false} />
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('it renders a settings gear icon', () => {
const wrapper = mount(
<AppSettingsPopover onClick={noop} onClose={noop} showPopover={false} />
);
expect(wrapper.find('[data-test-subj="gear"]').exists()).toEqual(true);
});
});
describe('onClick', () => {
test('it invokes onClick when clicked', () => {
const onClick = jest.fn();
const wrapper = mount(
<AppSettingsPopover onClick={onClick} onClose={noop} showPopover={false} />
);
wrapper
.find('[data-test-subj="gear"]')
.first()
.simulate('click');
expect(onClick).toHaveBeenCalled();
});
});
});

View file

@ -1,45 +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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPopover } from '@elastic/eui';
import * as React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
import * as i18n from './translations';
interface Props {
showPopover: boolean;
onClick: () => void;
onClose: () => void;
}
const SettingsPopover = styled(EuiPopover)`
cursor: pointer;
`;
export const AppSettingsPopover = pure<Props>(({ showPopover, onClick, onClose }) => (
<SettingsPopover
anchorPosition="downRight"
button={<EuiIcon data-test-subj="gear" type="gear" size="l" onClick={onClick} />}
closePopover={onClose}
data-test-subj="app-settings-popover"
id="timelineSettingsPopover"
isOpen={showPopover}
>
<EuiFlexGroup direction="column" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="add-data"
href="kibana#home/tutorial_directory/security"
target="_blank"
>
{i18n.ADD_DATA}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</SettingsPopover>
));

View file

@ -1,41 +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 * as React from 'react';
import { AppSettingsPopover } from './app_settings_popover';
export interface State {
showPopover: boolean;
}
export class AppSettings extends React.PureComponent<{}, State> {
public readonly state = {
showPopover: false,
};
public render() {
return (
<AppSettingsPopover
onClick={this.onClick}
onClose={this.onClose}
showPopover={this.state.showPopover}
/>
);
}
private onClick = () => {
this.setState({
showPopover: !this.state.showPopover,
});
};
private onClose = () => {
this.setState({
showPopover: false,
});
};
}

View file

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rendering renders correctly 1`] = `
<Component>
<p>
Additional filters here.
</p>
</Component>
`;

View file

@ -0,0 +1,23 @@
/*
* 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/match_media';
import { FiltersGlobal } from './index';
describe('rendering', () => {
test('renders correctly', () => {
const wrapper = shallow(
<FiltersGlobal>
<p>Additional filters here.</p>
</FiltersGlobal>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
});

View file

@ -0,0 +1,71 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import React from 'react';
import { Sticky } from 'react-sticky';
import { pure } from 'recompose';
import styled from 'styled-components';
import { SuperDatePicker } from '../super_date_picker';
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 }>`
${props => `
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
};
${props.isSticky &&
`
top: ${offsetChrome}px !important;
z-index: ${props.theme.eui.euiZNavigation};
`}
@media only ${disableSticky} {
position: static !important;
z-index: ${props.theme.eui.euiZContent} !important;
}
`}
`;
// Temporary fix for EuiSuperDatePicker whitespace bug and auto width - Michael Marcialis
const FlexItemWithDatePickerFix = styled(EuiFlexItem)`
.euiSuperDatePicker__flexWrapper {
max-width: none;
width: auto;
}
`;
export interface FiltersGlobalProps {
children: React.ReactNode;
}
export const FiltersGlobal = pure<FiltersGlobalProps>(({ children }) => (
<Sticky disableCompensation={disableStickyMq.matches} topOffset={-offsetChrome}>
{({ style, isSticky }) => (
<Aside isSticky={isSticky} style={style}>
<EuiFlexGroup>
<EuiFlexItem grow={8}>{children}</EuiFlexItem>
<FlexItemWithDatePickerFix grow={4}>
<SuperDatePicker id="global" />
</FlexItemWithDatePickerFix>
</EuiFlexGroup>
</Aside>
)}
</Sticky>
));

View file

@ -0,0 +1,7 @@
/*
* 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 { FiltersGlobal } from './filters_global';

View file

@ -2,7 +2,6 @@
exports[`Select Flow Direction rendering it renders the basic group button for uni-direction and bi-direction 1`] = `
<Component
id="TestFlowDirectionId"
onChangeDirection={[MockFunction]}
selectedDirection="uniDirectional"
/>

View file

@ -20,7 +20,6 @@ describe('Select Flow Direction', () => {
test('it renders the basic group button for uni-direction and bi-direction', () => {
const wrapper = shallow(
<FlowDirectionSelect
id={TestFlowDirectionId}
selectedDirection={FlowDirection.uniDirectional}
onChangeDirection={mockOnChange}
/>
@ -40,22 +39,18 @@ describe('Select Flow Direction', () => {
};
const wrapper = mount(
<FlowDirectionSelect
id={TestFlowDirectionId}
selectedDirection={FlowDirection.uniDirectional}
onChangeDirection={mockOnChange}
/>
);
wrapper
.find('input[value="biDirectional"]')
.find(`[data-test-subj="${FlowDirection.biDirectional}"]`)
.first()
.simulate('change', event);
.simulate('click', event);
wrapper.update();
expect(mockOnChange.mock.calls[0]).toEqual([
`${TestFlowDirectionId}-select-flow-direction-biDirectional`,
'biDirectional',
]);
expect(mockOnChange.mock.calls[0]).toEqual(['biDirectional']);
});
});
});

View file

@ -4,49 +4,36 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButtonGroup, EuiButtonGroupProps } from '@elastic/eui';
import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui';
import React from 'react';
import { pure } from 'recompose';
import { pure } from 'recompose';
import { FlowDirection } from '../../graphql/types';
import * as i18n from './translations';
type MyEuiButtonGroupProps = Pick<
EuiButtonGroupProps,
'options' | 'idSelected' | 'onChange' | 'color' | 'type'
> & {
name?: string;
};
const MyEuiButtonGroup: React.FC<MyEuiButtonGroupProps> = EuiButtonGroup;
const getToggleButtonDirection = (id: string) => [
{
id: `${id}-select-flow-direction-${FlowDirection.uniDirectional}`,
label: i18n.UNIDIRECTIONAL,
value: FlowDirection.uniDirectional,
},
{
id: `${id}-select-flow-direction-${FlowDirection.biDirectional}`,
label: i18n.BIDIRECTIONAL,
value: FlowDirection.biDirectional,
},
];
interface Props {
id: string;
selectedDirection: FlowDirection;
onChangeDirection: (id: string, value: FlowDirection) => void;
onChangeDirection: (value: FlowDirection) => void;
}
export const FlowDirectionSelect = pure<Props>(({ id, onChangeDirection, selectedDirection }) => (
<MyEuiButtonGroup
name={`${id}-${selectedDirection}`}
options={getToggleButtonDirection(id)}
idSelected={`${id}-select-flow-direction-${selectedDirection}`}
onChange={onChangeDirection}
color="primary"
type="single"
/>
export const FlowDirectionSelect = pure<Props>(({ onChangeDirection, selectedDirection }) => (
<EuiFilterGroup>
<EuiFilterButton
withNext
hasActiveFilters={selectedDirection === FlowDirection.uniDirectional}
onClick={() => onChangeDirection(FlowDirection.uniDirectional)}
data-test-subj={FlowDirection.uniDirectional}
>
{i18n.UNIDIRECTIONAL}
</EuiFilterButton>
<EuiFilterButton
hasActiveFilters={selectedDirection === FlowDirection.biDirectional}
onClick={() => onChangeDirection(FlowDirection.biDirectional)}
data-test-subj={FlowDirection.biDirectional}
>
{i18n.BIDIRECTIONAL}
</EuiFilterButton>
</EuiFilterGroup>
));

View file

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

View file

@ -0,0 +1,22 @@
/*
* 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 subtitle="My Test Subtitle" title="My Test Title">
<p>My test supplement.</p>
</HeaderPage>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
});

View file

@ -0,0 +1,42 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
const Header = styled.header`
${({ theme }) => `
border-bottom: ${theme.eui.euiBorderThin};
padding-bottom: ${theme.eui.euiSizeL};
margin: ${theme.eui.euiSizeL} 0;
`}
`;
export interface HeaderPageProps {
children?: React.ReactNode;
subtitle?: string | React.ReactNode;
title: string | React.ReactNode;
}
export const HeaderPage = pure<HeaderPageProps>(({ children, subtitle, title }) => (
<Header>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle size="l">
<h1 data-test-subj="page_headline_title">{title}</h1>
</EuiTitle>
<EuiText color="subdued" size="s">
{subtitle}
</EuiText>
</EuiFlexItem>
{children && <EuiFlexItem grow={false}>{children}</EuiFlexItem>}
</EuiFlexGroup>
</Header>
));

View file

@ -0,0 +1,7 @@
/*
* 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 { HeaderPage } from './header_page';

View file

@ -9,32 +9,45 @@ import React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
const Header = styled.header<{ border?: boolean }>`
${props => `
margin-bottom: ${props.theme.eui.euiSizeL};
${props.border &&
`
border-bottom: ${props.theme.eui.euiBorderThin};
padding-bottom: ${props.theme.eui.euiSizeL};
`}
`}
`;
export interface HeaderPanelProps {
border?: boolean;
children?: React.ReactNode;
subtitle?: string | React.ReactNode;
title: string | React.ReactNode;
tooltip?: string;
}
export const HeaderPanel = pure<HeaderPanelProps>(({ children, subtitle, title, tooltip }) => (
<FlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle>
<h2 data-test-subj="page_headline_title">
{title}{' '}
{tooltip && <EuiIconTip color="subdued" content={tooltip} position="top" size="l" />}
</h2>
</EuiTitle>
export const HeaderPanel = pure<HeaderPanelProps>(
({ border, children, subtitle, title, tooltip }) => (
<Header border={border}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle>
<h2 data-test-subj="page_headline_title">
{title}{' '}
{tooltip && <EuiIconTip color="subdued" content={tooltip} position="top" size="l" />}
</h2>
</EuiTitle>
<EuiText color="subdued" size="s">
{subtitle}
</EuiText>
</EuiFlexItem>
<EuiText color="subdued" size="s">
{subtitle}
</EuiText>
</EuiFlexItem>
{children && <EuiFlexItem grow={false}>{children}</EuiFlexItem>}
</FlexGroup>
));
const FlexGroup = styled(EuiFlexGroup)`
margin-bottom: 12px;
`;
{children && <EuiFlexItem grow={false}>{children}</EuiFlexItem>}
</EuiFlexGroup>
</Header>
)
);

View file

@ -1,195 +1,105 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Load More Table Component rendering it renders the default load more table 1`] = `
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="m"
>
<styled.div>
<pure(Component)
subtitle="Showing: 1 Test Unit"
title="Hosts"
tooltip="My test tooltip"
>
<p>
My test supplement.
</p>
</pure(Component)>
<Styled(EuiBasicTable)
columns={
Array [
Object {
"field": "node.host.name",
"hideForMobile": false,
"name": "Host",
"render": [Function],
"truncateText": false,
},
Object {
"field": "node.host.firstSeen",
"hideForMobile": false,
"name": "First seen",
"render": [Function],
"truncateText": false,
},
Object {
"field": "node.host.os",
"hideForMobile": false,
"name": "OS",
"render": [Function],
"truncateText": false,
},
Object {
"field": "node.host.version",
"hideForMobile": false,
"name": "Version",
"render": [Function],
"truncateText": false,
},
]
}
items={
Array [
Object {
"cursor": Object {
"value": "98966fa2013c396155c460d35c0902be",
},
"host": Object {
"_id": "cPsuhGcB0WOhS6qyTKC0",
"firstSeen": "2018-12-06T15:40:53.319Z",
"name": "elrond.elstc.co",
"os": "Ubuntu",
"version": "18.04.1 LTS (Bionic Beaver)",
},
},
Object {
"cursor": Object {
"value": "aa7ca589f1b8220002f2fc61c64cfbf1",
},
"host": Object {
"_id": "KwQDiWcB0WOhS6qyXmrW",
"firstSeen": "2018-12-07T14:12:38.560Z",
"name": "siem-kibana",
"os": "Debian GNU/Linux",
"version": "9 (stretch)",
},
},
]
}
onChange={[Function]}
sorting={null}
/>
<styled.div>
<EuiFlexGroup
alignItems="flexStart"
direction="row"
gutterSize="none"
justifyContent="flexStart"
>
<EuiFlexItem
grow={false}
>
<EuiPopover
anchorPosition="downCenter"
button={
<EuiButtonEmpty
color="text"
iconSide="right"
iconType="arrowDown"
onClick={[Function]}
size="s"
type="button"
>
Rows:
1
</EuiButtonEmpty>
}
closePopover={[Function]}
data-test-subj="loadingMoreSizeRowPopover"
hasArrow={true}
id="customizablePagination"
isOpen={false}
ownFocus={false}
panelPaddingSize="none"
>
<EuiContextMenuPanel
data-test-subj="loadingMorePickSizeRow"
hasFocus={true}
items={
Array [
<EuiContextMenuItem
icon="empty"
layoutAlign="center"
onClick={[Function]}
toolTipPosition="right"
>
2 rows
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
layoutAlign="center"
onClick={[Function]}
toolTipPosition="right"
>
5 rows
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
layoutAlign="center"
onClick={[Function]}
toolTipPosition="right"
>
10 rows
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
layoutAlign="center"
onClick={[Function]}
toolTipPosition="right"
>
20 rows
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
layoutAlign="center"
onClick={[Function]}
toolTipPosition="right"
>
50 rows
</EuiContextMenuItem>,
]
}
/>
</EuiPopover>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup
alignItems="flexStart"
direction="row"
gutterSize="none"
justifyContent="center"
>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="loadingMoreButton"
fill={false}
iconSide="left"
isLoading={false}
onClick={[Function]}
size="m"
type="button"
>
Load More
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</styled.div>
</styled.div>
</EuiPanel>
<LoadMoreTable
columns={
Array [
Object {
"field": "node.host.name",
"hideForMobile": false,
"name": "Host",
"render": [Function],
"truncateText": false,
},
Object {
"field": "node.host.firstSeen",
"hideForMobile": false,
"name": "First seen",
"render": [Function],
"truncateText": false,
},
Object {
"field": "node.host.os",
"hideForMobile": false,
"name": "OS",
"render": [Function],
"truncateText": false,
},
Object {
"field": "node.host.version",
"hideForMobile": false,
"name": "Version",
"render": [Function],
"truncateText": false,
},
]
}
hasNextPage={true}
headerCount={1}
headerSupplement={
<p>
My test supplement.
</p>
}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={
Array [
Object {
"numberOfRow": 2,
"text": "2 rows",
},
Object {
"numberOfRow": 5,
"text": "5 rows",
},
Object {
"numberOfRow": 10,
"text": "10 rows",
},
Object {
"numberOfRow": 20,
"text": "20 rows",
},
Object {
"numberOfRow": 50,
"text": "50 rows",
},
]
}
limit={1}
loadMore={[Function]}
loading={false}
loadingTitle="Hosts"
pageOfItems={
Array [
Object {
"cursor": Object {
"value": "98966fa2013c396155c460d35c0902be",
},
"host": Object {
"_id": "cPsuhGcB0WOhS6qyTKC0",
"firstSeen": "2018-12-06T15:40:53.319Z",
"name": "elrond.elstc.co",
"os": "Ubuntu",
"version": "18.04.1 LTS (Bionic Beaver)",
},
},
Object {
"cursor": Object {
"value": "aa7ca589f1b8220002f2fc61c64cfbf1",
},
"host": Object {
"_id": "KwQDiWcB0WOhS6qyXmrW",
"firstSeen": "2018-12-07T14:12:38.560Z",
"name": "siem-kibana",
"os": "Debian GNU/Linux",
"version": "9 (stretch)",
},
},
]
}
updateLimitPagination={[Function]}
/>
`;

View file

@ -12,29 +12,34 @@ import { Direction } from '../../graphql/types';
import { LoadMoreTable } from './index';
import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { ThemeProvider } from 'styled-components';
describe('Load More Table Component', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
const loadMore = jest.fn();
const updateLimitPagination = jest.fn();
describe('rendering', () => {
test('it renders the default load more table', () => {
const wrapper = shallow(
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={1}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
<ThemeProvider theme={theme}>
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={1}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
</ThemeProvider>
);
expect(toJson(wrapper)).toMatchSnapshot();
@ -42,22 +47,24 @@ describe('Load More Table Component', () => {
test('it renders the loading panel at the beginning ', () => {
const wrapper = mount(
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={1}
loading={true}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={[]}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
<ThemeProvider theme={theme}>
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={1}
loading={true}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={[]}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
</ThemeProvider>
);
expect(
@ -67,47 +74,49 @@ describe('Load More Table Component', () => {
test('it renders the over loading panel after data has been in the table ', () => {
const wrapper = mount(
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={1}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
<ThemeProvider theme={theme}>
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={1}
loading={true}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
</ThemeProvider>
);
wrapper.setState({ isEmptyTable: false });
wrapper.setProps({ loading: true });
expect(wrapper.find('[data-test-subj="LoadingPanelLoadMoreTable"]').exists()).toBeTruthy();
});
test('it renders the loadMore button if need to fetch more', () => {
const wrapper = mount(
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={1}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
<ThemeProvider theme={theme}>
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={1}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
</ThemeProvider>
);
expect(
@ -120,26 +129,26 @@ describe('Load More Table Component', () => {
test('it renders the Loading... in the more load button when fetching new data', () => {
const wrapper = mount(
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={1}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
<ThemeProvider theme={theme}>
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={1}
loading={true}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
</ThemeProvider>
);
wrapper.setProps({ loading: true });
expect(
wrapper.find('[data-test-subj="InitialLoadingPanelLoadMoreTable"]').exists()
).toBeFalsy();
@ -148,27 +157,29 @@ describe('Load More Table Component', () => {
.find('[data-test-subj="loadingMoreButton"]')
.first()
.text()
).toContain('Loading...');
).toContain('Loading');
});
test('it does NOT render the loadMore button because there is nothing else to fetch', () => {
const wrapper = mount(
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={false}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={2}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
<ThemeProvider theme={theme}>
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={false}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={2}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="loadingMoreButton"]').exists()).toBeFalsy();
@ -176,22 +187,24 @@ describe('Load More Table Component', () => {
test('it render popover to select new limit in table', () => {
const wrapper = mount(
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={true}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={2}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
<ThemeProvider theme={theme}>
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={true}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={2}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
</ThemeProvider>
);
wrapper
@ -203,22 +216,24 @@ describe('Load More Table Component', () => {
test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => {
const wrapper = mount(
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={true}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={[]}
limit={2}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
<ThemeProvider theme={theme}>
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={true}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={[]}
limit={2}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy();
@ -227,24 +242,26 @@ describe('Load More Table Component', () => {
test('It should render a sort icon if sorting is defined', () => {
const mockOnChange = jest.fn();
const wrapper = mount(
<LoadMoreTable
columns={sortedHosts}
hasNextPage={true}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={2}
loading={false}
loadingTitle="Hosts"
loadMore={jest.fn()}
onChange={mockOnChange}
pageOfItems={mockData.Hosts.edges}
sorting={{ direction: Direction.asc, field: 'node.host.name' }}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
<ThemeProvider theme={theme}>
<LoadMoreTable
columns={sortedHosts}
hasNextPage={true}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={2}
loading={false}
loadingTitle="Hosts"
loadMore={jest.fn()}
onChange={mockOnChange}
pageOfItems={mockData.Hosts.edges}
sorting={{ direction: Direction.asc, field: 'node.host.name' }}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
</ThemeProvider>
);
expect(wrapper.find('.euiTable thead tr th button svg')).toBeTruthy();
@ -254,22 +271,24 @@ describe('Load More Table Component', () => {
describe('Events', () => {
test('should call loadmore when clicking on the button load more', () => {
const wrapper = mount(
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={1}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
<ThemeProvider theme={theme}>
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={1}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
</ThemeProvider>
);
wrapper
@ -282,22 +301,24 @@ describe('Load More Table Component', () => {
test('Should call updateLimitPagination when you pick a new limit', () => {
const wrapper = mount(
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={true}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={2}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
<ThemeProvider theme={theme}>
<LoadMoreTable
columns={getHostsColumns()}
hasNextPage={true}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={2}
loading={false}
loadingTitle="Hosts"
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
pageOfItems={mockData.Hosts.edges}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
</ThemeProvider>
);
wrapper
@ -315,24 +336,26 @@ describe('Load More Table Component', () => {
test('Should call onChange when you choose a new sort in the table', () => {
const mockOnChange = jest.fn();
const wrapper = mount(
<LoadMoreTable
columns={sortedHosts}
hasNextPage={true}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={2}
loading={false}
loadingTitle="Hosts"
loadMore={jest.fn()}
onChange={mockOnChange}
pageOfItems={mockData.Hosts.edges}
sorting={{ direction: Direction.asc, field: 'node.host.name' }}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
<ThemeProvider theme={theme}>
<LoadMoreTable
columns={sortedHosts}
hasNextPage={true}
headerCount={1}
headerSupplement={<p>My test supplement.</p>}
headerTitle="Hosts"
headerTooltip="My test tooltip"
headerUnit="Test Unit"
itemsPerRow={rowItems}
limit={2}
loading={false}
loadingTitle="Hosts"
loadMore={jest.fn()}
onChange={mockOnChange}
pageOfItems={mockData.Hosts.edges}
sorting={{ direction: Direction.asc, field: 'node.host.name' }}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
/>
</ThemeProvider>
);
wrapper

View file

@ -267,7 +267,7 @@ export class LoadMoreTable<T, U, V, W, X, Y, Z, AA, AB> extends React.PureCompon
isLoading={loading}
onClick={this.props.loadMore}
>
{loading ? `${i18n.LOADING}...` : i18n.LOAD_MORE}
{loading ? `${i18n.LOADING}` : i18n.LOAD_MORE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
@ -296,10 +296,6 @@ export class LoadMoreTable<T, U, V, W, X, Y, Z, AA, AB> extends React.PureCompon
}
export const BasicTableContainer = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
height: auto;
position: relative;
`;

View file

@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
export const LOADING = i18n.translate('xpack.siem.loadingMoreTable.loadingDescription', {
defaultMessage: 'Loading',
defaultMessage: 'Loading',
});
export const LOAD_MORE = i18n.translate('xpack.siem.loadingMoreTable.loadMoreDescription', {

View file

@ -5,6 +5,7 @@
*/
import chrome from 'ui/chrome';
import '../../../mock/match_media';
import { encodeIpv6 } from '../../../lib/helpers';
import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/host_details';
import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details';

View file

@ -5,7 +5,6 @@
*/
import { EuiTab, EuiTabs } from '@elastic/eui';
import * as React from 'react';
import styled from 'styled-components';
import { getHostsUrl, getNetworkUrl, getOverviewUrl, getTimelinesUrl } from '../../link_to';
@ -53,13 +52,6 @@ interface TabNavigationState {
selectedTabId: string;
}
const TabNavigationContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
margin-top: -8px;
`;
export class TabNavigation extends React.PureComponent<TabNavigationProps, TabNavigationState> {
constructor(props: TabNavigationProps) {
super(props);
@ -81,11 +73,7 @@ export class TabNavigation extends React.PureComponent<TabNavigationProps, TabNa
}
}
public render() {
return (
<TabNavigationContainer>
<EuiTabs>{this.renderTabs()}</EuiTabs>
</TabNavigationContainer>
);
return <EuiTabs display="condensed">{this.renderTabs()}</EuiTabs>;
}
public mapLocationToTab = (pathname: string) =>
@ -109,10 +97,10 @@ export class TabNavigation extends React.PureComponent<TabNavigationProps, TabNa
<EuiTab
data-href={tab.href}
data-test-subj={`navigation-${tab.id}`}
onClick={() => this.handleTabClick(tab.href, tab.id)}
isSelected={this.state.selectedTabId === tab.id}
disabled={tab.disabled}
isSelected={this.state.selectedTabId === tab.id}
key={`navigation-${tab.id}`}
onClick={() => this.handleTabClick(tab.href, tab.id)}
>
{tab.name}
</EuiTab>

View file

@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
export const HOSTS = i18n.translate('xpack.siem.hostsTable.hostsTitle', {
defaultMessage: 'Hosts',
defaultMessage: 'All Hosts',
});
export const UNIT = (totalCount: number) =>

View file

@ -116,7 +116,6 @@ class DomainsTableComponent extends React.PureComponent<DomainsTableProps> {
headerCount={totalCount}
headerSupplement={
<FlowDirectionSelect
id={DomainsTableId}
selectedDirection={flowDirection}
onChangeDirection={this.onChangeDomainsDirection}
/>
@ -154,7 +153,7 @@ class DomainsTableComponent extends React.PureComponent<DomainsTableProps> {
}
};
private onChangeDomainsDirection = (_: string, flowDirection: FlowDirection) =>
private onChangeDomainsDirection = (flowDirection: FlowDirection) =>
this.props.updateDomainsDirection({ flowDirection, networkType: this.props.type });
}

View file

@ -50,7 +50,7 @@ describe('NetworkTopNFlow Table Component', () => {
});
describe('Direction', () => {
test('when you click on the bi-direction button, it get selected', () => {
test('when you click on the bi-directional button, it get selected', () => {
const event = {
target: { name: 'direction', value: FlowDirection.biDirectional },
};
@ -73,16 +73,18 @@ describe('NetworkTopNFlow Table Component', () => {
);
wrapper
.find('input[value="biDirectional"]')
.find(`[data-test-subj="${FlowDirection.biDirectional}"]`)
.first()
.simulate('change', event);
.simulate('click', event);
wrapper.update();
expect(
wrapper
.find(`button#${NetworkTopNFlowTableId}-select-flow-direction-biDirectional`)
.hasClass('euiButton--fill')
.find(`[data-test-subj="${FlowDirection.biDirectional}"]`)
.first()
.render()
.hasClass('euiFilterButton-hasActiveFilters')
).toEqual(true);
});
});

View file

@ -148,7 +148,6 @@ class NetworkTopNFlowTableComponent extends React.PureComponent<NetworkTopNFlowT
<EuiFlexItem grow={false}>
<FlowDirectionSelect
id={NetworkTopNFlowTableId}
selectedDirection={flowDirection}
onChangeDirection={this.onChangeTopNFlowDirection}
/>
@ -189,7 +188,7 @@ class NetworkTopNFlowTableComponent extends React.PureComponent<NetworkTopNFlowT
}
};
private onChangeTopNFlowDirection = (_: string, flowDirection: FlowDirection) =>
private onChangeTopNFlowDirection = (flowDirection: FlowDirection) =>
this.props.updateTopNFlowDirection({ flowDirection, networkType: this.props.type });
}

View file

@ -4,19 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiPanel,
EuiTitle,
} from '@elastic/eui';
import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { pure } from 'recompose';
import { manageQuery } from '../../../../components/page/manage_query';
import { HeaderPanel } from '../../../header_panel';
import { manageQuery } from '../../../page/manage_query';
import { OverviewHostQuery } from '../../../../containers/overview/overview_host';
import { inputsModel } from '../../../../store/inputs';
import { OverviewHostStats } from '../overview_host_stats';
@ -34,26 +28,25 @@ type OverviewHostProps = OwnProps;
export const OverviewHost = pure<OverviewHostProps>(({ endDate, startDate, setQuery }) => (
<EuiFlexItem>
<EuiPanel>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle>
<h2>
<FormattedMessage
id="xpack.siem.overview.hostsTitle"
defaultMessage="Hosts Ingest Indices"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton href="#/link-to/hosts">
<FormattedMessage id="xpack.siem.overview.hostsAction" defaultMessage="View Hosts" />
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule />
<HeaderPanel
border
subtitle={
<FormattedMessage
id="xpack.siem.overview.hostsSubtitle"
defaultMessage="Showing: Last 24 Hours"
/>
}
title={
<FormattedMessage
id="xpack.siem.overview.hostsTitle"
defaultMessage="Host Beats Events"
/>
}
>
<EuiButton href="#/link-to/hosts">
<FormattedMessage id="xpack.siem.overview.hostsAction" defaultMessage="View Hosts" />
</EuiButton>
</HeaderPanel>
<OverviewHostQuery endDate={endDate} sourceId="default" startDate={startDate}>
{({ overviewHost, loading, id, refetch }) => (

View file

@ -4,19 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiPanel,
EuiTitle,
} from '@elastic/eui';
import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { pure } from 'recompose';
import { manageQuery } from '../../../../components/page/manage_query';
import { HeaderPanel } from '../../../header_panel';
import { manageQuery } from '../../../page/manage_query';
import { OverviewNetworkQuery } from '../../../../containers/overview/overview_network';
import { inputsModel } from '../../../../store/inputs';
import { OverviewNetworkStats } from '../overview_network_stats';
@ -34,29 +28,25 @@ const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats);
export const OverviewNetwork = pure<OwnProps>(({ endDate, startDate, setQuery }) => (
<EuiFlexItem>
<EuiPanel>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle>
<h2>
<FormattedMessage
id="xpack.siem.overview.networkTitle"
defaultMessage="Network Ingest Indices"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton href="#/link-to/network/">
<FormattedMessage
id="xpack.siem.overview.networkAction"
defaultMessage="View Network"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule />
<HeaderPanel
border
subtitle={
<FormattedMessage
id="xpack.siem.overview.networkSubtitle"
defaultMessage="Showing: Last 24 Hours"
/>
}
title={
<FormattedMessage
id="xpack.siem.overview.networkTitle"
defaultMessage="Network Beats Events"
/>
}
>
<EuiButton href="#/link-to/network/">
<FormattedMessage id="xpack.siem.overview.networkAction" defaultMessage="View Network" />
</EuiButton>
</HeaderPanel>
<OverviewNetworkQuery endDate={endDate} sourceId="default" startDate={startDate}>
{({ overviewNetwork, loading, id, refetch }) => (

View file

@ -1,37 +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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
export const FlexGroup = styled(EuiFlexGroup)`
margin-top: 120px;
`;
export interface PageHeadlineProps {
children?: React.ReactNode;
statType?: string;
subtitle?: string | React.ReactNode;
title: string | React.ReactNode;
}
export const PageHeadlineComponent = pure<PageHeadlineProps>(({ children, subtitle, title }) => (
<FlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle size="l">
<h1 data-test-subj="page_headline_title">{title}</h1>
</EuiTitle>
<EuiText color="subdued" size="s">
{subtitle}
</EuiText>
</EuiFlexItem>
{children && <EuiFlexItem grow={false}>{children}</EuiFlexItem>}
</FlexGroup>
));

View file

@ -1,123 +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 { get } from 'lodash/fp';
import * as React from 'react';
import { PageHeadlineComponents } from '.';
type Action = 'PUSH' | 'POP' | 'REPLACE';
const pop: Action = 'POP';
const getMockProps = (routePath: string) => {
const location = {
pathname: routePath,
search: '',
state: '',
hash: '',
};
return {
location,
match: {
isExact: true,
params: {},
path: '',
url: '',
},
history: {
length: 2,
location,
action: pop,
push: jest.fn(),
replace: jest.fn(),
go: jest.fn(),
goBack: jest.fn(),
goForward: jest.fn(),
block: jest.fn(),
createHref: jest.fn(),
listen: jest.fn(),
},
};
};
describe('SIEM Page Headline', () => {
test('Renders hosts headline', () => {
const mockProps = getMockProps('/hosts');
const wrapper = shallow(<PageHeadlineComponents {...mockProps} />);
const pageHeadline = wrapper
.dive()
.find('[data-test-subj="page_headline"]')
.props().title;
expect(get('props.defaultMessage', pageHeadline)).toEqual('Hosts');
});
test('Renders overview headline', () => {
const mockProps = getMockProps('/overview');
const wrapper = shallow(<PageHeadlineComponents {...mockProps} />);
const pageHeadline = wrapper
.dive()
.find('[data-test-subj="page_headline"]')
.props().title;
expect(get('props.defaultMessage', pageHeadline)).toEqual('SIEM');
});
test('Renders network headline', () => {
const mockProps = getMockProps('/network');
const wrapper = shallow(<PageHeadlineComponents {...mockProps} />);
const pageHeadline = wrapper
.dive()
.find('[data-test-subj="page_headline"]')
.props().title;
expect(get('props.defaultMessage', pageHeadline)).toEqual('Network');
});
test('Renders timelines headline', () => {
const mockProps = getMockProps('/timelines');
const wrapper = shallow(<PageHeadlineComponents {...mockProps} />);
const pageHeadline = wrapper
.dive()
.find('[data-test-subj="page_headline"]')
.props().title;
expect(get('props.defaultMessage', pageHeadline)).toEqual('Timelines');
});
test('Renders host details headline', () => {
const mockProps = getMockProps('/hosts/siem-kibana');
const wrapper = shallow(<PageHeadlineComponents {...mockProps} />);
const pageHeadline = wrapper
.dive()
.find('[data-test-subj="page_headline"]')
.props().title;
expect(pageHeadline).toEqual('siem-kibana');
});
test('Renders ip overview headline', () => {
const mockProps = getMockProps('/network/ip/123.45.678.10');
const wrapper = shallow(<PageHeadlineComponents {...mockProps} />);
const pageHeadline = wrapper
.dive()
.find('[data-test-subj="page_headline"]')
.props().title;
expect(pageHeadline).toEqual('123.45.678.10');
});
test('Wacky path renders overview headline', () => {
const mockProps = getMockProps('/boogity/woogity');
const wrapper = shallow(<PageHeadlineComponents {...mockProps} />);
const pageHeadline = wrapper
.dive()
.find('[data-test-subj="page_headline"]')
.props().title;
expect(get('props.defaultMessage', pageHeadline)).toEqual('SIEM');
});
});

View file

@ -1,91 +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 { EuiHorizontalRule } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { pure } from 'recompose';
import { LastEventIndexKey } from '../../graphql/types';
import { decodeIpv6 } from '../../lib/helpers';
import { FlowTargetSelectConnected } from '../page/network/flow_target_select_connected';
import { LastEventTime } from '../last_event_time';
import { PageHeadlineComponent } from './headline';
const overviewPageHeadline = {
subtitle: (
<FormattedMessage
id="xpack.siem.overview.pageSubtitle"
defaultMessage="Security Information & Event Management with the Elastic Stack"
/>
),
title: <FormattedMessage id="xpack.siem.overview.pageTitle" defaultMessage="SIEM" />,
};
export const getHeaderForRoute = (pathname: string) => {
const trailingPath = pathname.match(/.*\/(.*)$/);
if (trailingPath !== null) {
const pathSegment = trailingPath[1];
switch (pathSegment) {
case 'hosts': {
return {
subtitle: <LastEventTime indexKey={LastEventIndexKey.hosts} />,
title: <FormattedMessage id="xpack.siem.hosts.pageTitle" defaultMessage="Hosts" />,
};
}
case 'overview': {
return overviewPageHeadline;
}
case 'network': {
return {
subtitle: <LastEventTime indexKey={LastEventIndexKey.network} />,
title: <FormattedMessage id="xpack.siem.network.pageTitle" defaultMessage="Network" />,
};
}
case 'timelines': {
return {
subtitle: null,
title: (
<FormattedMessage id="xpack.siem.timelines.pageTitle" defaultMessage="Timelines" />
),
};
}
}
if (pathname.match(/hosts\/.*?/)) {
const hostId = pathSegment;
return {
subtitle: <LastEventTime indexKey={LastEventIndexKey.hostDetails} hostName={hostId} />,
title: hostId,
};
}
if (pathname.match(/network\/ip\/.*?/)) {
const ip = decodeIpv6(pathSegment);
return {
subtitle: <LastEventTime indexKey={LastEventIndexKey.ipDetails} ip={ip} />,
title: ip,
children: <FlowTargetSelectConnected />,
};
}
}
return overviewPageHeadline;
};
type PageHeadlineComponentProps = RouteComponentProps;
export const PageHeadlineComponents = pure<PageHeadlineComponentProps>(({ location }) => {
return (
<>
<PageHeadlineComponent
data-test-subj="page_headline"
{...getHeaderForRoute(location.pathname)}
/>
<EuiHorizontalRule />
</>
);
});
export const PageHeadline = withRouter(PageHeadlineComponents);

View file

@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const ADD_DATA = i18n.translate('xpack.siem.appSettings.addData', {
defaultMessage: 'Add Data',
});
export const THEME = i18n.translate('xpack.siem.appSettings.theme', {
defaultMessage: 'Theme',
window.matchMedia = jest.fn().mockImplementation(query => {
return {
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
};
});

View file

@ -4,29 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPageBody,
EuiPageHeader,
EuiPageHeaderSection,
} from '@elastic/eui';
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 chrome from 'ui/chrome';
import { AppSettings } from '../../components/app_settings';
import { AutoSizer } from '../../components/auto_sizer';
import { DragDropContextWrapper } from '../../components/drag_and_drop/drag_drop_context_wrapper';
import { Flyout, flyoutHeaderHeight } from '../../components/flyout';
import { HelpMenu } from '../../components/help_menu';
import { LinkToPage } from '../../components/link_to';
import { SiemNavigation } from '../../components/navigation';
import { PageHeadline } from '../../components/page_headline';
import { SuperDatePicker } from '../../components/super_date_picker';
import { StatefulTimeline } from '../../components/timeline';
import { NotFoundPage } from '../404';
import { HostsContainer } from '../hosts';
@ -38,6 +29,23 @@ const WrappedByAutoSizer = styled.div`
height: 100%;
`;
const gutterTimeline = '70px'; // Temporary until timeline is moved - MichaelMarcialis
const Page = styled(EuiPage)`
${({ theme }) => `
padding: 0 ${gutterTimeline} ${theme.eui.euiSizeL} ${theme.eui.euiSizeL};
`}
`;
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};
`}
`;
const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance)
/** Returns true if we are running with the k7 design */
@ -58,6 +66,8 @@ export const HomePage = pure(() => (
{({ measureRef, windowMeasurement: { height: windowHeight = 0 } }) => (
<WrappedByAutoSizer data-test-subj="wrapped-by-auto-sizer" innerRef={measureRef}>
<Page data-test-subj="pageContainer">
<HelpMenu />
<DragDropContextWrapper>
<Flyout
flyoutHeight={calculateFlyoutHeight({
@ -77,28 +87,26 @@ export const HomePage = pure(() => (
id="timeline-1"
/>
</Flyout>
<EuiPageBody>
<PageHeader data-test-subj="pageHeader">
<PageHeaderSection>
<FixEuiFlexGroup justifyContent="spaceBetween" alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<SiemNavigation />
<HelpMenu />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" wrap={false} gutterSize="s">
<EuiFlexItem grow={false} data-test-subj="datePickerContainer">
<SuperDatePicker id="global" />
</EuiFlexItem>
<EuiFlexItem grow={false} data-test-subj="appSettingsContainer">
<AppSettings />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</FixEuiFlexGroup>
</PageHeaderSection>
</PageHeader>
<PageHeadline />
<NavGlobal>
<EuiFlexGroup alignItems="center" gutterSize="m" justifyContent="spaceBetween">
<EuiFlexItem>
<SiemNavigation />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="add-data"
href="kibana#home/tutorial_directory/security"
iconType="plusInCircle"
>
<FormattedMessage id="xpack.siem.global.addData" defaultMessage="Add data" />
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</NavGlobal>
<Switch>
<Redirect from="/" exact={true} to="/overview" />
<Route path="/overview" component={Overview} />
@ -115,27 +123,3 @@ export const HomePage = pure(() => (
)}
</AutoSizer>
));
const Page = styled(EuiPage)`
padding: 0px 70px 24px 24px; // 70px temporary until timeline is moved - MichaelMarcialis
`;
const PageHeader = styled(EuiPageHeader)`
background-color: ${props => props.theme.eui.euiColorLightestShade};
position: fixed;
width: calc(100% - 75px);
z-index: 10;
padding: 6px 0px 6px 0px;
margin-bottom: 0px;
margin-left: -1px;
margin-top: -1px;
`;
const PageHeaderSection = styled(EuiPageHeaderSection)`
width: 100%;
user-select: none;
`;
const FixEuiFlexGroup = styled(EuiFlexGroup)`
margin-top: -6px;
`;

View file

@ -8,12 +8,16 @@ import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import { getOr, isEmpty } from 'lodash/fp';
import React from 'react';
import { connect } from 'react-redux';
import { StickyContainer } from 'react-sticky';
import { pure } from 'recompose';
import chrome, { Breadcrumb } from 'ui/chrome';
import { StaticIndexPattern } from 'ui/index_patterns';
import { ESTermQuery } from '../../../common/typed_json';
import { EmptyPage } from '../../components/empty_page';
import { FiltersGlobal } from '../../components/filters_global';
import { HeaderPage } from '../../components/header_page';
import { LastEventTime } from '../../components/last_event_time';
import { getHostsUrl, HostComponentProps } from '../../components/link_to/redirect_to_hosts';
import { EventsTable, UncommonProcessTable } from '../../components/page/hosts';
import { AuthenticationTable } from '../../components/page/hosts/authentications_table';
@ -25,7 +29,7 @@ import { GlobalTime } from '../../containers/global_time';
import { HostOverviewByNameQuery } from '../../containers/hosts/overview';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
import { UncommonProcessesQuery } from '../../containers/uncommon_processes';
import { IndexType } from '../../graphql/types';
import { IndexType, LastEventIndexKey } from '../../graphql/types';
import { convertKueryToElasticSearchQuery, escapeQueryValue } from '../../lib/keury';
import { hostsModel, hostsSelectors, State } from '../../store';
@ -58,8 +62,17 @@ const HostDetailsComponent = pure<HostDetailsComponentProps>(
<WithSource sourceId="default" indexTypes={indexTypes}>
{({ auditbeatIndicesExist, indexPattern }) =>
indicesExistOrDataTemporarilyUnavailable(auditbeatIndicesExist) ? (
<>
<HostsKql indexPattern={indexPattern} type={type} />
<StickyContainer>
<FiltersGlobal>
<HostsKql indexPattern={indexPattern} type={type} />
</FiltersGlobal>
<HeaderPage
subtitle={
<LastEventTime indexKey={LastEventIndexKey.hostDetails} hostName={hostName} />
}
title={hostName}
/>
<GlobalTime>
{({ to, from, setQuery }) => (
@ -81,9 +94,7 @@ const HostDetailsComponent = pure<HostDetailsComponentProps>(
)}
</HostOverviewByNameQuery>
<EuiSpacer size="s" />
<EuiHorizontalRule margin="xs" />
<EuiSpacer />
<EuiHorizontalRule />
<AuthenticationsQuery
sourceId="default"
@ -177,7 +188,7 @@ const HostDetailsComponent = pure<HostDetailsComponentProps>(
</>
)}
</GlobalTime>
</>
</StickyContainer>
) : (
<EmptyPage
title={i18n.NO_AUDITBEAT_INDICES}

View file

@ -7,6 +7,7 @@
import { mount } from 'enzyme';
import * as React from 'react';
import '../../mock/match_media';
import { Hosts } from './hosts';
import { mocksSource } from '../../containers/source/mock';

View file

@ -8,10 +8,14 @@ import { EuiSpacer } from '@elastic/eui';
import { getOr } from 'lodash/fp';
import React from 'react';
import { connect } from 'react-redux';
import { StickyContainer } from 'react-sticky';
import { pure } from 'recompose';
import chrome from 'ui/chrome';
import { EmptyPage } from '../../components/empty_page';
import { FiltersGlobal } from '../../components/filters_global';
import { HeaderPage } from '../../components/header_page';
import { LastEventTime } from '../../components/last_event_time';
import {
EventsTable,
HostsTable,
@ -27,7 +31,7 @@ import { HostsQuery } from '../../containers/hosts';
import { KpiHostsQuery } from '../../containers/kpi_hosts';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
import { UncommonProcessesQuery } from '../../containers/uncommon_processes';
import { IndexType } from '../../graphql/types';
import { IndexType, LastEventIndexKey } from '../../graphql/types';
import { hostsModel, hostsSelectors, State } from '../../store';
import { HostsKql } from './kql';
@ -52,8 +56,16 @@ const HostsComponent = pure<HostsComponentProps>(({ filterQuery }) => (
<WithSource sourceId="default" indexTypes={indexTypes}>
{({ auditbeatIndicesExist, indexPattern }) =>
indicesExistOrDataTemporarilyUnavailable(auditbeatIndicesExist) ? (
<>
<HostsKql indexPattern={indexPattern} type={hostsModel.HostsType.page} />
<StickyContainer>
<FiltersGlobal>
<HostsKql indexPattern={indexPattern} type={hostsModel.HostsType.page} />
</FiltersGlobal>
<HeaderPage
subtitle={<LastEventTime indexKey={LastEventIndexKey.hosts} />}
title={i18n.HOSTS}
/>
<GlobalTime>
{({ to, from, setQuery }) => (
<>
@ -186,7 +198,7 @@ const HostsComponent = pure<HostsComponentProps>(({ filterQuery }) => (
</>
)}
</GlobalTime>
</>
</StickyContainer>
) : (
<EmptyPage
title={i18n.NO_AUDITBEAT_INDICES}

View file

@ -12,7 +12,6 @@ import { AutocompleteField } from '../../components/autocomplete_field';
import { HostsFilter } from '../../containers/hosts';
import { KueryAutocompletion } from '../../containers/kuery_autocompletion';
import { hostsModel } from '../../store';
import { PageHeader, PageHeaderSection } from '../styles';
import * as i18n from './translations';
@ -22,31 +21,27 @@ interface HostsKqlProps {
}
export const HostsKql = pure<HostsKqlProps>(({ indexPattern, type }) => (
<PageHeader data-test-subj="paneHeader">
<PageHeaderSection>
<KueryAutocompletion indexPattern={indexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<HostsFilter indexPattern={indexPattern} type={type}>
{({
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={i18n.KQL_PLACE_HOLDER}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
)}
</HostsFilter>
<KueryAutocompletion indexPattern={indexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<HostsFilter indexPattern={indexPattern} type={type}>
{({
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={i18n.KQL_PLACE_HOLDER}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
)}
</KueryAutocompletion>
</PageHeaderSection>
</PageHeader>
</HostsFilter>
)}
</KueryAutocompletion>
));

View file

@ -15,7 +15,7 @@ export const NO_AUDITBEAT_INDICES = i18n.translate('xpack.siem.hosts.noAuditBeat
});
export const KQL_PLACE_HOLDER = i18n.translate('xpack.siem.hosts.kqlPlaceHolder', {
defaultMessage: 'Search… (e.g. host.name:"foo" AND process.name:"bar")',
defaultMessage: 'e.g. host.name: "foo"',
});
export const LETS_ADD_SOME = i18n.translate('xpack.siem.hosts.letsAddSome.description', {

View file

@ -8,19 +8,24 @@ import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import { getOr } from 'lodash/fp';
import React from 'react';
import { connect } from 'react-redux';
import { StickyContainer } from 'react-sticky';
import { pure } from 'recompose';
import chrome, { Breadcrumb } from 'ui/chrome';
import { EmptyPage } from '../../components/empty_page';
import { FiltersGlobal } from '../../components/filters_global';
import { HeaderPage } from '../../components/header_page';
import { LastEventTime } from '../../components/last_event_time';
import { getNetworkUrl, NetworkComponentProps } from '../../components/link_to/redirect_to_network';
import { manageQuery } from '../../components/page/manage_query';
import { DomainsTable } from '../../components/page/network/domains_table';
import { FlowTargetSelectConnected } from '../../components/page/network/flow_target_select_connected';
import { IpOverview } from '../../components/page/network/ip_overview';
import { DomainsQuery } from '../../containers/domains';
import { GlobalTime } from '../../containers/global_time';
import { IpOverviewQuery } from '../../containers/ip_overview';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
import { FlowTarget, IndexType } from '../../graphql/types';
import { FlowTarget, IndexType, LastEventIndexKey } from '../../graphql/types';
import { decodeIpv6 } from '../../lib/helpers';
import { networkModel, networkSelectors, State } from '../../store';
import { TlsTable } from '../../components/page/network/tls_table';
@ -57,8 +62,17 @@ const IPDetailsComponent = pure<IPDetailsComponentProps>(
<WithSource sourceId="default" indexTypes={indexTypes}>
{({ filebeatIndicesExist, indexPattern }) =>
indicesExistOrDataTemporarilyUnavailable(filebeatIndicesExist) ? (
<>
<NetworkKql indexPattern={indexPattern} type={networkModel.NetworkType.details} />
<StickyContainer>
<FiltersGlobal>
<NetworkKql indexPattern={indexPattern} type={networkModel.NetworkType.details} />
</FiltersGlobal>
<HeaderPage
subtitle={<LastEventTime indexKey={LastEventIndexKey.ipDetails} ip={ip} />}
title={ip}
>
<FlowTargetSelectConnected />
</HeaderPage>
<GlobalTime>
{({ to, from, setQuery }) => (
@ -79,9 +93,8 @@ const IPDetailsComponent = pure<IPDetailsComponentProps>(
/>
)}
</IpOverviewQuery>
<EuiSpacer size="s" />
<EuiHorizontalRule margin="xs" />
<EuiSpacer />
<EuiHorizontalRule />
<DomainsQuery
endDate={to}
@ -168,7 +181,7 @@ const IPDetailsComponent = pure<IPDetailsComponentProps>(
</>
)}
</GlobalTime>
</>
</StickyContainer>
) : (
<EmptyPage
title={i18n.NO_FILEBEAT_INDICES}

View file

@ -12,7 +12,6 @@ import { AutocompleteField } from '../../components/autocomplete_field';
import { KueryAutocompletion } from '../../containers/kuery_autocompletion';
import { NetworkFilter } from '../../containers/network';
import { networkModel } from '../../store';
import { PageHeader, PageHeaderSection } from '../styles';
import * as i18n from './translations';
@ -22,31 +21,27 @@ interface NetworkKqlProps {
}
export const NetworkKql = pure<NetworkKqlProps>(({ indexPattern, type }) => (
<PageHeader data-test-subj="paneHeader">
<PageHeaderSection>
<KueryAutocompletion indexPattern={indexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<NetworkFilter indexPattern={indexPattern} type={type}>
{({
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={i18n.KQL_PLACE_HOLDER}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
)}
</NetworkFilter>
<KueryAutocompletion indexPattern={indexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<NetworkFilter indexPattern={indexPattern} type={type}>
{({
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={i18n.KQL_PLACE_HOLDER}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
)}
</KueryAutocompletion>
</PageHeaderSection>
</PageHeader>
</NetworkFilter>
)}
</KueryAutocompletion>
));

View file

@ -7,6 +7,7 @@
import { mount } from 'enzyme';
import * as React from 'react';
import '../../mock/match_media';
import { Network } from './network';
import { mocksSource } from '../../containers/source/mock';

View file

@ -8,10 +8,14 @@ import { EuiSpacer } from '@elastic/eui';
import { getOr } from 'lodash/fp';
import React from 'react';
import { connect } from 'react-redux';
import { StickyContainer } from 'react-sticky';
import { pure } from 'recompose';
import chrome from 'ui/chrome';
import { EmptyPage } from '../../components/empty_page';
import { FiltersGlobal } from '../../components/filters_global';
import { HeaderPage } from '../../components/header_page';
import { LastEventTime } from '../../components/last_event_time';
import { manageQuery } from '../../components/page/manage_query';
import { KpiNetworkComponent, NetworkTopNFlowTable } from '../../components/page/network';
import { NetworkDnsTable } from '../../components/page/network/network_dns_table';
@ -20,7 +24,7 @@ import { KpiNetworkQuery } from '../../containers/kpi_network';
import { NetworkDnsQuery } from '../../containers/network_dns';
import { NetworkTopNFlowQuery } from '../../containers/network_top_n_flow';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
import { IndexType } from '../../graphql/types';
import { IndexType, LastEventIndexKey } from '../../graphql/types';
import { networkModel, networkSelectors, State } from '../../store';
import { NetworkKql } from './kql';
@ -42,8 +46,15 @@ const NetworkComponent = pure<NetworkComponentProps>(({ filterQuery }) => (
<WithSource sourceId="default" indexTypes={indexTypes}>
{({ filebeatIndicesExist, indexPattern }) =>
indicesExistOrDataTemporarilyUnavailable(filebeatIndicesExist) ? (
<>
<NetworkKql indexPattern={indexPattern} type={networkModel.NetworkType.page} />
<StickyContainer>
<FiltersGlobal>
<NetworkKql indexPattern={indexPattern} type={networkModel.NetworkType.page} />
</FiltersGlobal>
<HeaderPage
subtitle={<LastEventTime indexKey={LastEventIndexKey.network} />}
title={i18n.NETWORK}
/>
<GlobalTime>
{({ to, from, setQuery }) => (
@ -118,7 +129,7 @@ const NetworkComponent = pure<NetworkComponentProps>(({ filterQuery }) => (
</>
)}
</GlobalTime>
</>
</StickyContainer>
) : (
<EmptyPage
title={i18n.NO_FILEBEAT_INDICES}

View file

@ -15,7 +15,7 @@ export const NO_FILEBEAT_INDICES = i18n.translate('xpack.siem.network.noFilebeat
});
export const KQL_PLACE_HOLDER = i18n.translate('xpack.siem.network.kqlPlaceHolder', {
defaultMessage: 'Search… (e.g. network.name:"foo" AND process.name:"bar")',
defaultMessage: 'e.g. source.ip: "foo"',
});
export const LETS_ADD_SOME = i18n.translate('xpack.siem.network.letsAddSome.description', {

View file

@ -5,10 +5,12 @@
*/
import { EuiFlexGroup } from '@elastic/eui';
import moment from 'moment';
import React from 'react';
import { pure } from 'recompose';
import chrome from 'ui/chrome';
import { HeaderPage } from '../../components/header_page';
import { OverviewHost } from '../../components/page/overview/overview_host';
import { OverviewNetwork } from '../../components/page/overview/overview_network';
import { GlobalTime } from '../../containers/global_time';
@ -24,28 +26,38 @@ const basePath = chrome.getBasePath();
const indexTypes = [IndexType.FILEBEAT, IndexType.AUDITBEAT, IndexType.PACKETBEAT];
export const OverviewComponent = pure(() => (
<WithSource sourceId="default" indexTypes={indexTypes}>
{({ auditbeatIndicesExist, filebeatIndicesExist }) =>
indicesExistOrDataTemporarilyUnavailable(auditbeatIndicesExist) &&
indicesExistOrDataTemporarilyUnavailable(filebeatIndicesExist) ? (
<GlobalTime>
{({ to, from, setQuery }) => (
<EuiFlexGroup>
<Summary />
<OverviewHost endDate={to} startDate={from} setQuery={setQuery} />
<OverviewNetwork endDate={to} startDate={from} setQuery={setQuery} />
</EuiFlexGroup>
)}
</GlobalTime>
) : (
<EmptyPage
title={i18n.NO_FILEBEAT_INDICES}
message={i18n.LETS_ADD_SOME}
actionLabel={i18n.SETUP_INSTRUCTIONS}
actionUrl={`${basePath}/app/kibana#/home/tutorial_directory/security`}
/>
)
}
</WithSource>
));
export const OverviewComponent = pure(() => {
const dateEnd = Date.now();
const dateRange = moment.duration(24, 'hours').asMilliseconds();
const dateStart = dateEnd - dateRange;
return (
<WithSource sourceId="default" indexTypes={indexTypes}>
{({ auditbeatIndicesExist, filebeatIndicesExist }) =>
indicesExistOrDataTemporarilyUnavailable(auditbeatIndicesExist) &&
indicesExistOrDataTemporarilyUnavailable(filebeatIndicesExist) ? (
<>
<HeaderPage subtitle={i18n.PAGE_SUBTITLE} title={i18n.PAGE_TITLE} />
<GlobalTime>
{({ setQuery }) => (
<EuiFlexGroup>
<Summary />
<OverviewHost endDate={dateEnd} startDate={dateStart} setQuery={setQuery} />
<OverviewNetwork endDate={dateEnd} startDate={dateStart} setQuery={setQuery} />
</EuiFlexGroup>
)}
</GlobalTime>
</>
) : (
<EmptyPage
title={i18n.NO_FILEBEAT_INDICES}
message={i18n.LETS_ADD_SOME}
actionLabel={i18n.SETUP_INSTRUCTIONS}
actionUrl={`${basePath}/app/kibana#/home/tutorial_directory/security`}
/>
)
}
</WithSource>
);
});

View file

@ -6,6 +6,14 @@
import { i18n } from '@kbn/i18n';
export const PAGE_TITLE = i18n.translate('xpack.siem.overview.pageTitle', {
defaultMessage: 'SIEM',
});
export const PAGE_SUBTITLE = i18n.translate('xpack.siem.overview.pageSubtitle', {
defaultMessage: 'Security Information & Event Management with the Elastic Stack',
});
export const NO_FILEBEAT_INDICES = i18n.translate(
'xpack.siem.overview.network.noFilebeatIndicies',
{

View file

@ -1,22 +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 { EuiPageHeader, EuiPageHeaderSection } from '@elastic/eui';
import styled from 'styled-components';
export const PageHeader = styled(EuiPageHeader)`
background-color: ${props => props.theme.eui.euiColorLightestShade};
position: fixed;
width: calc(100% - 32px);
z-index: 10;
padding: 6px 0px 0px 0px;
margin-bottom: 0px;
margin-top: 50px;
`;
export const PageHeaderSection = styled(EuiPageHeaderSection)`
width: 100%;
`;

View file

@ -9,6 +9,7 @@ import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import { HeaderPage } from '../../components/header_page';
import { StatefulOpenTimeline } from '../../components/open_timeline';
import * as i18n from './translations';
@ -27,15 +28,19 @@ export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10;
class Timelines extends React.PureComponent<DispatchProps> {
public render() {
return (
<TimelinesContainer>
<StatefulOpenTimeline
deleteTimelines={this.deleteTimelines}
openTimeline={this.openTimeline}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={[]}
title={i18n.ALL_TIMELINES}
/>
</TimelinesContainer>
<>
<HeaderPage title={i18n.PAGE_TITLE} />
<TimelinesContainer>
<StatefulOpenTimeline
deleteTimelines={this.deleteTimelines}
openTimeline={this.openTimeline}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={[]}
title={i18n.ALL_TIMELINES_PANEL_TITLE}
/>
</TimelinesContainer>
</>
);
}

View file

@ -6,6 +6,13 @@
import { i18n } from '@kbn/i18n';
export const ALL_TIMELINES = i18n.translate('xpack.siem.pages.timeline.allTimelinesTitle', {
defaultMessage: 'All Timelines',
export const PAGE_TITLE = i18n.translate('xpack.siem.timelines.pageTitle', {
defaultMessage: 'Timelines',
});
export const ALL_TIMELINES_PANEL_TITLE = i18n.translate(
'xpack.siem.timelines.allTimelines.panelTitle',
{
defaultMessage: 'All Timelines',
}
);

View file

@ -3626,6 +3626,13 @@
"@types/history" "*"
"@types/react" "*"
"@types/react-sticky@^6.0.3":
version "6.0.3"
resolved "https://registry.yarnpkg.com/@types/react-sticky/-/react-sticky-6.0.3.tgz#94d16a951467b29ad44c224081d9503e7e590434"
integrity sha512-tW0Y1hTr2Tao4yX58iKl0i7BaqrdObGXAzsyzd8VGVrWVEgbQuV6P6QKVd/kFC7FroXyelftiVNJ09pnfkcjww==
dependencies:
"@types/react" "*"
"@types/react-test-renderer@^16.8.0":
version "16.8.1"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.8.1.tgz#96f3ce45a3a41c94eca532a99103dd3042c9d055"