[SIEM] Top Talkers: Split into Source and Destination (#43719)

* wip

* sources table working woohoo

* rename table back to n flow

* wired up both source and destination tables

* cleanups

* flows and ips sort

* flows and ips sort

* fix flow sorting

* differentiate tables

* bring back network

* fix tests

* integration tests updated

* add country names to flags, sort by desc on first click

* yarn doing its thing

* correct panel width during initial load

* remove default props

* fix inspect btn placement at small viewport sizes

* reformatting bytes

* used compressed table prop

* fix unit tests bytes

* update header subtext size

* override table cell display flex

* make titles plural

* less distracting empty string

* simplify markup and align numbers right

* improve more items experience

* undo compressed tables…looks bad

* stack tables

* restore compressed and side by side layout

* sentence case for titles

* table cleanup

* force more to separate line

* dnd poc changes

* jest test updates

* TypeScript, i18n, and bean-gen fixes

* fix for filter action color

* single quotes instead of backticks

* use getEmptyValue() instead of static emdash

* remove “ AS” prefix

* address PR concerns

* add space

* first attempt at prop change with matchMedia

* split table display by mediaMatch

* rm comments

* fix type issue

* correct react hook

* lint fix

* fix jest

* update snapshots
This commit is contained in:
Steph Milovic 2019-08-29 04:32:56 -06:00 committed by Tudor Golubenco
parent 93e2eb3e74
commit 29b8ce2b7e
78 changed files with 4036 additions and 2749 deletions

View file

@ -53,6 +53,11 @@ export const sharedSchema = gql`
source
}
enum FlowTargetNew {
destination
source
}
enum FlowDirection {
uniDirectional
biDirectional

View file

@ -39,4 +39,4 @@
"**/mocha-multi-reporters/**"
]
}
}
}

View file

@ -26,6 +26,6 @@ describe('Bytes', () => {
.find(PreferenceFormattedBytes)
.first()
.text()
).toEqual('1.177MB');
).toEqual('1.2MB');
});
});

View file

@ -5,7 +5,6 @@
*/
import { isEqual } from 'lodash/fp';
import { EuiText } from '@elastic/eui';
import * as React from 'react';
import {
Draggable,
@ -14,7 +13,7 @@ import {
Droppable,
} from 'react-beautiful-dnd';
import { connect } from 'react-redux';
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import { ActionCreator } from 'typescript-fsa';
import { dragAndDropActions } from '../../store/drag_and_drop';
@ -28,17 +27,114 @@ export const DragEffects = styled.div``;
DragEffects.displayName = 'DragEffects';
const ProviderContainer = styled.div`
&:hover {
transition: background-color 0.7s ease;
background-color: ${props => props.theme.eui.euiColorLightShade};
const Wrapper = styled.div`
.euiPageBody & {
display: inline-block;
}
`;
Wrapper.displayName = 'Wrapper';
const ProviderContainer = styled.div<{ isDragging: boolean }>`
${({ theme, isDragging }) => css`
// ALL DRAGGABLES
&,
&::before,
&::after {
transition: background ${theme.eui.euiAnimSpeedFast} ease,
color ${theme.eui.euiAnimSpeedFast} ease;
}
// PAGE DRAGGABLES
${!isDragging &&
`
.euiPageBody & {
border-radius: 2px;
padding: 0 4px 0 8px;
position: relative;
z-index: ${theme.eui.euiZLevel0} !important;
&::before {
background-image: linear-gradient(
135deg,
${theme.eui.euiColorMediumShade} 25%,
transparent 25%
),
linear-gradient(-135deg, ${theme.eui.euiColorMediumShade} 25%, transparent 25%),
linear-gradient(135deg, transparent 75%, ${theme.eui.euiColorMediumShade} 75%),
linear-gradient(-135deg, transparent 75%, ${theme.eui.euiColorMediumShade} 75%);
background-position: 0 0, 1px 0, 1px -1px, 0px 1px;
background-size: 2px 2px;
bottom: 2px;
content: '';
display: block;
left: 2px;
position: absolute;
top: 2px;
width: 4px;
}
}
.euiPageBody tr:hover & {
background-color: ${theme.eui.euiColorLightShade};
&::before {
background-image: linear-gradient(
135deg,
${theme.eui.euiColorDarkShade} 25%,
transparent 25%
),
linear-gradient(-135deg, ${theme.eui.euiColorDarkShade} 25%, transparent 25%),
linear-gradient(135deg, transparent 75%, ${theme.eui.euiColorDarkShade} 75%),
linear-gradient(-135deg, transparent 75%, ${theme.eui.euiColorDarkShade} 75%);
}
}
.euiPageBody &:hover,
.euiPageBody &:focus,
.euiPageBody tr:hover &:hover,
.euiPageBody tr:hover &:focus {
background-color: ${theme.eui.euiColorPrimary};
&,
& a {
color: ${theme.eui.euiColorEmptyShade};
}
&::before {
background-image: linear-gradient(
135deg,
${theme.eui.euiColorEmptyShade} 25%,
transparent 25%
),
linear-gradient(-135deg, ${theme.eui.euiColorEmptyShade} 25%, transparent 25%),
linear-gradient(135deg, transparent 75%, ${theme.eui.euiColorEmptyShade} 75%),
linear-gradient(-135deg, transparent 75%, ${theme.eui.euiColorEmptyShade} 75%);
}
}
`}
${isDragging &&
`
.euiPageBody & {
z-index: ${theme.eui.euiZLevel9} !important;
`}
// TIMELINE DRAGGABLES
.timeline-flyout &:hover {
background-color: ${theme.eui.euiColorLightShade};
}
`}
`;
ProviderContainer.displayName = 'ProviderContainer';
interface OwnProps {
dataProvider: DataProvider;
inline?: boolean;
render: (
props: DataProvider,
provided: DraggableProvided,
@ -86,7 +182,7 @@ class DraggableWrapperComponent extends React.Component<Props> {
const { dataProvider, render, width } = this.props;
return (
<div data-test-subj="draggableWrapperDiv">
<Wrapper data-test-subj="draggableWrapperDiv">
<Droppable isDropDisabled={true} droppableId={getDroppableId(dataProvider.id)}>
{droppableProvided => (
<div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
@ -95,38 +191,40 @@ class DraggableWrapperComponent extends React.Component<Props> {
index={0}
key={dataProvider.id}
>
{(provided, snapshot) => (
<ProviderContainer
{...provided.draggableProps}
{...provided.dragHandleProps}
innerRef={provided.innerRef}
data-test-subj="providerContainer"
style={{
...provided.draggableProps.style,
zIndex: 9000, // EuiFlyout has a z-index of 8000
}}
>
{width != null && !snapshot.isDragging ? (
<TruncatableText
data-test-subj="draggable-truncatable-content"
size="s"
width={width}
>
{render(dataProvider, provided, snapshot)}
</TruncatableText>
) : (
<EuiText data-test-subj="draggable-content" size="s">
{render(dataProvider, provided, snapshot)}
</EuiText>
)}
</ProviderContainer>
)}
{(provided, snapshot) => {
return (
<ProviderContainer
{...provided.draggableProps}
{...provided.dragHandleProps}
innerRef={provided.innerRef}
data-test-subj="providerContainer"
isDragging={snapshot.isDragging}
style={{
...provided.draggableProps.style,
}}
>
{width != null && !snapshot.isDragging ? (
<TruncatableText
data-test-subj="draggable-truncatable-content"
size="s"
width={width}
>
{render(dataProvider, provided, snapshot)}
</TruncatableText>
) : (
<span data-test-subj="draggable-content">
{render(dataProvider, provided, snapshot)}
</span>
)}
</ProviderContainer>
);
}}
</Draggable>
{droppableProvided.placeholder}
</div>
)}
</Droppable>
</div>
</Wrapper>
);
}
}

View file

@ -29,7 +29,7 @@ describe('EmptyValue', () => {
});
describe('#getEmptyValue', () => {
test('should return an empty value', () => expect(getEmptyValue()).toBe('--'));
test('should return an empty value', () => expect(getEmptyValue()).toBe(''));
});
describe('#getEmptyString', () => {
@ -44,8 +44,12 @@ describe('EmptyValue', () => {
});
describe('#getEmptyTagValue', () => {
const wrapper = mount(<p>{getEmptyTagValue()}</p>);
test('should return an empty tag value', () => expect(wrapper.text()).toBe('--'));
const wrapper = mount(
<ThemeProvider theme={theme}>
<p>{getEmptyTagValue()}</p>
</ThemeProvider>
);
test('should return an empty tag value', () => expect(wrapper.text()).toBe('—'));
});
describe('#getEmptyStringTag', () => {
@ -70,12 +74,20 @@ describe('EmptyValue', () => {
describe('#defaultToEmptyTag', () => {
test('should default to an empty value when a value is null', () => {
const wrapper = mount(<p>{defaultToEmptyTag(null)}</p>);
const wrapper = mount(
<ThemeProvider theme={theme}>
<p>{defaultToEmptyTag(null)}</p>
</ThemeProvider>
);
expect(wrapper.text()).toBe(getEmptyValue());
});
test('should default to an empty value when a value is undefined', () => {
const wrapper = mount(<p>{defaultToEmptyTag(undefined)}</p>);
const wrapper = mount(
<ThemeProvider theme={theme}>
<p>{defaultToEmptyTag(undefined)}</p>
</ThemeProvider>
);
expect(wrapper.text()).toBe(getEmptyValue());
});
@ -101,7 +113,11 @@ describe('EmptyValue', () => {
},
},
};
const wrapper = mount(<p>{getOrEmptyTag('a.b.c', test)}</p>);
const wrapper = mount(
<ThemeProvider theme={theme}>
<p>{getOrEmptyTag('a.b.c', test)}</p>
</ThemeProvider>
);
expect(wrapper.text()).toBe(getEmptyValue());
});
@ -113,7 +129,11 @@ describe('EmptyValue', () => {
},
},
};
const wrapper = mount(<p>{getOrEmptyTag('a.b.c', test)}</p>);
const wrapper = mount(
<ThemeProvider theme={theme}>
<p>{getOrEmptyTag('a.b.c', test)}</p>
</ThemeProvider>
);
expect(wrapper.text()).toBe(getEmptyValue());
});
@ -123,7 +143,11 @@ describe('EmptyValue', () => {
b: {},
},
};
const wrapper = mount(<p>{getOrEmptyTag('a.b.c', test)}</p>);
const wrapper = mount(
<ThemeProvider theme={theme}>
<p>{getOrEmptyTag('a.b.c', test)}</p>
</ThemeProvider>
);
expect(wrapper.text()).toBe(getEmptyValue());
});

View file

@ -10,21 +10,17 @@ import styled from 'styled-components';
import * as i18n from './translations';
const EmptyString = styled.span`
color: ${({
theme: {
eui: { euiColorMediumShade },
},
}) => euiColorMediumShade};
const EmptyWrapper = styled.span`
color: ${props => props.theme.eui.euiColorMediumShade};
`;
EmptyString.displayName = 'EmptyString';
EmptyWrapper.displayName = 'EmptyWrapper';
export const getEmptyValue = () => '--';
export const getEmptyValue = () => '';
export const getEmptyString = () => `(${i18n.EMPTY_STRING})`;
export const getEmptyTagValue = () => <>{getEmptyValue()}</>;
export const getEmptyStringTag = () => <EmptyString>{getEmptyString()}</EmptyString>;
export const getEmptyTagValue = () => <EmptyWrapper>{getEmptyValue()}</EmptyWrapper>;
export const getEmptyStringTag = () => <EmptyWrapper>{getEmptyString()}</EmptyWrapper>;
export const defaultToEmptyTag = <T extends unknown>(item: T): JSX.Element => {
if (item == null) {

View file

@ -6,6 +6,8 @@
import { mount } from 'enzyme';
import * as React from 'react';
import { ThemeProvider } from 'styled-components';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { mockBrowserFields } from '../../containers/source/mock';
@ -16,17 +18,20 @@ import * as i18n from './translations';
const timelineId = 'test';
describe('CategoriesPane', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
test('it renders the expected title', () => {
const wrapper = mount(
<CategoriesPane
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
width={CATEGORY_PANE_WIDTH}
onCategorySelected={jest.fn()}
onUpdateColumns={jest.fn()}
selectedCategoryId={''}
timelineId={timelineId}
/>
<ThemeProvider theme={theme}>
<CategoriesPane
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
width={CATEGORY_PANE_WIDTH}
onCategorySelected={jest.fn()}
onUpdateColumns={jest.fn()}
selectedCategoryId={''}
timelineId={timelineId}
/>
</ThemeProvider>
);
expect(
@ -39,7 +44,7 @@ describe('CategoriesPane', () => {
test('it renders a "No fields match" message when filteredBrowserFields is empty', () => {
const wrapper = mount(
<div>
<ThemeProvider theme={theme}>
<CategoriesPane
browserFields={mockBrowserFields}
filteredBrowserFields={{}}
@ -49,7 +54,7 @@ describe('CategoriesPane', () => {
selectedCategoryId={''}
timelineId={timelineId}
/>
</div>
</ThemeProvider>
);
expect(

View file

@ -12,14 +12,17 @@ import { mockBrowserFields } from '../../containers/source/mock';
import { CATEGORY_PANE_WIDTH, getFieldCount } from './helpers';
import { CategoriesPane } from './categories_pane';
import { ThemeProvider } from 'styled-components';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
const timelineId = 'test';
const theme = () => ({ eui: euiDarkVars, darkMode: true });
describe('getCategoryColumns', () => {
Object.keys(mockBrowserFields).forEach(categoryId => {
test(`it renders the ${categoryId} category name (from filteredBrowserFields)`, () => {
const wrapper = mount(
<div>
<ThemeProvider theme={theme}>
<CategoriesPane
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
@ -29,7 +32,7 @@ describe('getCategoryColumns', () => {
selectedCategoryId={''}
timelineId={timelineId}
/>
</div>
</ThemeProvider>
);
expect(
@ -44,7 +47,7 @@ describe('getCategoryColumns', () => {
Object.keys(mockBrowserFields).forEach(categoryId => {
test(`it renders the correct field count for the ${categoryId} category (from filteredBrowserFields)`, () => {
const wrapper = mount(
<div>
<ThemeProvider theme={theme}>
<CategoriesPane
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
@ -54,7 +57,7 @@ describe('getCategoryColumns', () => {
selectedCategoryId={''}
timelineId={timelineId}
/>
</div>
</ThemeProvider>
);
expect(
@ -68,7 +71,7 @@ describe('getCategoryColumns', () => {
test('it renders a hover actions panel for the category name', () => {
const wrapper = mount(
<div>
<ThemeProvider theme={theme}>
<CategoriesPane
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
@ -78,9 +81,8 @@ describe('getCategoryColumns', () => {
selectedCategoryId={''}
timelineId={timelineId}
/>
</div>
</ThemeProvider>
);
expect(
wrapper
.find('[data-test-subj="category-link"]')
@ -95,7 +97,7 @@ describe('getCategoryColumns', () => {
const selectedCategoryId = 'auditd';
const wrapper = mount(
<div>
<ThemeProvider theme={theme}>
<CategoriesPane
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
@ -105,7 +107,7 @@ describe('getCategoryColumns', () => {
selectedCategoryId={selectedCategoryId}
timelineId={timelineId}
/>
</div>
</ThemeProvider>
);
expect(
@ -118,7 +120,7 @@ describe('getCategoryColumns', () => {
const notTheSelectedCategoryId = 'base';
const wrapper = mount(
<div>
<ThemeProvider theme={theme}>
<CategoriesPane
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
@ -128,7 +130,7 @@ describe('getCategoryColumns', () => {
selectedCategoryId={selectedCategoryId}
timelineId={timelineId}
/>
</div>
</ThemeProvider>
);
expect(
@ -143,7 +145,7 @@ describe('getCategoryColumns', () => {
const onCategorySelected = jest.fn();
const wrapper = mount(
<div>
<ThemeProvider theme={theme}>
<CategoriesPane
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
@ -153,7 +155,7 @@ describe('getCategoryColumns', () => {
selectedCategoryId={selectedCategoryId}
timelineId={timelineId}
/>
</div>
</ThemeProvider>
);
wrapper

View file

@ -2,6 +2,6 @@
exports[`formatted_bytes PreferenceFormattedBytes rendering renders correctly against snapshot 1`] = `
<Fragment>
2.676MB
2.7MB
</Fragment>
`;

View file

@ -34,7 +34,7 @@ describe('formatted_bytes', () => {
test('it renders bytes to hardcoded format when no configuration exists', () => {
mockUseKibanaUiSetting.mockImplementation(() => [null]);
const wrapper = mount(<PreferenceFormattedBytes value={bytes} />);
expect(wrapper.text()).toEqual('2.676MB');
expect(wrapper.text()).toEqual('2.7MB');
});
test('it renders bytes according to the default format', () => {
@ -42,7 +42,7 @@ describe('formatted_bytes', () => {
getMockKibanaUiSetting(mockFrameworks.default_browser)
);
const wrapper = mount(<PreferenceFormattedBytes value={bytes} />);
expect(wrapper.text()).toEqual('2.676MB');
expect(wrapper.text()).toEqual('2.7MB');
});
test('it renders bytes supplied as a number according to the default format', () => {
@ -50,7 +50,7 @@ describe('formatted_bytes', () => {
getMockKibanaUiSetting(mockFrameworks.default_browser)
);
const wrapper = mount(<PreferenceFormattedBytes value={+bytes} />);
expect(wrapper.text()).toEqual('2.676MB');
expect(wrapper.text()).toEqual('2.7MB');
});
test('it renders bytes according to new format', () => {

View file

@ -13,7 +13,7 @@ import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting';
export const PreferenceFormattedBytes = React.memo<{ value: string | number }>(({ value }) => {
const [bytesFormat] = useKibanaUiSetting(DEFAULT_BYTES_FORMAT);
return (
<>{bytesFormat ? numeral(value).format(bytesFormat) : numeral(value).format('0,0.[000]b')}</>
<>{bytesFormat ? numeral(value).format(bytesFormat) : numeral(value).format('0,0.[0]b')}</>
);
});

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getEmptyValue } from '../empty_value';
import {
getFormattedDurationString,
getHumanizedDuration,
@ -15,17 +16,16 @@ import {
ONE_SECOND,
ONE_YEAR,
} from './helpers';
import * as i18n from './translations';
describe('FormattedDurationHelpers', () => {
describe('#getFormattedDurationString', () => {
test('it returns a placeholder when the input is undefined', () => {
expect(getFormattedDurationString(undefined)).toEqual('--');
expect(getFormattedDurationString(undefined)).toEqual(getEmptyValue());
});
test('it returns a placeholder when the input is null', () => {
expect(getFormattedDurationString(null)).toEqual('--');
expect(getFormattedDurationString(null)).toEqual(getEmptyValue());
});
test('it echos back the input as a string when the input is not a number', () => {

View file

@ -49,7 +49,7 @@ export const HeaderPage = pure<HeaderPageProps>(
</h1>
</EuiTitle>
<EuiText color="subdued" size="s">
<EuiText color="subdued" size="xs">
{subtitle}
</EuiText>
</EuiFlexItem>

View file

@ -38,27 +38,33 @@ export interface HeaderPanelProps {
export const HeaderPanel = pure<HeaderPanelProps>(
({ border, children, id, showInspect = false, subtitle, title, tooltip }) => (
<Header border={border}>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle>
<h2 data-test-subj="panel_headline_title">
{title}
{tooltip && (
<>
{' '}
<EuiIconTip color="subdued" content={tooltip} size="l" type="iInCircle" />
</>
)}
</h2>
</EuiTitle>
<EuiFlexGroup alignItems="center" responsive={false}>
<EuiFlexItem>
<EuiTitle>
<h2 data-test-subj="panel_headline_title">
{title}
{tooltip && (
<>
{' '}
<EuiIconTip color="subdued" content={tooltip} size="l" type="iInCircle" />
</>
)}
</h2>
</EuiTitle>
<EuiText color="subdued" size="s">
{subtitle}
</EuiText>
</EuiFlexItem>
<EuiText color="subdued" size="xs">
{subtitle}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{id && <InspectButton queryId={id} inspectIndex={0} show={showInspect} title={title} />}
{id && (
<EuiFlexItem grow={false}>
<InspectButton queryId={id} inspectIndex={0} show={showInspect} title={title} />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
{children && <EuiFlexItem grow={false}>{children}</EuiFlexItem>}

View file

@ -9,6 +9,7 @@ import * as React from 'react';
import { MockedProvider } from 'react-apollo/test-utils';
import { render } from 'react-testing-library';
import { getEmptyValue } from '../empty_value';
import { LastEventIndexKey } from '../../graphql/types';
import { mockLastEventTimeQuery } from '../../containers/events/last_event_time/mock';
import { wait } from '../../lib/helpers';
@ -92,6 +93,6 @@ describe('Last Event Time Stat', () => {
);
await wait();
expect(container.innerHTML).toBe('--');
expect(container.innerHTML).toContain(getEmptyValue());
});
});

View file

@ -104,6 +104,7 @@ type Func<T> = (arg: T) => string | number;
export interface Columns<T, U = T> {
field?: string;
align?: string;
name: string | React.ReactNode;
isMobileHeader?: boolean;
sortable?: boolean | Func<T>;
@ -209,8 +210,9 @@ export class LoadMoreTable<T, U, V, W, X, Y, Z, AA, AB> extends React.PureCompon
) : (
<>
<BasicTable
items={pageOfItems}
columns={columns}
compressed
items={pageOfItems}
onChange={onChange}
sorting={
sorting
@ -311,6 +313,10 @@ const BasicTable = styled(EuiBasicTable)`
td {
vertical-align: top;
}
.euiTableCellContent {
display: block;
}
}
`;

View file

@ -72,7 +72,13 @@ export const AnomaliesHostTable = React.memo<AnomaliesHostTableProps>(
tooltip={i18n.TOOLTIP}
/>
<BasicTable items={hosts} columns={columns} pagination={pagination} sorting={sorting} />
<BasicTable
columns={columns}
compressed
items={hosts}
pagination={pagination}
sorting={sorting}
/>
{loading && (
<Loader data-test-subj="anomalies-host-table-loading-panel" overlay size="xl" />

View file

@ -71,8 +71,9 @@ export const AnomaliesNetworkTable = React.memo<AnomaliesNetworkTableProps>(
/>
<BasicTable
items={networks}
columns={columns}
compressed
items={networks}
pagination={pagination}
sorting={sorting}
/>

View file

@ -5,14 +5,22 @@
*/
import styled from 'styled-components';
import { EuiInMemoryTable } from '@elastic/eui';
import { EuiInMemoryTable, EuiInMemoryTableProps } from '@elastic/eui';
export const BasicTable = styled(EuiInMemoryTable)`
// TODO: Remove this once EuiBasicTable supports in its table props the boolean of compressed
type ExtendedInMemoryTable = EuiInMemoryTableProps & { compressed: boolean };
const Extended: React.FunctionComponent<ExtendedInMemoryTable> = EuiInMemoryTable;
export const BasicTable = styled(Extended)`
tbody {
th,
td {
vertical-align: top;
}
.euiTableCellContent {
display: block;
}
}
`;

View file

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NoteCardBody renders correctly against snapshot 1`] = `
<Component
<NoteCardBody
rawNote="# This is a note"
/>
`;

View file

@ -6,6 +6,8 @@
import * as React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { ThemeProvider } from 'styled-components';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { NoteCard } from '.';
@ -13,15 +15,24 @@ describe('NoteCard', () => {
const created = new Date();
const rawNote = 'noteworthy';
const user = 'elastic';
const theme = () => ({ eui: euiDarkVars, darkMode: true });
test('it renders a note card header', () => {
const wrapper = mountWithIntl(<NoteCard created={created} rawNote={rawNote} user={user} />);
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<NoteCard created={created} rawNote={rawNote} user={user} />
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="note-card-header"]').exists()).toEqual(true);
});
test('it renders a note card body', () => {
const wrapper = mountWithIntl(<NoteCard created={created} rawNote={rawNote} user={user} />);
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<NoteCard created={created} rawNote={rawNote} user={user} />
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="note-card-body"]').exists()).toEqual(true);
});

View file

@ -7,6 +7,8 @@
import { mount, shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import * as React from 'react';
import { ThemeProvider } from 'styled-components';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { NoteCardBody } from './note_card_body';
@ -14,14 +16,23 @@ describe('NoteCardBody', () => {
const markdownHeaderPrefix = '# '; // translates to an h1 in markdown
const noteText = 'This is a note';
const rawNote = `${markdownHeaderPrefix} ${noteText}`;
const theme = () => ({ eui: euiDarkVars, darkMode: true });
test('renders correctly against snapshot', () => {
const wrapper = shallow(<NoteCardBody rawNote={rawNote} />);
const wrapper = shallow(
<ThemeProvider theme={theme}>
<NoteCardBody rawNote={rawNote} />
</ThemeProvider>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('it renders the text of the note in an h1', () => {
const wrapper = mount(<NoteCardBody rawNote={rawNote} />);
const wrapper = mount(
<ThemeProvider theme={theme}>
<NoteCardBody rawNote={rawNote} />
</ThemeProvider>
);
expect(
wrapper

View file

@ -6,6 +6,8 @@
import * as React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { ThemeProvider } from 'styled-components';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { Note } from '../../../lib/note';
@ -13,6 +15,7 @@ import { NoteCards } from '.';
describe('NoteCards', () => {
const noteIds = ['abc', 'def'];
const theme = () => ({ eui: euiDarkVars, darkMode: true });
const getNotesByIds = (_: string[]): Note[] => [
{
@ -37,15 +40,17 @@ describe('NoteCards', () => {
test('it renders the notes column when noteIds are specified', () => {
const wrapper = mountWithIntl(
<NoteCards
associateNote={jest.fn()}
getNotesByIds={getNotesByIds}
getNewNoteId={jest.fn()}
noteIds={noteIds}
showAddNote={true}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
/>
<ThemeProvider theme={theme}>
<NoteCards
associateNote={jest.fn()}
getNotesByIds={getNotesByIds}
getNewNoteId={jest.fn()}
noteIds={noteIds}
showAddNote={true}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="notes"]').exists()).toEqual(true);
@ -53,15 +58,17 @@ describe('NoteCards', () => {
test('it does NOT render the notes column when noteIds are NOT specified', () => {
const wrapper = mountWithIntl(
<NoteCards
associateNote={jest.fn()}
getNotesByIds={getNotesByIds}
getNewNoteId={jest.fn()}
noteIds={[]}
showAddNote={true}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
/>
<ThemeProvider theme={theme}>
<NoteCards
associateNote={jest.fn()}
getNotesByIds={getNotesByIds}
getNewNoteId={jest.fn()}
noteIds={[]}
showAddNote={true}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="notes"]').exists()).toEqual(false);
@ -69,15 +76,17 @@ describe('NoteCards', () => {
test('renders note cards', () => {
const wrapper = mountWithIntl(
<NoteCards
associateNote={jest.fn()}
getNotesByIds={getNotesByIds}
getNewNoteId={jest.fn()}
noteIds={noteIds}
showAddNote={true}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
/>
<ThemeProvider theme={theme}>
<NoteCards
associateNote={jest.fn()}
getNotesByIds={getNotesByIds}
getNewNoteId={jest.fn()}
noteIds={noteIds}
showAddNote={true}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
/>
</ThemeProvider>
);
expect(
@ -92,15 +101,17 @@ describe('NoteCards', () => {
test('it shows controls for adding notes when showAddNote is true', () => {
const wrapper = mountWithIntl(
<NoteCards
associateNote={jest.fn()}
getNotesByIds={getNotesByIds}
getNewNoteId={jest.fn()}
noteIds={noteIds}
showAddNote={true}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
/>
<ThemeProvider theme={theme}>
<NoteCards
associateNote={jest.fn()}
getNotesByIds={getNotesByIds}
getNewNoteId={jest.fn()}
noteIds={noteIds}
showAddNote={true}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="add-note"]').exists()).toEqual(true);
@ -108,15 +119,17 @@ describe('NoteCards', () => {
test('it does NOT show controls for adding notes when showAddNote is false', () => {
const wrapper = mountWithIntl(
<NoteCards
associateNote={jest.fn()}
getNotesByIds={getNotesByIds}
getNewNoteId={jest.fn()}
noteIds={noteIds}
showAddNote={false}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
/>
<ThemeProvider theme={theme}>
<NoteCards
associateNote={jest.fn()}
getNotesByIds={getNotesByIds}
getNewNoteId={jest.fn()}
noteIds={noteIds}
showAddNote={false}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="add-note"]').exists()).toEqual(false);

View file

@ -187,7 +187,7 @@ describe('NotePreview', () => {
.find('[data-test-subj="posted"]')
.first()
.text()
).toEqual('Posted: --');
).toEqual(`Posted: ${getEmptyValue()}`);
});
test('it renders placeholder text when updated is null', () => {
@ -202,7 +202,7 @@ describe('NotePreview', () => {
.find('[data-test-subj="posted"]')
.first()
.text()
).toEqual('Posted: --');
).toEqual(`Posted: ${getEmptyValue()}`);
});
});
});

View file

@ -5,9 +5,11 @@
*/
import { EuiButtonIconProps } from '@elastic/eui';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { cloneDeep, omit } from 'lodash/fp';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import * as React from 'react';
import { ThemeProvider } from 'styled-components';
import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timelines_page';
import { mockTimelineResults } from '../../../mock/timeline_results';
@ -18,6 +20,7 @@ import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants';
jest.mock('../../../lib/settings/use_kibana_ui_setting');
describe('#getActionsColumns', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
let mockResults: OpenTimelineResult[];
beforeEach(() => {
@ -26,23 +29,25 @@ describe('#getActionsColumns', () => {
test('it renders the delete timeline (trash icon) when showDeleteAction is true (because showExtendedColumnsAndActions is true)', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toBe(true);
@ -50,23 +55,25 @@ describe('#getActionsColumns', () => {
test('it does NOT render the delete timeline (trash icon) when showDeleteAction is false (because showExtendedColumnsAndActions is false)', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toBe(false);
@ -74,22 +81,24 @@ describe('#getActionsColumns', () => {
test('it does NOT render the delete timeline (trash icon) when deleteTimelines is not provided', () => {
const wrapper = mountWithIntl(
<TimelinesTable
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toBe(false);
@ -130,23 +139,25 @@ describe('#getActionsColumns', () => {
test('it renders an enabled the open duplicate button if the timeline has have a saved object id', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
const props = wrapper
@ -161,23 +172,25 @@ describe('#getActionsColumns', () => {
const onOpenTimeline = jest.fn();
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={onOpenTimeline}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={onOpenTimeline}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
wrapper

View file

@ -346,23 +346,25 @@ describe('#getCommonColumns', () => {
describe('Timeline Name column', () => {
test('it renders the expected column name', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -375,23 +377,25 @@ describe('#getCommonColumns', () => {
test('it renders the title when the timeline has a title and a saved object id', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -567,23 +571,25 @@ describe('#getCommonColumns', () => {
test('it renders a hyperlink when the timeline has a saved object id', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -631,23 +637,25 @@ describe('#getCommonColumns', () => {
const onOpenTimeline = jest.fn();
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={onOpenTimeline}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={onOpenTimeline}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
wrapper
@ -665,23 +673,25 @@ describe('#getCommonColumns', () => {
describe('Description column', () => {
test('it renders the expected column name', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -694,23 +704,25 @@ describe('#getCommonColumns', () => {
test('it renders the description when the timeline has a description', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -725,23 +737,25 @@ describe('#getCommonColumns', () => {
const missingDescription: OpenTimelineResult[] = [omit('description', { ...mockResults[0] })];
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={missingDescription}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={missingDescription.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={missingDescription}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={missingDescription.length}
/>
</ThemeProvider>
);
expect(
wrapper
@ -757,23 +771,25 @@ describe('#getCommonColumns', () => {
];
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={justWhitespaceDescription}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={justWhitespaceDescription.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={justWhitespaceDescription}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={justWhitespaceDescription.length}
/>
</ThemeProvider>
);
expect(
wrapper
@ -787,23 +803,25 @@ describe('#getCommonColumns', () => {
describe('Last Modified column', () => {
test('it renders the expected column name', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -816,23 +834,25 @@ describe('#getCommonColumns', () => {
test('it renders the last modified (updated) date when the timeline has an updated property', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -848,23 +868,25 @@ describe('#getCommonColumns', () => {
const missingUpdated: OpenTimelineResult[] = [omit('updated', { ...mockResults[0] })];
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={missingUpdated}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={missingUpdated.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={missingUpdated}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={missingUpdated.length}
/>
</ThemeProvider>
);
expect(
wrapper

View file

@ -4,9 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { cloneDeep, omit } from 'lodash/fp';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import * as React from 'react';
import { ThemeProvider } from 'styled-components';
import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timelines_page';
import { getEmptyValue } from '../../empty_value';
@ -21,6 +23,7 @@ import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants';
jest.mock('../../../lib/settings/use_kibana_ui_setting');
describe('#getExtendedColumns', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
let mockResults: OpenTimelineResult[];
beforeEach(() => {
@ -30,23 +33,25 @@ describe('#getExtendedColumns', () => {
describe('Modified By column', () => {
test('it renders the expected column name', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -59,23 +64,25 @@ describe('#getExtendedColumns', () => {
test('it renders the username when the timeline has an updatedBy property', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -90,23 +97,25 @@ describe('#getExtendedColumns', () => {
const missingUpdatedBy: OpenTimelineResult[] = [omit('updatedBy', { ...mockResults[0] })];
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={missingUpdatedBy}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={missingUpdatedBy.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={missingUpdatedBy}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={missingUpdatedBy.length}
/>
</ThemeProvider>
);
expect(

View file

@ -4,10 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { cloneDeep, omit } from 'lodash/fp';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import 'jest-styled-components';
import * as React from 'react';
import { ThemeProvider } from 'styled-components';
import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timelines_page';
import { mockTimelineResults } from '../../../mock/timeline_results';
@ -18,6 +20,7 @@ import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants';
jest.mock('../../../lib/settings/use_kibana_ui_setting');
describe('#getActionsColumns', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
let mockResults: OpenTimelineResult[];
beforeEach(() => {
@ -26,23 +29,25 @@ describe('#getActionsColumns', () => {
test('it renders the pinned events header icon', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="pinned-event-header-icon"]').exists()).toBe(true);
@ -76,23 +81,25 @@ describe('#getActionsColumns', () => {
test('it renders the notes count header icon', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="notes-count-header-icon"]').exists()).toBe(true);
@ -126,23 +133,25 @@ describe('#getActionsColumns', () => {
test('it renders the favorites header icon', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="favorites-header-icon"]').exists()).toBe(true);

View file

@ -4,9 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { cloneDeep } from 'lodash/fp';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import * as React from 'react';
import { ThemeProvider } from 'styled-components';
import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timelines_page';
import { mockTimelineResults } from '../../../mock/timeline_results';
@ -19,6 +21,7 @@ import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants';
jest.mock('../../../lib/settings/use_kibana_ui_setting');
describe('TimelinesTable', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
let mockResults: OpenTimelineResult[];
beforeEach(() => {
@ -27,23 +30,25 @@ describe('TimelinesTable', () => {
test('it renders the select all timelines header checkbox when showExtendedColumnsAndActions is true', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -56,23 +61,25 @@ describe('TimelinesTable', () => {
test('it does NOT render the select all timelines header checkbox when showExtendedColumnsAndActions is false', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -85,23 +92,25 @@ describe('TimelinesTable', () => {
test('it renders the Modified By column when showExtendedColumnsAndActions is true ', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -114,23 +123,25 @@ describe('TimelinesTable', () => {
test('it renders the notes column in the position of the Modified By column when showExtendedColumnsAndActions is false', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -145,23 +156,25 @@ describe('TimelinesTable', () => {
test('it renders the delete timeline (trash icon) when showExtendedColumnsAndActions is true', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -174,23 +187,25 @@ describe('TimelinesTable', () => {
test('it does NOT render the delete timeline (trash icon) when showExtendedColumnsAndActions is false', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -203,23 +218,25 @@ describe('TimelinesTable', () => {
test('it renders the rows per page selector when showExtendedColumnsAndActions is true', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -232,23 +249,25 @@ describe('TimelinesTable', () => {
test('it does NOT render the rows per page selector when showExtendedColumnsAndActions is false', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -263,23 +282,25 @@ describe('TimelinesTable', () => {
const defaultPageSize = 123;
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={defaultPageSize}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={defaultPageSize}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={defaultPageSize}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={defaultPageSize}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -292,23 +313,25 @@ describe('TimelinesTable', () => {
test('it sorts the Last Modified column in descending order when showExtendedColumnsAndActions is true ', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -321,23 +344,25 @@ describe('TimelinesTable', () => {
test('it sorts the Last Modified column in descending order when showExtendedColumnsAndActions is false ', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={false}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
expect(
@ -381,23 +406,25 @@ describe('TimelinesTable', () => {
const onTableChange = jest.fn();
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={onTableChange}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={onTableChange}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
wrapper
@ -417,23 +444,25 @@ describe('TimelinesTable', () => {
const onSelectionChange = jest.fn();
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={onSelectionChange}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={onSelectionChange}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
wrapper
@ -448,23 +477,25 @@ describe('TimelinesTable', () => {
test('it enables the table loading animation when isLoading is true', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={true}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={true}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
const props = wrapper
@ -477,23 +508,25 @@ describe('TimelinesTable', () => {
test('it disables the table loading animation when isLoading is false', () => {
const wrapper = mountWithIntl(
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
<ThemeProvider theme={theme}>
<TimelinesTable
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
loading={false}
itemIdToExpandedNotesRowMap={{}}
onOpenTimeline={jest.fn()}
onSelectionChange={jest.fn()}
onTableChange={jest.fn()}
onToggleShowNotes={jest.fn()}
pageIndex={0}
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
searchResults={mockResults}
showExtendedColumnsAndActions={true}
sortDirection={DEFAULT_SORT_DIRECTION}
sortField={DEFAULT_SORT_FIELD}
totalSearchResultsCount={mockResults.length}
/>
</ThemeProvider>
);
const props = wrapper

View file

@ -144,13 +144,19 @@ describe('AddToKql Component', () => {
expect(store.getState().network.page).toEqual({
queries: {
topNFlow: {
topNFlowDestination: {
activePage: 0,
limit: 10,
flowDirection: 'uniDirectional',
flowTarget: 'source',
topNFlowSort: {
field: 'bytes',
field: 'bytes_out',
direction: 'desc',
},
},
topNFlowSource: {
activePage: 0,
limit: 10,
topNFlowSort: {
field: 'bytes_out',
direction: 'desc',
},
},

View file

@ -154,6 +154,7 @@ const getEventsColumns = (pageType: hostsModel.HostsType): EventsTableColumns =>
) : (
getEmptyTagValue()
),
width: '15%',
},
{
field: 'node',
@ -167,6 +168,7 @@ const getEventsColumns = (pageType: hostsModel.HostsType): EventsTableColumns =>
idPrefix: `host-${pageType}-events-table-${node._id}`,
render: item => <HostDetailsLink hostName={item} />,
}),
width: '15%',
},
{
field: 'node',
@ -180,7 +182,7 @@ const getEventsColumns = (pageType: hostsModel.HostsType): EventsTableColumns =>
attrName: 'event.module',
idPrefix: `host-${pageType}-events-table-${node._id}`,
})}
{'/'}
{' / '}
{getRowItemDraggables({
rowItems: getOr(null, 'event.dataset', node),
attrName: 'event.dataset',
@ -254,7 +256,7 @@ const getEventsColumns = (pageType: hostsModel.HostsType): EventsTableColumns =>
name: i18n.MESSAGE,
sortable: false,
truncateText: true,
width: '25%',
width: '15%',
render: node => {
const message = getOr(null, 'message[0]', node);
return message != null ? (

View file

@ -41,7 +41,7 @@ export const MESSAGE = i18n.translate('xpack.siem.eventsTable.messageTitle', {
});
export const EVENT_MODULE_DATASET = i18n.translate('xpack.siem.eventsTable.moduleDatasetTitle', {
defaultMessage: 'Module/dataset',
defaultMessage: 'Module / dataset',
});
export const USER = i18n.translate('xpack.siem.eventsTable.userTitle', {

View file

@ -151,6 +151,7 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [
attrName: 'process.name',
idPrefix: `uncommon-process-table-${node._id}-processName`,
}),
width: '15%',
},
{
name: i18n.NUMBER_OF_HOSTS,
@ -175,6 +176,7 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [
idPrefix: `uncommon-process-table-${node._id}-processHost`,
render: item => <HostDetailsLink hostName={item} />,
}),
width: '15%',
},
{
name: i18n.LAST_COMMAND,
@ -187,6 +189,7 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [
idPrefix: `uncommon-process-table-${node._id}-processArgs`,
displayCount: 1, // TODO: Change this back once we have improved the UI
}),
width: '35%',
},
{
name: i18n.LAST_USER,

View file

@ -11,18 +11,41 @@ exports[`NetworkTopNFlow Table Component rendering it renders the default Networ
"node": Object {
"destination": null,
"network": Object {
"bytes": 3826633497,
"direction": Array [
"inbound",
],
"packets": 4185805,
"bytes_in": 3826633497,
"bytes_out": 1083495734,
},
"source": Object {
"count": 1,
"autonomous_system": Object {
"name": "Google, Inc",
"number": 15169,
},
"destination_ips": 12,
"domain": Array [
"test.domain.com",
],
"flows": 12345,
"ip": "8.8.8.8",
"location": Object {
"flowTarget": "source",
"geo": Object {
"city_name": Array [
"Mountain View",
],
"continent_name": Array [
"North America",
],
"country_iso_code": Array [
"US",
],
"country_name": null,
"region_iso_code": Array [
"US-CA",
],
"region_name": Array [
"California",
],
},
},
},
},
},
@ -33,26 +56,50 @@ exports[`NetworkTopNFlow Table Component rendering it renders the default Networ
"node": Object {
"destination": null,
"network": Object {
"bytes": 325909849,
"direction": Array [
"inbound",
"outbound",
],
"packets": 221494,
"bytes_in": 3826633497,
"bytes_out": 1083495734,
},
"source": Object {
"autonomous_system": Object {
"name": "TM Net, Internet Service Provider",
"number": 4788,
},
"destination_ips": 12,
"domain": Array [
"test.domain.net",
"test.old.domain.net",
],
"flows": 12345,
"ip": "9.9.9.9",
"location": Object {
"flowTarget": "source",
"geo": Object {
"city_name": Array [
"Petaling Jaya",
],
"continent_name": Array [
"Asia",
],
"country_iso_code": Array [
"MY",
],
"country_name": null,
"region_iso_code": Array [
"MY-10",
],
"region_name": Array [
"Selangor",
],
},
},
},
},
},
]
}
fakeTotalCount={50}
id="topNFlow"
flowTargeted="source"
id="topNFlowSource"
indexPattern={
Object {
"fields": Array [

View file

@ -4,91 +4,96 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash/fp';
import numeral from '@elastic/numeral';
import { get, isEmpty } from 'lodash/fp';
import React from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
import { CountryFlag } from '../../../source_destination/country_flag';
import {
FlowDirection,
FlowTarget,
TopNFlowNetworkEcsField,
AutonomousSystemItem,
FlowTargetNew,
NetworkTopNFlowEdges,
TopNFlowItem,
TopNFlowNetworkEcsField,
} from '../../../../graphql/types';
import { assertUnreachable } from '../../../../lib/helpers';
import { escapeQueryValue } from '../../../../lib/keury';
import { networkModel } from '../../../../store';
import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper';
import { escapeDataProviderId } from '../../../drag_and_drop/helpers';
import { defaultToEmptyTag, getEmptyTagValue } from '../../../empty_value';
import { getEmptyTagValue } from '../../../empty_value';
import { IPDetailsLink } from '../../../links';
import { Columns } from '../../../load_more_table';
import { IS_OPERATOR } from '../../../timeline/data_providers/data_provider';
import { Provider } from '../../../timeline/data_providers/provider';
import { AddToKql } from '../../add_to_kql';
import * as i18n from './translations';
import { getRowItemDraggables } from '../../../tables/helpers';
import { getRowItemDraggable, getRowItemDraggables } from '../../../tables/helpers';
import { PreferenceFormattedBytes } from '../../../formatted_bytes';
export type NetworkTopNFlowColumns = [
Columns<NetworkTopNFlowEdges>,
Columns<NetworkTopNFlowEdges>,
Columns<TopNFlowNetworkEcsField['direction']>,
Columns<TopNFlowNetworkEcsField['bytes']>,
Columns<TopNFlowNetworkEcsField['packets']>,
Columns<TopNFlowItem['count']>
Columns<NetworkTopNFlowEdges>,
Columns<TopNFlowNetworkEcsField['bytes_in']>,
Columns<TopNFlowNetworkEcsField['bytes_out']>,
Columns<NetworkTopNFlowEdges>,
Columns<NetworkTopNFlowEdges>
];
export const getNetworkTopNFlowColumns = (
indexPattern: StaticIndexPattern,
flowDirection: FlowDirection,
flowTarget: FlowTarget,
flowTarget: FlowTargetNew,
type: networkModel.NetworkType,
tableId: string
): NetworkTopNFlowColumns => [
{
name: getIpTitle(flowTarget),
truncateText: false,
hideForMobile: false,
name: i18n.IP_TITLE,
render: ({ node }) => {
const ipAttr = `${flowTarget}.ip`;
const ip: string | null = get(ipAttr, node);
const id = escapeDataProviderId(`${tableId}-table-${flowTarget}-${flowDirection}-ip-${ip}`);
const geoAttr = `${flowTarget}.location.geo.country_iso_code[0]`;
const geo: string | null = get(geoAttr, node);
const id = escapeDataProviderId(`${tableId}-table-${flowTarget}-ip-${ip}`);
if (ip != null) {
return (
<DraggableWrapper
key={id}
dataProvider={{
and: [],
enabled: true,
id,
name: ip,
excluded: false,
kqlQuery: '',
queryMatch: { field: ipAttr, value: ip, operator: IS_OPERATOR },
}}
render={(dataProvider, _, snapshot) =>
snapshot.isDragging ? (
<DragEffects>
<Provider dataProvider={dataProvider} />
</DragEffects>
) : (
<IPDetailsLink ip={ip} />
)
}
/>
<>
<DraggableWrapper
key={id}
dataProvider={{
and: [],
enabled: true,
id,
name: ip,
excluded: false,
kqlQuery: '',
queryMatch: { field: ipAttr, value: ip, operator: IS_OPERATOR },
}}
render={(dataProvider, _, snapshot) =>
snapshot.isDragging ? (
<DragEffects>
<Provider dataProvider={dataProvider} />
</DragEffects>
) : (
<IPDetailsLink ip={ip} />
)
}
/>
{geo && (
<>
{' '}
<CountryFlag countryCode={geo} /> {geo}
</>
)}
</>
);
} else {
return getEmptyTagValue();
}
},
width: '15%',
},
{
name: i18n.DOMAIN,
truncateText: false,
hideForMobile: false,
render: ({ node }) => {
const domainAttr = `${flowTarget}.domain`;
const ipAttr = `${flowTarget}.ip`;
@ -96,7 +101,7 @@ export const getNetworkTopNFlowColumns = (
const ip: string | null = get(ipAttr, node);
if (Array.isArray(domains) && domains.length > 0) {
const id = escapeDataProviderId(`${tableId}-table-${ip}-${flowDirection}`);
const id = escapeDataProviderId(`${tableId}-table-${ip}`);
return getRowItemDraggables({
rowItems: domains,
attrName: domainAttr,
@ -107,38 +112,41 @@ export const getNetworkTopNFlowColumns = (
return getEmptyTagValue();
}
},
width: '20%',
},
{
field: 'node.network.direction',
name: i18n.DIRECTION,
truncateText: false,
hideForMobile: false,
render: directions =>
isEmpty(directions)
? getEmptyTagValue()
: directions &&
directions.map((direction, index) => (
<AddToKql
indexPattern={indexPattern}
key={escapeDataProviderId(
`${tableId}-table-${flowTarget}-${flowDirection}-direction-${direction}`
)}
expression={`network.direction: "${escapeQueryValue(direction)}"`}
componentFilterType="network"
type={type}
>
<>
{defaultToEmptyTag(direction)}
{index < directions.length - 1 ? '\u00A0' : null}
</>
</AddToKql>
)),
name: i18n.AUTONOMOUS_SYSTEM,
render: ({ node, cursor: { value: ipAddress } }) => {
const asAttr = `${flowTarget}.autonomous_system`;
const as: AutonomousSystemItem | null = get(asAttr, node);
if (as != null) {
const id = escapeDataProviderId(`${tableId}-table-${flowTarget}-ip-${ipAddress}`);
return (
<>
{as.name &&
getRowItemDraggable({
rowItem: as.name,
attrName: `${flowTarget}.as.organization.name`,
idPrefix: `${id}-name`,
})}
{as.number &&
getRowItemDraggable({
rowItem: `${as.number}`,
attrName: `${flowTarget}.as.number`,
idPrefix: `${id}-number`,
})}
</>
);
} else {
return getEmptyTagValue();
}
},
width: '20%',
},
{
field: 'node.network.bytes',
name: i18n.BYTES,
truncateText: false,
hideForMobile: false,
align: 'right',
field: 'node.network.bytes_in',
name: i18n.BYTES_IN,
sortable: true,
render: bytes => {
if (bytes != null) {
@ -149,28 +157,39 @@ export const getNetworkTopNFlowColumns = (
},
},
{
field: 'node.network.packets',
name: i18n.PACKETS,
truncateText: false,
hideForMobile: false,
align: 'right',
field: 'node.network.bytes_out',
name: i18n.BYTES_OUT,
sortable: true,
render: packets => {
if (packets != null) {
return numeral(packets).format('0,000');
render: bytes => {
if (bytes != null) {
return <PreferenceFormattedBytes value={bytes} />;
} else {
return getEmptyTagValue();
}
},
},
{
field: `node.${flowTarget}.count`,
name: getUniqueTitle(flowTarget),
truncateText: false,
hideForMobile: false,
align: 'right',
field: `node.${flowTarget}.flows`,
name: i18n.FLOWS,
sortable: true,
render: ipCount => {
if (ipCount != null) {
return numeral(ipCount).format('0,000');
render: flows => {
if (flows != null) {
return numeral(flows).format('0,000');
} else {
return getEmptyTagValue();
}
},
},
{
align: 'right',
field: `node.${flowTarget}.${getOppositeField(flowTarget)}_ips`,
name: flowTarget === FlowTargetNew.source ? i18n.DESTINATION_IPS : i18n.SOURCE_IPS,
sortable: true,
render: ips => {
if (ips != null) {
return numeral(ips).format('0,000');
} else {
return getEmptyTagValue();
}
@ -178,30 +197,5 @@ export const getNetworkTopNFlowColumns = (
},
];
const getIpTitle = (flowTarget: FlowTarget) => {
switch (flowTarget) {
case FlowTarget.source:
return i18n.SOURCE_IP;
case FlowTarget.destination:
return i18n.DESTINATION_IP;
case FlowTarget.client:
return i18n.CLIENT_IP;
case FlowTarget.server:
return i18n.SERVER_IP;
}
assertUnreachable(flowTarget);
};
const getUniqueTitle = (flowTarget: FlowTarget) => {
switch (flowTarget) {
case FlowTarget.source:
return i18n.UNIQUE_DESTINATION_IP;
case FlowTarget.destination:
return i18n.UNIQUE_SOURCE_IP;
case FlowTarget.client:
return i18n.UNIQUE_SERVER_IP;
case FlowTarget.server:
return i18n.UNIQUE_CLIENT_IP;
}
assertUnreachable(flowTarget);
};
const getOppositeField = (flowTarget: FlowTargetNew): FlowTargetNew =>
flowTarget === FlowTargetNew.source ? FlowTargetNew.destination : FlowTargetNew.source;

View file

@ -11,20 +11,18 @@ import * as React from 'react';
import { MockedProvider } from 'react-apollo/test-utils';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { FlowDirection } from '../../../../graphql/types';
import { FlowTargetNew } from '../../../../graphql/types';
import {
apolloClientObservable,
mockIndexPattern,
mockGlobalState,
mockIndexPattern,
TestProviders,
} from '../../../../mock';
import { createStore, networkModel, State } from '../../../../store';
import { NetworkTopNFlowTable, NetworkTopNFlowTableId } from '.';
import { NetworkTopNFlowTable } from '.';
import { mockData } from './mock';
jest.mock('../../../../lib/settings/use_kibana_ui_setting');
describe('NetworkTopNFlow Table Component', () => {
const loadPage = jest.fn();
const state: State = mockGlobalState;
@ -42,7 +40,8 @@ describe('NetworkTopNFlow Table Component', () => {
<NetworkTopNFlowTable
data={mockData.NetworkTopNFlow.edges}
fakeTotalCount={getOr(50, 'fakeTotalCount', mockData.NetworkTopNFlow.pageInfo)}
id="topNFlow"
flowTargeted={FlowTargetNew.source}
id="topNFlowSource"
indexPattern={mockIndexPattern}
loading={false}
loadPage={loadPage}
@ -61,97 +60,6 @@ describe('NetworkTopNFlow Table Component', () => {
});
});
describe('Direction', () => {
test('when you click on the bi-directional button, it get selected', () => {
const event = {
target: { name: 'direction', value: FlowDirection.biDirectional },
};
const wrapper = mount(
<MockedProvider>
<TestProviders store={store}>
<NetworkTopNFlowTable
data={mockData.NetworkTopNFlow.edges}
fakeTotalCount={getOr(50, 'fakeTotalCount', mockData.NetworkTopNFlow.pageInfo)}
id="topNFlow"
indexPattern={mockIndexPattern}
loading={false}
loadPage={loadPage}
showMorePagesIndicator={getOr(
false,
'showMorePagesIndicator',
mockData.NetworkTopNFlow.pageInfo
)}
totalCount={mockData.NetworkTopNFlow.totalCount}
type={networkModel.NetworkType.page}
/>
</TestProviders>
</MockedProvider>
);
wrapper
.find(`[data-test-subj="${FlowDirection.biDirectional}"]`)
.first()
.simulate('click', event);
wrapper.update();
expect(
wrapper
.find(`[data-test-subj="${FlowDirection.biDirectional}"]`)
.first()
.render()
.hasClass('euiFilterButton-hasActiveFilters')
).toEqual(true);
});
});
describe('Sorting by type', () => {
test('when you click on the sorting dropdown, and picked destination', () => {
const wrapper = mount(
<MockedProvider>
<TestProviders store={store}>
<NetworkTopNFlowTable
data={mockData.NetworkTopNFlow.edges}
fakeTotalCount={getOr(50, 'fakeTotalCount', mockData.NetworkTopNFlow.pageInfo)}
id="topNFlow"
indexPattern={mockIndexPattern}
loading={false}
loadPage={loadPage}
showMorePagesIndicator={getOr(
false,
'showMorePagesIndicator',
mockData.NetworkTopNFlow.pageInfo
)}
totalCount={mockData.NetworkTopNFlow.totalCount}
type={networkModel.NetworkType.page}
/>
</TestProviders>
</MockedProvider>
);
wrapper
.find(`[data-test-subj="${NetworkTopNFlowTableId}-select-flow-target"] button`)
.first()
.simulate('click');
wrapper.update();
wrapper
.find(`button#${NetworkTopNFlowTableId}-select-flow-target-destination`)
.first()
.simulate('click');
expect(
wrapper
.find(`[data-test-subj="${NetworkTopNFlowTableId}-select-flow-target"] button`)
.first()
.text()
.toLocaleLowerCase()
).toEqual('by destination ip');
});
});
describe('Sorting on Table', () => {
test('when you click on the column header, you should show the sorting icon', () => {
const wrapper = mount(
@ -160,7 +68,8 @@ describe('NetworkTopNFlow Table Component', () => {
<NetworkTopNFlowTable
data={mockData.NetworkTopNFlow.edges}
fakeTotalCount={getOr(50, 'fakeTotalCount', mockData.NetworkTopNFlow.pageInfo)}
id="topNFlow"
flowTargeted={FlowTargetNew.source}
id="topNFlowSource"
indexPattern={mockIndexPattern}
loading={false}
loadPage={loadPage}
@ -175,9 +84,9 @@ describe('NetworkTopNFlow Table Component', () => {
</TestProviders>
</MockedProvider>
);
expect(store.getState().network.page.queries!.topNFlow.topNFlowSort).toEqual({
expect(store.getState().network.page.queries.topNFlowSource.topNFlowSort).toEqual({
direction: 'desc',
field: 'bytes',
field: 'bytes_out',
});
wrapper
@ -187,22 +96,22 @@ describe('NetworkTopNFlow Table Component', () => {
wrapper.update();
expect(store.getState().network.page.queries!.topNFlow.topNFlowSort).toEqual({
expect(store.getState().network.page.queries.topNFlowSource.topNFlowSort).toEqual({
direction: 'asc',
field: 'packets',
field: 'bytes_out',
});
expect(
wrapper
.find('.euiTable thead tr th button')
.first()
.text()
).toEqual('BytesClick to sort in ascending order');
).toEqual('Bytes inClick to sort in ascending order');
expect(
wrapper
.find('.euiTable thead tr th button')
.at(1)
.text()
).toEqual('PacketsClick to sort in descending order');
).toEqual('Bytes outClick to sort in descending order');
expect(
wrapper
.find('.euiTable thead tr th button')

View file

@ -3,7 +3,7 @@
* 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 { EuiFlexItem } from '@elastic/eui';
import { isEqual, last } from 'lodash/fp';
import React from 'react';
import { connect } from 'react-redux';
@ -13,25 +13,22 @@ import { StaticIndexPattern } from 'ui/index_patterns';
import { networkActions } from '../../../../store/actions';
import {
FlowDirection,
FlowTarget,
Direction,
FlowTargetNew,
NetworkTopNFlowEdges,
NetworkTopNFlowFields,
NetworkTopNFlowSortField,
} from '../../../../graphql/types';
import { networkModel, networkSelectors, State } from '../../../../store';
import { FlowDirectionSelect } from '../../../flow_controls/flow_direction_select';
import { FlowTargetSelect } from '../../../flow_controls/flow_target_select';
import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table';
import { getNetworkTopNFlowColumns } from './columns';
import * as i18n from './translations';
const tableType = networkModel.NetworkTableType.topNFlow;
interface OwnProps {
data: NetworkTopNFlowEdges[];
fakeTotalCount: number;
flowTargeted: FlowTargetNew;
id: string;
indexPattern: StaticIndexPattern;
loading: boolean;
@ -43,9 +40,7 @@ interface OwnProps {
interface NetworkTopNFlowTableReduxProps {
limit: number;
flowDirection: FlowDirection;
topNFlowSort: NetworkTopNFlowSortField;
flowTarget: FlowTarget;
}
interface NetworkTopNFlowTableDispatchProps {
@ -53,20 +48,15 @@ interface NetworkTopNFlowTableDispatchProps {
activePage: number;
tableType: networkModel.NetworkTableType;
}>;
updateTopNFlowDirection: ActionCreator<{
flowDirection: FlowDirection;
networkType: networkModel.NetworkType;
}>;
updateTopNFlowLimit: ActionCreator<{
limit: number;
networkType: networkModel.NetworkType;
tableType: networkModel.TopNTableType;
}>;
updateTopNFlowSort: ActionCreator<{
topNFlowSort: NetworkTopNFlowSortField;
networkType: networkModel.NetworkType;
}>;
updateTopNFlowTarget: ActionCreator<{
flowTarget: FlowTarget;
tableType: networkModel.TopNTableType;
}>;
}
@ -85,15 +75,14 @@ const rowItems: ItemsPerRow[] = [
},
];
export const NetworkTopNFlowTableId = 'networkTopNFlow-top-talkers';
export const NetworkTopNFlowTableId = 'networkTopSourceFlow-top-talkers';
class NetworkTopNFlowTableComponent extends React.PureComponent<NetworkTopNFlowTableProps> {
public render() {
const {
data,
flowDirection,
flowTarget,
fakeTotalCount,
flowTargeted,
id,
indexPattern,
limit,
@ -104,64 +93,43 @@ class NetworkTopNFlowTableComponent extends React.PureComponent<NetworkTopNFlowT
totalCount,
type,
updateTopNFlowLimit,
updateTopNFlowTarget,
updateTableActivePage,
} = this.props;
let tableType: networkModel.TopNTableType;
let headerTitle: string;
if (flowTargeted === FlowTargetNew.source) {
headerTitle = i18n.SOURCE_IP;
tableType = networkModel.NetworkTableType.topNFlowSource;
} else {
headerTitle = i18n.DESTINATION_IP;
tableType = networkModel.NetworkTableType.topNFlowDestination;
}
const field =
topNFlowSort.field === NetworkTopNFlowFields.ipCount
? `node.${flowTarget}.count`
: `node.network.${topNFlowSort.field}`;
topNFlowSort.field === NetworkTopNFlowFields.bytes_out ||
topNFlowSort.field === NetworkTopNFlowFields.bytes_in
? `node.network.${topNFlowSort.field}`
: `node.${flowTargeted}.${topNFlowSort.field}`;
return (
<PaginatedTable
columns={getNetworkTopNFlowColumns(
indexPattern,
flowDirection,
flowTarget,
flowTargeted,
type,
NetworkTopNFlowTableId
)}
headerCount={totalCount}
headerSupplement={
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<SelectTypeItem
grow={false}
data-test-subj={`${NetworkTopNFlowTableId}-select-flow-target`}
>
<FlowTargetSelect
id={NetworkTopNFlowTableId}
isLoading={loading}
selectedDirection={flowDirection}
selectedTarget={flowTarget}
displayTextOverride={[
i18n.BY_SOURCE_IP,
i18n.BY_DESTINATION_IP,
i18n.BY_CLIENT_IP,
i18n.BY_SERVER_IP,
]}
updateFlowTargetAction={updateTopNFlowTarget}
/>
</SelectTypeItem>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FlowDirectionSelect
selectedDirection={flowDirection}
onChangeDirection={this.onChangeTopNFlowDirection}
/>
</EuiFlexItem>
</EuiFlexGroup>
}
headerTitle={i18n.TOP_TALKERS}
headerTitle={headerTitle}
headerUnit={i18n.UNIT(totalCount)}
id={id}
itemsPerRow={rowItems}
limit={limit}
loading={loading}
loadPage={newActivePage => loadPage(newActivePage)}
onChange={this.onChange}
onChange={criteria => this.onChange(criteria, tableType)}
pageOfItems={data}
showMorePagesIndicator={showMorePagesIndicator}
sorting={{ field, direction: topNFlowSort.direction }}
@ -173,47 +141,42 @@ class NetworkTopNFlowTableComponent extends React.PureComponent<NetworkTopNFlowT
})
}
updateLimitPagination={newLimit =>
updateTopNFlowLimit({ limit: newLimit, networkType: type })
updateTopNFlowLimit({ limit: newLimit, networkType: type, tableType })
}
updateProps={{ flowDirection, flowTarget, totalCount, topNFlowSort, field }}
updateProps={{ totalCount, topNFlowSort, field }}
/>
);
}
private onChange = (criteria: Criteria) => {
private onChange = (criteria: Criteria, tableType: networkModel.TopNTableType) => {
if (criteria.sort != null) {
const splitField = criteria.sort.field.split('.');
const field = last(splitField) === 'count' ? NetworkTopNFlowFields.ipCount : last(splitField);
const field = last(splitField);
const newSortDirection =
field !== this.props.topNFlowSort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click
const newTopNFlowSort: NetworkTopNFlowSortField = {
field: field as NetworkTopNFlowFields,
direction: criteria.sort.direction,
direction: newSortDirection,
};
if (!isEqual(newTopNFlowSort, this.props.topNFlowSort)) {
this.props.updateTopNFlowSort({
topNFlowSort: newTopNFlowSort,
networkType: this.props.type,
tableType,
});
}
}
};
private onChangeTopNFlowDirection = (flowDirection: FlowDirection) =>
this.props.updateTopNFlowDirection({ flowDirection, networkType: this.props.type });
}
const makeMapStateToProps = () => {
const getNetworkTopNFlowSelector = networkSelectors.topNFlowSelector();
const mapStateToProps = (state: State) => getNetworkTopNFlowSelector(state);
return mapStateToProps;
};
const mapStateToProps = (state: State, ownProps: OwnProps) =>
networkSelectors.topNFlowSelector(ownProps.flowTargeted);
export const NetworkTopNFlowTable = connect(
makeMapStateToProps,
mapStateToProps,
{
updateTopNFlowLimit: networkActions.updateTopNFlowLimit,
updateTopNFlowSort: networkActions.updateTopNFlowSort,
updateTopNFlowTarget: networkActions.updateTopNFlowTarget,
updateTopNFlowDirection: networkActions.updateTopNFlowDirection,
updateTableActivePage: networkActions.updateNetworkPageTableActivePage,
}
)(NetworkTopNFlowTableComponent);

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { NetworkDirectionEcs, NetworkTopNFlowData } from '../../../../graphql/types';
import { NetworkTopNFlowData, FlowTarget } from '../../../../graphql/types';
export const mockData: { NetworkTopNFlow: NetworkTopNFlowData } = {
NetworkTopNFlow: {
@ -13,15 +13,30 @@ export const mockData: { NetworkTopNFlow: NetworkTopNFlowData } = {
{
node: {
source: {
ip: '8.8.8.8',
autonomous_system: {
name: 'Google, Inc',
number: 15169,
},
domain: ['test.domain.com'],
count: 1,
flows: 12345,
destination_ips: 12,
ip: '8.8.8.8',
location: {
geo: {
continent_name: ['North America'],
country_name: null,
country_iso_code: ['US'],
city_name: ['Mountain View'],
region_iso_code: ['US-CA'],
region_name: ['California'],
},
flowTarget: FlowTarget.source,
},
},
destination: null,
network: {
bytes: 3826633497,
packets: 4185805,
direction: [NetworkDirectionEcs.inbound],
bytes_in: 3826633497,
bytes_out: 1083495734,
},
},
cursor: {
@ -31,14 +46,30 @@ export const mockData: { NetworkTopNFlow: NetworkTopNFlowData } = {
{
node: {
source: {
ip: '9.9.9.9',
autonomous_system: {
name: 'TM Net, Internet Service Provider',
number: 4788,
},
domain: ['test.domain.net', 'test.old.domain.net'],
flows: 12345,
destination_ips: 12,
ip: '9.9.9.9',
location: {
geo: {
continent_name: ['Asia'],
country_name: null,
country_iso_code: ['MY'],
city_name: ['Petaling Jaya'],
region_iso_code: ['MY-10'],
region_name: ['Selangor'],
},
flowTarget: FlowTarget.source,
},
},
destination: null,
network: {
bytes: 325909849,
packets: 221494,
direction: [NetworkDirectionEcs.inbound, NetworkDirectionEcs.outbound],
bytes_in: 3826633497,
bytes_out: 1083495734,
},
},
cursor: {

View file

@ -6,10 +6,6 @@
import { i18n } from '@kbn/i18n';
export const TOP_TALKERS = i18n.translate('xpack.siem.networkTopNFlowTable.title', {
defaultMessage: 'Top talkers',
});
export const UNIT = (totalCount: number) =>
i18n.translate('xpack.siem.networkTopNFlowTable.unit', {
values: { totalCount },
@ -17,95 +13,47 @@ export const UNIT = (totalCount: number) =>
});
export const SOURCE_IP = i18n.translate('xpack.siem.networkTopNFlowTable.column.sourceIpTitle', {
defaultMessage: 'Source IP',
defaultMessage: 'Source IPs',
});
export const DESTINATION_IP = i18n.translate(
'xpack.siem.networkTopNFlowTable.column.destinationIpTitle',
{
defaultMessage: 'Destination IP',
defaultMessage: 'Destination IPs',
}
);
export const CLIENT_IP = i18n.translate('xpack.siem.networkTopNFlowTable.column.clientIpTitle', {
defaultMessage: 'Client IP',
export const IP_TITLE = i18n.translate('xpack.siem.networkTopNFlowTable.column.IpTitle', {
defaultMessage: 'IP',
});
export const SERVER_IP = i18n.translate('xpack.siem.networkTopNFlowTable.column.serverIpTitle', {
defaultMessage: 'Server IP',
export const DOMAIN = i18n.translate('xpack.siem.networkTopNFlowTable.column.domainTitle', {
defaultMessage: 'Domain',
});
export const DOMAIN = i18n.translate('xpack.siem.networkTopNFlowTable.column.lastDomainTitle', {
defaultMessage: 'Last domain',
export const BYTES_IN = i18n.translate('xpack.siem.networkTopNFlowTable.column.bytesInTitle', {
defaultMessage: 'Bytes in',
});
export const BYTES = i18n.translate('xpack.siem.networkTopNFlowTable.column.bytesTitle', {
defaultMessage: 'Bytes',
export const BYTES_OUT = i18n.translate('xpack.siem.networkTopNFlowTable.column.bytesOutTitle', {
defaultMessage: 'Bytes out',
});
export const PACKETS = i18n.translate('xpack.siem.networkTopNFlowTable.column.packetsTitle', {
defaultMessage: 'Packets',
export const AUTONOMOUS_SYSTEM = i18n.translate('xpack.siem.networkTopNFlowTable.column.asTitle', {
defaultMessage: 'Autonomous system',
});
export const DIRECTION = i18n.translate('xpack.siem.networkTopNFlowTable.column.directionTitle', {
defaultMessage: 'Direction',
export const FLOWS = i18n.translate('xpack.siem.networkTopNFlowTable.flows', {
defaultMessage: 'Flows',
});
export const UNIQUE_SOURCE_IP = i18n.translate(
'xpack.siem.networkTopNFlowTable.column.uniqueSourceIpsTitle',
{
defaultMessage: 'Unique source IPs',
}
);
export const DESTINATION_IPS = i18n.translate('xpack.siem.networkTopNFlowTable.destinationIps', {
defaultMessage: 'Destination IPs',
});
export const UNIQUE_DESTINATION_IP = i18n.translate(
'xpack.siem.networkTopNFlowTable.column.uniqueDestinationIpsTitle',
{
defaultMessage: 'Unique destination IPs',
}
);
export const UNIQUE_CLIENT_IP = i18n.translate(
'xpack.siem.networkTopNFlowTable.column.uniqueClientIpsTitle',
{
defaultMessage: 'Unique client IPs',
}
);
export const UNIQUE_SERVER_IP = i18n.translate(
'xpack.siem.networkTopNFlowTable.column.uniqueServerIpsTitle',
{
defaultMessage: 'Unique server IPs',
}
);
export const BY_SOURCE_IP = i18n.translate(
'xpack.siem.networkTopNFlowTable.select.bySourceIpDropDownOptionLabel',
{
defaultMessage: 'By source IP',
}
);
export const BY_DESTINATION_IP = i18n.translate(
'xpack.siem.networkTopNFlowTable.select.byDestinationIpDropDownOptionLabel',
{
defaultMessage: 'By destination IP',
}
);
export const BY_CLIENT_IP = i18n.translate(
'xpack.siem.networkTopNFlowTable.select.byClientIpDropDownOptionLabel',
{
defaultMessage: 'By client IP',
}
);
export const BY_SERVER_IP = i18n.translate(
'xpack.siem.networkTopNFlowTable.select.byServerIpDropDownOptionLabel',
{
defaultMessage: 'By server IP',
}
);
export const SOURCE_IPS = i18n.translate('xpack.siem.networkTopNFlowTable.sourceIps', {
defaultMessage: 'Source IPs',
});
export const ROWS_5 = i18n.translate('xpack.siem.networkTopNFlowTable.rows', {
values: { numRows: 5 },

View file

@ -237,8 +237,9 @@ export const PaginatedTable = memo<SiemTables>(
) : (
<>
<BasicTable
items={pageOfItems}
columns={columns}
compressed
items={pageOfItems}
onChange={onChange}
sorting={
sorting
@ -306,6 +307,10 @@ const BasicTable = styled(EuiBasicTable)`
td {
vertical-align: top;
}
.euiTableCellContent {
display: block;
}
}
`;

View file

@ -4,8 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import * as React from 'react';
import { pure } from 'recompose';
import React, { memo, useEffect } from 'react';
import { isEmpty } from 'lodash/fp';
import { EuiToolTip } from '@elastic/eui';
import countries from 'i18n-iso-countries';
import countryJson from 'i18n-iso-countries/langs/en.json';
/**
* Returns the flag for the specified country code, or null if the specified
@ -20,12 +23,27 @@ export const getFlag = (countryCode: string): string | null =>
: null;
/** Renders an emjoi flag for the specified country code */
export const CountryFlag = pure<{
export const CountryFlag = memo<{
countryCode: string;
}>(({ countryCode }) => {
displayCountryNameOnHover?: boolean;
}>(({ countryCode, displayCountryNameOnHover = false }) => {
useEffect(() => {
if (displayCountryNameOnHover && isEmpty(countries.getNames('en'))) {
countries.registerLocale(countryJson);
}
}, []);
const flag = getFlag(countryCode);
return flag !== null ? <span data-test-subj="country-flag">{flag}</span> : null;
if (flag !== null) {
return displayCountryNameOnHover ? (
<EuiToolTip position="top" content={countries.getName(countryCode, 'en')}>
<span data-test-subj="country-flag">{flag}</span>
</EuiToolTip>
) : (
<span data-test-subj="country-flag">{flag}</span>
);
}
return null;
});
CountryFlag.displayName = 'CountryFlag';

View file

@ -1,102 +1,92 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Stat Items Component disable charts it renders the default widget 1`] = `
<Provider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
}
}
<ThemeProvider
theme={[Function]}
>
<StatItemsComponent
description="HOSTS"
fields={
Array [
Object {
"color": "#3185FC",
"icon": "cross",
"key": "hosts",
"value": null,
},
]
<Provider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
}
}
from={1560578400000}
id="statItems"
index={0}
key="mock-keys"
narrowDateRange={[MockFunction]}
to={1560837600000}
>
<FlexItem>
<EuiFlexItem
className="sc-bZQynM kpuYFd"
>
<div
className="euiFlexItem sc-bZQynM kpuYFd"
<StatItemsComponent
description="HOSTS"
fields={
Array [
Object {
"color": "#3185FC",
"icon": "cross",
"key": "hosts",
"value": null,
},
]
}
from={1560578400000}
id="statItems"
index={0}
key="mock-keys"
narrowDateRange={[MockFunction]}
to={1560837600000}
>
<FlexItem>
<EuiFlexItem
className="sc-bZQynM kpuYFd"
>
<EuiPanel
grow={true}
hasShadow={false}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
paddingSize="m"
<div
className="euiFlexItem sc-bZQynM kpuYFd"
>
<div
className="euiPanel euiPanel--paddingMedium"
<EuiPanel
grow={true}
hasShadow={false}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
paddingSize="m"
>
<EuiFlexGroup
gutterSize="none"
<div
className="euiPanel euiPanel--paddingMedium"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className="euiFlexGroup euiFlexGroup--directionRow euiFlexGroup--responsive"
<EuiFlexGroup
gutterSize="none"
>
<EuiFlexItem>
<div
className="euiFlexItem"
>
<EuiTitle
size="xxxs"
>
<h6
className="euiTitle euiTitle--xxxsmall"
>
HOSTS
</h6>
</EuiTitle>
</div>
</EuiFlexItem>
<EuiFlexItem
grow={false}
<div
className="euiFlexGroup euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<Connect(InspectButtonComponent)
inspectIndex={0}
queryId="statItems"
show={false}
title="KPI HOSTS"
<EuiFlexItem>
<div
className="euiFlexItem"
>
<InspectButtonComponent
id=""
inspect={null}
<EuiTitle
size="xxxs"
>
<h6
className="euiTitle euiTitle--xxxsmall"
>
HOSTS
</h6>
</EuiTitle>
</div>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<Connect(InspectButtonComponent)
inspectIndex={0}
isInspected={false}
loading={false}
queryId="statItems"
refetch={null}
selectedInspectIndex={0}
setIsInspected={[Function]}
show={false}
title="KPI HOSTS"
>
<Component
<InspectButtonComponent
id=""
inspect={null}
inspectIndex={0}
@ -109,238 +99,248 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] =
show={false}
title="KPI HOSTS"
>
<InspectContainer
showInspect={false}
<Component
id=""
inspect={null}
inspectIndex={0}
isInspected={false}
loading={false}
queryId="statItems"
refetch={null}
selectedInspectIndex={0}
setIsInspected={[Function]}
show={false}
title="KPI HOSTS"
>
<div
className="sc-EHOje dUUqHB"
<InspectContainer
showInspect={false}
>
<EuiButtonIcon
aria-label="Inspect"
className=""
color="primary"
data-test-subj="inspect-icon-button"
iconSize="m"
iconType="inspect"
onClick={[Function]}
title="Inspect"
type="button"
<div
className="sc-EHOje wlqEL"
>
<button
<EuiButtonIcon
aria-label="Inspect"
className="euiButtonIcon euiButtonIcon--primary"
className=""
color="primary"
data-test-subj="inspect-icon-button"
iconSize="m"
iconType="inspect"
onClick={[Function]}
title="Inspect"
type="button"
>
<EuiIcon
aria-hidden="true"
className="euiButtonIcon__icon"
size="m"
type="inspect"
<button
aria-label="Inspect"
className="euiButtonIcon euiButtonIcon--primary"
data-test-subj="inspect-icon-button"
onClick={[Function]}
title="Inspect"
type="button"
>
<EuiIconEmpty
<EuiIcon
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
style={null}
className="euiButtonIcon__icon"
size="m"
type="inspect"
>
<svg
<EuiIconEmpty
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</EuiIconEmpty>
</EuiIcon>
</button>
</EuiButtonIcon>
<ModalInspectQuery
closeModal={[Function]}
data-test-subj="inspect-modal"
isShowing={false}
request={null}
response={null}
title="KPI HOSTS"
/>
</div>
</InspectContainer>
</Component>
</InspectButtonComponent>
</Connect(InspectButtonComponent)>
</div>
</EuiFlexItem>
</div>
</EuiFlexGroup>
<EuiFlexGroup>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<FlexItem
key="stat-items-field-hosts"
>
<EuiFlexItem
className="sc-bZQynM kpuYFd"
>
<div
className="euiFlexItem sc-bZQynM kpuYFd"
>
<EuiFlexGroup
alignItems="center"
gutterSize="m"
responsive={false}
>
<div
className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<FlexItem>
<EuiFlexItem
className="sc-bZQynM kpuYFd"
>
<div
className="euiFlexItem sc-bZQynM kpuYFd"
>
<StatValue>
<EuiTitle
className="sc-gzVnrw dJnFxB"
>
<p
className="euiTitle euiTitle--medium sc-gzVnrw dJnFxB"
data-test-subj="stat-title"
>
--
</p>
</EuiTitle>
</StatValue>
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</EuiIconEmpty>
</EuiIcon>
</button>
</EuiButtonIcon>
<ModalInspectQuery
closeModal={[Function]}
data-test-subj="inspect-modal"
isShowing={false}
request={null}
response={null}
title="KPI HOSTS"
/>
</div>
</EuiFlexItem>
</FlexItem>
</div>
</EuiFlexGroup>
</InspectContainer>
</Component>
</InspectButtonComponent>
</Connect(InspectButtonComponent)>
</div>
</EuiFlexItem>
</FlexItem>
</div>
</EuiFlexGroup>
<EuiFlexGroup>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
/>
</EuiFlexGroup>
</div>
</EuiPanel>
</div>
</EuiFlexItem>
</FlexItem>
</StatItemsComponent>
</Provider>
</div>
</EuiFlexGroup>
<EuiFlexGroup>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<FlexItem
key="stat-items-field-hosts"
>
<EuiFlexItem
className="sc-bZQynM kpuYFd"
>
<div
className="euiFlexItem sc-bZQynM kpuYFd"
>
<EuiFlexGroup
alignItems="center"
gutterSize="m"
responsive={false}
>
<div
className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<FlexItem>
<EuiFlexItem
className="sc-bZQynM kpuYFd"
>
<div
className="euiFlexItem sc-bZQynM kpuYFd"
>
<StatValue>
<EuiTitle
className="sc-gzVnrw dJnFxB"
>
<p
className="euiTitle euiTitle--medium sc-gzVnrw dJnFxB"
data-test-subj="stat-title"
>
<EmptyWrapper>
<span
className="sc-htpNat bijuWJ"
>
</span>
</EmptyWrapper>
</p>
</EuiTitle>
</StatValue>
</div>
</EuiFlexItem>
</FlexItem>
</div>
</EuiFlexGroup>
</div>
</EuiFlexItem>
</FlexItem>
</div>
</EuiFlexGroup>
<EuiFlexGroup>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
/>
</EuiFlexGroup>
</div>
</EuiPanel>
</div>
</EuiFlexItem>
</FlexItem>
</StatItemsComponent>
</Provider>
</ThemeProvider>
`;
exports[`Stat Items Component disable charts it renders the default widget 2`] = `
<Provider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
}
}
<ThemeProvider
theme={[Function]}
>
<StatItemsComponent
areaChart={Array []}
barChart={Array []}
description="HOSTS"
fields={
Array [
Object {
"color": "#3185FC",
"icon": "cross",
"key": "hosts",
"value": null,
},
]
<Provider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
}
}
from={1560578400000}
id="statItems"
index={0}
key="mock-keys"
narrowDateRange={[MockFunction]}
to={1560837600000}
>
<FlexItem>
<EuiFlexItem
className="sc-bZQynM kpuYFd"
>
<div
className="euiFlexItem sc-bZQynM kpuYFd"
<StatItemsComponent
areaChart={Array []}
barChart={Array []}
description="HOSTS"
fields={
Array [
Object {
"color": "#3185FC",
"icon": "cross",
"key": "hosts",
"value": null,
},
]
}
from={1560578400000}
id="statItems"
index={0}
key="mock-keys"
narrowDateRange={[MockFunction]}
to={1560837600000}
>
<FlexItem>
<EuiFlexItem
className="sc-bZQynM kpuYFd"
>
<EuiPanel
grow={true}
hasShadow={false}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
paddingSize="m"
<div
className="euiFlexItem sc-bZQynM kpuYFd"
>
<div
className="euiPanel euiPanel--paddingMedium"
<EuiPanel
grow={true}
hasShadow={false}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
paddingSize="m"
>
<EuiFlexGroup
gutterSize="none"
<div
className="euiPanel euiPanel--paddingMedium"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className="euiFlexGroup euiFlexGroup--directionRow euiFlexGroup--responsive"
<EuiFlexGroup
gutterSize="none"
>
<EuiFlexItem>
<div
className="euiFlexItem"
>
<EuiTitle
size="xxxs"
>
<h6
className="euiTitle euiTitle--xxxsmall"
>
HOSTS
</h6>
</EuiTitle>
</div>
</EuiFlexItem>
<EuiFlexItem
grow={false}
<div
className="euiFlexGroup euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<Connect(InspectButtonComponent)
inspectIndex={0}
queryId="statItems"
show={false}
title="KPI HOSTS"
<EuiFlexItem>
<div
className="euiFlexItem"
>
<InspectButtonComponent
id=""
inspect={null}
<EuiTitle
size="xxxs"
>
<h6
className="euiTitle euiTitle--xxxsmall"
>
HOSTS
</h6>
</EuiTitle>
</div>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<Connect(InspectButtonComponent)
inspectIndex={0}
isInspected={false}
loading={false}
queryId="statItems"
refetch={null}
selectedInspectIndex={0}
setIsInspected={[Function]}
show={false}
title="KPI HOSTS"
>
<Component
<InspectButtonComponent
id=""
inspect={null}
inspectIndex={0}
@ -353,138 +353,158 @@ exports[`Stat Items Component disable charts it renders the default widget 2`] =
show={false}
title="KPI HOSTS"
>
<InspectContainer
showInspect={false}
<Component
id=""
inspect={null}
inspectIndex={0}
isInspected={false}
loading={false}
queryId="statItems"
refetch={null}
selectedInspectIndex={0}
setIsInspected={[Function]}
show={false}
title="KPI HOSTS"
>
<div
className="sc-EHOje dUUqHB"
<InspectContainer
showInspect={false}
>
<EuiButtonIcon
aria-label="Inspect"
className=""
color="primary"
data-test-subj="inspect-icon-button"
iconSize="m"
iconType="inspect"
onClick={[Function]}
title="Inspect"
type="button"
<div
className="sc-EHOje wlqEL"
>
<button
<EuiButtonIcon
aria-label="Inspect"
className="euiButtonIcon euiButtonIcon--primary"
className=""
color="primary"
data-test-subj="inspect-icon-button"
iconSize="m"
iconType="inspect"
onClick={[Function]}
title="Inspect"
type="button"
>
<EuiIcon
aria-hidden="true"
className="euiButtonIcon__icon"
size="m"
type="inspect"
<button
aria-label="Inspect"
className="euiButtonIcon euiButtonIcon--primary"
data-test-subj="inspect-icon-button"
onClick={[Function]}
title="Inspect"
type="button"
>
<EuiIconEmpty
<EuiIcon
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
style={null}
className="euiButtonIcon__icon"
size="m"
type="inspect"
>
<svg
<EuiIconEmpty
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</EuiIconEmpty>
</EuiIcon>
</button>
</EuiButtonIcon>
<ModalInspectQuery
closeModal={[Function]}
data-test-subj="inspect-modal"
isShowing={false}
request={null}
response={null}
title="KPI HOSTS"
/>
</div>
</InspectContainer>
</Component>
</InspectButtonComponent>
</Connect(InspectButtonComponent)>
</div>
</EuiFlexItem>
</div>
</EuiFlexGroup>
<EuiFlexGroup>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<FlexItem
key="stat-items-field-hosts"
>
<EuiFlexItem
className="sc-bZQynM kpuYFd"
>
<div
className="euiFlexItem sc-bZQynM kpuYFd"
>
<EuiFlexGroup
alignItems="center"
gutterSize="m"
responsive={false}
>
<div
className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
0
<FlexItem>
<EuiFlexItem
className="sc-bZQynM kpuYFd"
>
<div
className="euiFlexItem sc-bZQynM kpuYFd"
>
<StatValue>
<EuiTitle
className="sc-gzVnrw dJnFxB"
>
<p
className="euiTitle euiTitle--medium sc-gzVnrw dJnFxB"
data-test-subj="stat-title"
>
--
</p>
</EuiTitle>
</StatValue>
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</EuiIconEmpty>
</EuiIcon>
</button>
</EuiButtonIcon>
<ModalInspectQuery
closeModal={[Function]}
data-test-subj="inspect-modal"
isShowing={false}
request={null}
response={null}
title="KPI HOSTS"
/>
</div>
</EuiFlexItem>
</FlexItem>
</div>
</EuiFlexGroup>
</InspectContainer>
</Component>
</InspectButtonComponent>
</Connect(InspectButtonComponent)>
</div>
</EuiFlexItem>
</FlexItem>
</div>
</EuiFlexGroup>
<EuiFlexGroup>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
/>
</EuiFlexGroup>
</div>
</EuiPanel>
</div>
</EuiFlexItem>
</FlexItem>
</StatItemsComponent>
</Provider>
</div>
</EuiFlexGroup>
<EuiFlexGroup>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<FlexItem
key="stat-items-field-hosts"
>
<EuiFlexItem
className="sc-bZQynM kpuYFd"
>
<div
className="euiFlexItem sc-bZQynM kpuYFd"
>
<EuiFlexGroup
alignItems="center"
gutterSize="m"
responsive={false}
>
<div
className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
0
<FlexItem>
<EuiFlexItem
className="sc-bZQynM kpuYFd"
>
<div
className="euiFlexItem sc-bZQynM kpuYFd"
>
<StatValue>
<EuiTitle
className="sc-gzVnrw dJnFxB"
>
<p
className="euiTitle euiTitle--medium sc-gzVnrw dJnFxB"
data-test-subj="stat-title"
>
<EmptyWrapper>
<span
className="sc-htpNat bijuWJ"
>
</span>
</EmptyWrapper>
</p>
</EuiTitle>
</StatValue>
</div>
</EuiFlexItem>
</FlexItem>
</div>
</EuiFlexGroup>
</div>
</EuiFlexItem>
</FlexItem>
</div>
</EuiFlexGroup>
<EuiFlexGroup>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
/>
</EuiFlexGroup>
</div>
</EuiPanel>
</div>
</EuiFlexItem>
</FlexItem>
</StatItemsComponent>
</Provider>
</ThemeProvider>
`;
exports[`Stat Items Component rendering kpis with charts it renders the default widget 1`] = `

View file

@ -4,9 +4,11 @@
* 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, ReactWrapper } from 'enzyme';
import toJson from 'enzyme-to-json';
import * as React from 'react';
import { ThemeProvider } from 'styled-components';
import {
StatItemsComponent,
@ -36,42 +38,47 @@ const from = new Date('2019-06-15T06:00:00.000Z').valueOf();
const to = new Date('2019-06-18T06:00:00.000Z').valueOf();
describe('Stat Items Component', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
const state: State = mockGlobalState;
const store = createStore(state, apolloClientObservable);
describe.each([
[
mount(
<ReduxStoreProvider store={store}>
<StatItemsComponent
description="HOSTS"
fields={[{ key: 'hosts', value: null, color: '#3185FC', icon: 'cross' }]}
from={from}
id="statItems"
index={0}
key="mock-keys"
to={to}
narrowDateRange={mockNarrowDateRange}
/>
</ReduxStoreProvider>
<ThemeProvider theme={theme}>
<ReduxStoreProvider store={store}>
<StatItemsComponent
description="HOSTS"
fields={[{ key: 'hosts', value: null, color: '#3185FC', icon: 'cross' }]}
from={from}
id="statItems"
index={0}
key="mock-keys"
to={to}
narrowDateRange={mockNarrowDateRange}
/>
</ReduxStoreProvider>
</ThemeProvider>
),
],
[
mount(
<ReduxStoreProvider store={store}>
<StatItemsComponent
areaChart={[]}
barChart={[]}
description="HOSTS"
fields={[{ key: 'hosts', value: null, color: '#3185FC', icon: 'cross' }]}
from={from}
id="statItems"
index={0}
key="mock-keys"
to={to}
narrowDateRange={mockNarrowDateRange}
/>
</ReduxStoreProvider>
<ThemeProvider theme={theme}>
<ReduxStoreProvider store={store}>
<StatItemsComponent
areaChart={[]}
barChart={[]}
description="HOSTS"
fields={[{ key: 'hosts', value: null, color: '#3185FC', icon: 'cross' }]}
from={from}
id="statItems"
index={0}
key="mock-keys"
to={to}
narrowDateRange={mockNarrowDateRange}
/>
</ReduxStoreProvider>
</ThemeProvider>
),
],
])('disable charts', wrapper => {

View file

@ -97,32 +97,37 @@ exports[`Table Helpers #getRowItemDraggables it returns correctly against snapsh
exports[`Table Helpers #getRowItemOverflow it returns correctly against snapshot 1`] = `
<div>
<EuiToolTip
content={
<React.Fragment>
<span>
<React.Fragment>
item2
</React.Fragment>
<br />
</span>
<b>
<br />
<Popover
count={2}
idPrefix="attrName"
>
<EuiText
size="xs"
>
<ul>
<li
key="attrName-item2"
>
item2
</li>
</ul>
<p
data-test-subj="popover-additional-overflow"
>
<EuiTextColor
color="subdued"
>
1
<FormattedMessage
defaultMessage="More..."
defaultMessage="more not shown"
id="xpack.siem.tables.rowItemHelper.moreDescription"
values={Object {}}
/>
</b>
</React.Fragment>
}
delay="regular"
position="top"
>
<MoreRowItems
type="boxesHorizontal"
/>
</EuiToolTip>
</EuiTextColor>
</p>
</EuiText>
</Popover>
</div>
`;

View file

@ -37,8 +37,8 @@ describe('Table Helpers', () => {
idPrefix: 'idPrefix',
displayCount: 0,
});
const wrapper = shallow(<TestProviders>{rowItem}</TestProviders>);
expect(wrapper.html()).toBe(getEmptyValue());
const wrapper = mount(<TestProviders>{rowItem}</TestProviders>);
expect(wrapper.text()).toBe(getEmptyValue());
});
test('it returns empty string value when rowItem is empty', () => {
@ -64,8 +64,9 @@ describe('Table Helpers', () => {
idPrefix: 'idPrefix',
displayCount: 0,
});
const wrapper = shallow(<TestProviders>{rowItem}</TestProviders>);
expect(wrapper.html()).toBe(getEmptyValue());
const wrapper = mount(<TestProviders>{rowItem}</TestProviders>);
expect(wrapper.text()).toBe(getEmptyValue());
});
test('it uses custom renderer', () => {
@ -104,8 +105,8 @@ describe('Table Helpers', () => {
idPrefix: 'idPrefix',
displayCount: 0,
});
const wrapper = shallow(<TestProviders>{rowItems}</TestProviders>);
expect(wrapper.html()).toBe(getEmptyValue());
const wrapper = mount(<TestProviders>{rowItems}</TestProviders>);
expect(wrapper.text()).toBe(getEmptyValue());
});
test('it returns empty string value when rowItem is empty', () => {
@ -130,8 +131,8 @@ describe('Table Helpers', () => {
idPrefix: 'idPrefix',
displayCount: 0,
});
const wrapper = shallow(<TestProviders>{rowItems}</TestProviders>);
expect(wrapper.html()).toBe(getEmptyValue());
const wrapper = mount(<TestProviders>{rowItems}</TestProviders>);
expect(wrapper.text()).toBe(getEmptyValue());
});
test('it returns no items when provided a 0 displayCount', () => {
@ -141,8 +142,8 @@ describe('Table Helpers', () => {
idPrefix: 'idPrefix',
displayCount: 0,
});
const wrapper = shallow(<TestProviders>{rowItems}</TestProviders>);
expect(wrapper.html()).toBe(getEmptyValue());
const wrapper = mount(<TestProviders>{rowItems}</TestProviders>);
expect(wrapper.text()).toBe(getEmptyValue());
});
test('it returns no items when provided an empty array', () => {
@ -151,10 +152,12 @@ describe('Table Helpers', () => {
attrName: 'attrName',
idPrefix: 'idPrefix',
});
const wrapper = shallow(<TestProviders>{rowItems}</TestProviders>);
expect(wrapper.html()).toBe(getEmptyValue());
const wrapper = mount(<TestProviders>{rowItems}</TestProviders>);
expect(wrapper.text()).toBe(getEmptyValue());
});
// Using hostNodes due to this issue: https://github.com/airbnb/enzyme/issues/836
test('it returns 2 items then overflows', () => {
const rowItems = getRowItemDraggables({
rowItems: items,
@ -163,7 +166,7 @@ describe('Table Helpers', () => {
displayCount: 2,
});
const wrapper = mount(<TestProviders>{rowItems}</TestProviders>);
expect(wrapper.find('[data-test-subj="draggableWrapperDiv"]').length).toBe(2);
expect(wrapper.find('[data-test-subj="draggableWrapperDiv"]').hostNodes().length).toBe(2);
});
test('it uses custom renderer', () => {
@ -191,20 +194,16 @@ describe('Table Helpers', () => {
expect(toJson(wrapper)).toMatchSnapshot();
});
test('it does not show "More..." when maxOverflowItems are not exceeded', () => {
test('it does not show "more not shown" when maxOverflowItems are not exceeded', () => {
const rowItemOverflow = getRowItemOverflow(items, 'attrName', 1, 5);
const wrapper = shallow(<div>{rowItemOverflow}</div>);
expect(JSON.stringify(wrapper.find('EuiToolTip').prop('content'))).not.toContain(
'defaultMessage'
);
expect(wrapper.find('[data-test-subj="popover-additional-overflow"]').length).toBe(0);
});
test('it shows "More..." when maxOverflowItems are exceeded', () => {
test('it shows "more not shown" when maxOverflowItems are exceeded', () => {
const rowItemOverflow = getRowItemOverflow(items, 'attrName', 1, 1);
const wrapper = shallow(<div>{rowItemOverflow}</div>);
expect(JSON.stringify(wrapper.find('EuiToolTip').prop('content'))).toContain(
'defaultMessage'
);
expect(wrapper.find('[data-test-subj="popover-additional-overflow"]').length).toBe(1);
});
});

View file

@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiToolTip } from '@elastic/eui';
import styled from 'styled-components';
import { EuiLink, EuiPopover, EuiToolTip, EuiText, EuiTextColor } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { useState } from 'react';
import { escapeDataProviderId } from '../drag_and_drop/helpers';
import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper';
@ -15,6 +16,10 @@ import { Provider } from '../timeline/data_providers/provider';
import { defaultToEmptyTag, getEmptyTagValue } from '../empty_value';
import { MoreRowItems, Spacer } from '../page';
const Subtext = styled.div`
font-size: ${props => props.theme.eui.euiFontSizeXS};
`;
export const getRowItemDraggable = ({
rowItem,
attrName,
@ -144,36 +149,57 @@ export const getRowItemOverflow = (
return (
<>
{rowItems.length > overflowIndexStart && (
<EuiToolTip
content={
<>
<Popover count={rowItems.length - overflowIndexStart} idPrefix={idPrefix}>
<EuiText size="xs">
<ul>
{rowItems
.slice(overflowIndexStart, overflowIndexStart + maxOverflowItems)
.map(rowItem => (
<span key={`${idPrefix}-${rowItem}`}>
{defaultToEmptyTag(rowItem)}
<br />
</span>
<li key={`${idPrefix}-${rowItem}`}>{defaultToEmptyTag(rowItem)}</li>
))}
{rowItems.length > overflowIndexStart + maxOverflowItems && (
<b>
<br />
</ul>
{rowItems.length > overflowIndexStart + maxOverflowItems && (
<p data-test-subj="popover-additional-overflow">
<EuiTextColor color="subdued">
{rowItems.length - overflowIndexStart - maxOverflowItems}{' '}
<FormattedMessage
id="xpack.siem.tables.rowItemHelper.moreDescription"
defaultMessage="More..."
defaultMessage="more not shown"
/>
</b>
)}
</>
}
>
<MoreRowItems type="boxesHorizontal" />
</EuiToolTip>
</EuiTextColor>
</p>
)}
</EuiText>
</Popover>
)}
</>
);
};
export const Popover = React.memo<{
children: React.ReactNode;
count: number;
idPrefix: string;
}>(({ children, count, idPrefix }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Subtext>
<EuiPopover
button={<EuiLink onClick={() => setIsOpen(!isOpen)}>{`+${count} More`}</EuiLink>}
closePopover={() => setIsOpen(!isOpen)}
id={`${idPrefix}-popover`}
isOpen={isOpen}
>
{children}
</EuiPopover>
</Subtext>
);
});
Popover.displayName = 'Popover';
export const OverflowField = React.memo<{
value: string;
showToolTip?: boolean;

View file

@ -12,7 +12,7 @@ exports[`empty_column_renderer renders correctly against snapshot 1`] = `
"kqlQuery": "",
"name": "source.ip: ",
"queryMatch": Object {
"displayValue": "--",
"displayValue": "",
"field": "source.ip",
"operator": ":*",
"value": "",

View file

@ -2,6 +2,8 @@
exports[`unknown_column_renderer renders correctly against snapshot 1`] = `
<span>
--
<EmptyWrapper>
</EmptyWrapper>
</span>
`;

View file

@ -4,10 +4,12 @@
* 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 { get } from 'lodash/fp';
import * as React from 'react';
import { ThemeProvider } from 'styled-components';
import { mockTimelineData, TestProviders } from '../../../../mock';
import { getEmptyValue } from '../../../empty_value';
@ -16,6 +18,8 @@ import { FormattedFieldValue } from './formatted_field';
jest.mock('../../../../lib/settings/use_kibana_ui_setting');
describe('Events', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
test('renders correctly against snapshot', () => {
const wrapper = shallow(
<FormattedFieldValue
@ -92,13 +96,15 @@ describe('Events', () => {
test('it renders placeholder text for a non-date field when the field is NOT populated', () => {
const wrapper = mount(
<FormattedFieldValue
eventId={mockTimelineData[0].ecs._id}
contextId="test"
fieldName="fake.field"
fieldType="text"
value={get('fake.field', mockTimelineData[0].ecs)}
/>
<ThemeProvider theme={theme}>
<FormattedFieldValue
eventId={mockTimelineData[0].ecs._id}
contextId="test"
fieldName="fake.field"
fieldType="text"
value={get('fake.field', mockTimelineData[0].ecs)}
/>
</ThemeProvider>
);
expect(wrapper.text()).toEqual(getEmptyValue());

View file

@ -95,7 +95,7 @@ describe('plain_column_renderer', () => {
<span>{column}</span>
</TestProviders>
);
expect(wrapper.text()).toEqual('120.563KB');
expect(wrapper.text()).toEqual('120.6KB');
});
test('should return the value of event.action if event has a valid value', () => {

View file

@ -4,10 +4,12 @@
* 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 { cloneDeep } from 'lodash';
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { TimelineNonEcsData } from '../../../../graphql/types';
import { defaultHeaders, mockTimelineData } from '../../../../mock';
@ -16,6 +18,7 @@ import { unknownColumnRenderer } from './unknown_column_renderer';
import { getValues } from './helpers';
describe('unknown_column_renderer', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
let mockDatum: TimelineNonEcsData[];
const _id = mockTimelineData[0]._id;
beforeEach(() => {
@ -44,7 +47,11 @@ describe('unknown_column_renderer', () => {
values: getValues('a made up column name', mockDatum),
field: defaultHeaders.find(h => h.id === 'a made up column name')!,
});
const wrapper = mount(<span>{emptyColumn}</span>);
const wrapper = mount(
<ThemeProvider theme={theme}>
<span>{emptyColumn}</span>
</ThemeProvider>
);
expect(wrapper.text()).toEqual(getEmptyValue());
});
@ -55,7 +62,11 @@ describe('unknown_column_renderer', () => {
values: getValues('@timestamp', mockDatum),
field: defaultHeaders.find(h => h.id === '@timestamp')!,
});
const wrapper = mount(<span>{emptyColumn}</span>);
const wrapper = mount(
<ThemeProvider theme={theme}>
<span>{emptyColumn}</span>
</ThemeProvider>
);
expect(wrapper.text()).toEqual(getEmptyValue());
});
});

View file

@ -33,6 +33,7 @@ interface State {
}
const HoverActionsPanelContainer = styled.div`
color: ${props => props.theme.eui.textColors.default}
height: 100%;
position: relative;
`;

View file

@ -9,11 +9,10 @@ import gql from 'graphql-tag';
export const networkTopNFlowQuery = gql`
query GetNetworkTopNFlowQuery(
$sourceId: ID!
$flowDirection: FlowDirection!
$filterQuery: String
$pagination: PaginationInputPaginated!
$sort: NetworkTopNFlowSortField!
$flowTarget: FlowTarget!
$flowTarget: FlowTargetNew!
$timerange: TimerangeInput!
$defaultIndex: [String!]!
$inspect: Boolean!
@ -22,7 +21,6 @@ export const networkTopNFlowQuery = gql`
id
NetworkTopNFlow(
filterQuery: $filterQuery
flowDirection: $flowDirection
flowTarget: $flowTarget
pagination: $pagination
sort: $sort
@ -33,29 +31,50 @@ export const networkTopNFlowQuery = gql`
edges {
node {
source {
count
ip
autonomous_system {
name
number
}
domain
ip
location {
geo {
continent_name
country_name
country_iso_code
city_name
region_iso_code
region_name
}
flowTarget
}
flows
destination_ips
}
destination {
count
ip
autonomous_system {
name
number
}
domain
}
client {
count
ip
domain
}
server {
count
ip
domain
location {
geo {
continent_name
country_name
country_iso_code
city_name
region_iso_code
region_name
}
flowTarget
}
flows
source_ips
}
network {
bytes
direction
packets
bytes_in
bytes_out
}
}
cursor {

View file

@ -12,14 +12,13 @@ import { connect } from 'react-redux';
import chrome from 'ui/chrome';
import { DEFAULT_INDEX_KEY } from '../../../common/constants';
import {
FlowDirection,
FlowTarget,
FlowTargetNew,
GetNetworkTopNFlowQuery,
NetworkTopNFlowEdges,
NetworkTopNFlowSortField,
PageInfoPaginated,
} from '../../graphql/types';
import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store';
import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store';
import { generateTablePaginationOptions } from '../../components/paginated_table/helpers';
import { createFilter } from '../helpers';
import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated';
@ -40,13 +39,12 @@ export interface NetworkTopNFlowArgs {
export interface OwnProps extends QueryTemplatePaginatedProps {
children: (args: NetworkTopNFlowArgs) => React.ReactNode;
flowTarget: FlowTargetNew;
type: networkModel.NetworkType;
}
export interface NetworkTopNFlowComponentReduxProps {
activePage: number;
flowDirection: FlowDirection;
flowTarget: FlowTarget;
isInspected: boolean;
limit: number;
topNFlowSort: NetworkTopNFlowSortField;
@ -64,9 +62,8 @@ class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated<
activePage,
children,
endDate,
filterQuery,
flowDirection,
flowTarget,
filterQuery,
id = ID,
isInspected,
limit,
@ -84,7 +81,6 @@ class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated<
variables={{
defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY),
filterQuery: createFilter(filterQuery),
flowDirection,
flowTarget,
inspect: isInspected,
pagination: generateTablePaginationOptions(activePage, limit),
@ -136,17 +132,14 @@ class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated<
}
}
const makeMapStateToProps = () => {
const getNetworkTopNFlowSelector = networkSelectors.topNFlowSelector();
const mapStateToProps = (state: State, { flowTarget, id = ID }: OwnProps) => {
const getNetworkTopNFlowSelector = networkSelectors.topNFlowSelector(flowTarget);
const getQuery = inputsSelectors.globalQueryByIdSelector();
const mapStateToProps = (state: State, { id = ID }: OwnProps) => {
const { isInspected } = getQuery(state, id);
return {
...getNetworkTopNFlowSelector(state),
isInspected,
};
const { isInspected } = getQuery(state, id);
return {
...getNetworkTopNFlowSelector(state),
isInspected,
};
return mapStateToProps;
};
export const NetworkTopNFlowQuery = connect(makeMapStateToProps)(NetworkTopNFlowComponentQuery);
export const NetworkTopNFlowQuery = connect(mapStateToProps)(NetworkTopNFlowComponentQuery);

View file

@ -1685,23 +1685,13 @@
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "flowDirection",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "ENUM", "name": "FlowDirection", "ofType": null }
},
"defaultValue": null
},
{
"name": "flowTarget",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "ENUM", "name": "FlowTarget", "ofType": null }
"ofType": { "kind": "ENUM", "name": "FlowTargetNew", "ofType": null }
},
"defaultValue": null
},
@ -7416,6 +7406,24 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "FlowTargetNew",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "destination",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{ "name": "source", "description": "", "isDeprecated": false, "deprecationReason": null }
],
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "NetworkTopNFlowSortField",
@ -7455,14 +7463,31 @@
"inputFields": null,
"interfaces": null,
"enumValues": [
{ "name": "bytes", "description": "", "isDeprecated": false, "deprecationReason": null },
{
"name": "packets",
"name": "bytes_in",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{ "name": "ipCount", "description": "", "isDeprecated": false, "deprecationReason": null }
{
"name": "bytes_out",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{ "name": "flows", "description": "", "isDeprecated": false, "deprecationReason": null },
{
"name": "destination_ips",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "source_ips",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
@ -7581,7 +7606,7 @@
"name": "source",
"description": "",
"args": [],
"type": { "kind": "OBJECT", "name": "TopNFlowItem", "ofType": null },
"type": { "kind": "OBJECT", "name": "TopNFlowItemSource", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
@ -7589,23 +7614,7 @@
"name": "destination",
"description": "",
"args": [],
"type": { "kind": "OBJECT", "name": "TopNFlowItem", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "client",
"description": "",
"args": [],
"type": { "kind": "OBJECT", "name": "TopNFlowItem", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "server",
"description": "",
"args": [],
"type": { "kind": "OBJECT", "name": "TopNFlowItem", "ofType": null },
"type": { "kind": "OBJECT", "name": "TopNFlowItemDestination", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
@ -7625,14 +7634,14 @@
},
{
"kind": "OBJECT",
"name": "TopNFlowItem",
"name": "TopNFlowItemSource",
"description": "",
"fields": [
{
"name": "count",
"name": "autonomous_system",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"type": { "kind": "OBJECT", "name": "AutonomousSystemItem", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
@ -7659,6 +7668,151 @@
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "location",
"description": "",
"args": [],
"type": { "kind": "OBJECT", "name": "GeoItem", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "flows",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "destination_ips",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AutonomousSystemItem",
"description": "",
"fields": [
{
"name": "name",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "number",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GeoItem",
"description": "",
"fields": [
{
"name": "geo",
"description": "",
"args": [],
"type": { "kind": "OBJECT", "name": "GeoEcsFields", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "flowTarget",
"description": "",
"args": [],
"type": { "kind": "ENUM", "name": "FlowTarget", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TopNFlowItemDestination",
"description": "",
"fields": [
{
"name": "autonomous_system",
"description": "",
"args": [],
"type": { "kind": "OBJECT", "name": "AutonomousSystemItem", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "domain",
"description": "",
"args": [],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ip",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "location",
"description": "",
"args": [],
"type": { "kind": "OBJECT", "name": "GeoItem", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "flows",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "source_ips",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@ -7672,7 +7826,7 @@
"description": "",
"fields": [
{
"name": "bytes",
"name": "bytes_in",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
@ -7680,36 +7834,12 @@
"deprecationReason": null
},
{
"name": "packets",
"name": "bytes_out",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "transport",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "direction",
"description": "",
"args": [],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "ENUM", "name": "NetworkDirectionEcs", "ofType": null }
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,

View file

@ -1166,33 +1166,57 @@ export interface NetworkTopNFlowEdges {
export interface NetworkTopNFlowItem {
_id?: string | null;
source?: TopNFlowItem | null;
source?: TopNFlowItemSource | null;
destination?: TopNFlowItem | null;
client?: TopNFlowItem | null;
server?: TopNFlowItem | null;
destination?: TopNFlowItemDestination | null;
network?: TopNFlowNetworkEcsField | null;
}
export interface TopNFlowItem {
count?: number | null;
export interface TopNFlowItemSource {
autonomous_system?: AutonomousSystemItem | null;
domain?: string[] | null;
ip?: string | null;
location?: GeoItem | null;
flows?: number | null;
destination_ips?: number | null;
}
export interface AutonomousSystemItem {
name?: string | null;
number?: number | null;
}
export interface GeoItem {
geo?: GeoEcsFields | null;
flowTarget?: FlowTarget | null;
}
export interface TopNFlowItemDestination {
autonomous_system?: AutonomousSystemItem | null;
domain?: string[] | null;
ip?: string | null;
location?: GeoItem | null;
flows?: number | null;
source_ips?: number | null;
}
export interface TopNFlowNetworkEcsField {
bytes?: number | null;
bytes_in?: number | null;
packets?: number | null;
transport?: string | null;
direction?: NetworkDirectionEcs[] | null;
bytes_out?: number | null;
}
export interface NetworkDnsData {
@ -1955,9 +1979,7 @@ export interface NetworkTopNFlowSourceArgs {
filterQuery?: string | null;
flowDirection: FlowDirection;
flowTarget: FlowTarget;
flowTarget: FlowTargetNew;
pagination: PaginationInputPaginated;
@ -2123,10 +2145,17 @@ export enum UsersFields {
count = 'count',
}
export enum FlowTargetNew {
destination = 'destination',
source = 'source',
}
export enum NetworkTopNFlowFields {
bytes = 'bytes',
packets = 'packets',
ipCount = 'ipCount',
bytes_in = 'bytes_in',
bytes_out = 'bytes_out',
flows = 'flows',
destination_ips = 'destination_ips',
source_ips = 'source_ips',
}
export enum NetworkDnsFields {
@ -3277,11 +3306,10 @@ export namespace GetNetworkDnsQuery {
export namespace GetNetworkTopNFlowQuery {
export type Variables = {
sourceId: string;
flowDirection: FlowDirection;
filterQuery?: string | null;
pagination: PaginationInputPaginated;
sort: NetworkTopNFlowSortField;
flowTarget: FlowTarget;
flowTarget: FlowTargetNew;
timerange: TimerangeInput;
defaultIndex: string[];
inspect: boolean;
@ -3328,61 +3356,111 @@ export namespace GetNetworkTopNFlowQuery {
destination?: Destination | null;
client?: Client | null;
server?: Server | null;
network?: Network | null;
};
export type _Source = {
__typename?: 'TopNFlowItem';
__typename?: 'TopNFlowItemSource';
count?: number | null;
autonomous_system?: AutonomousSystem | null;
domain?: string[] | null;
ip?: string | null;
domain?: string[] | null;
location?: Location | null;
flows?: number | null;
destination_ips?: number | null;
};
export type AutonomousSystem = {
__typename?: 'AutonomousSystemItem';
name?: string | null;
number?: number | null;
};
export type Location = {
__typename?: 'GeoItem';
geo?: Geo | null;
flowTarget?: FlowTarget | null;
};
export type Geo = {
__typename?: 'GeoEcsFields';
continent_name?: ToStringArray | null;
country_name?: ToStringArray | null;
country_iso_code?: ToStringArray | null;
city_name?: ToStringArray | null;
region_iso_code?: ToStringArray | null;
region_name?: ToStringArray | null;
};
export type Destination = {
__typename?: 'TopNFlowItem';
__typename?: 'TopNFlowItemDestination';
count?: number | null;
autonomous_system?: _AutonomousSystem | null;
domain?: string[] | null;
ip?: string | null;
domain?: string[] | null;
location?: _Location | null;
flows?: number | null;
source_ips?: number | null;
};
export type Client = {
__typename?: 'TopNFlowItem';
export type _AutonomousSystem = {
__typename?: 'AutonomousSystemItem';
count?: number | null;
name?: string | null;
ip?: string | null;
domain?: string[] | null;
number?: number | null;
};
export type Server = {
__typename?: 'TopNFlowItem';
export type _Location = {
__typename?: 'GeoItem';
count?: number | null;
geo?: _Geo | null;
ip?: string | null;
flowTarget?: FlowTarget | null;
};
domain?: string[] | null;
export type _Geo = {
__typename?: 'GeoEcsFields';
continent_name?: ToStringArray | null;
country_name?: ToStringArray | null;
country_iso_code?: ToStringArray | null;
city_name?: ToStringArray | null;
region_iso_code?: ToStringArray | null;
region_name?: ToStringArray | null;
};
export type Network = {
__typename?: 'TopNFlowNetworkEcsField';
bytes?: number | null;
bytes_in?: number | null;
direction?: NetworkDirectionEcs[] | null;
packets?: number | null;
bytes_out?: number | null;
};
export type Cursor = {

View file

@ -65,12 +65,15 @@ export const mockGlobalState: State = {
network: {
page: {
queries: {
topNFlow: {
topNFlowSource: {
activePage: 0,
limit: 10,
flowTarget: FlowTarget.source,
flowDirection: FlowDirection.uniDirectional,
topNFlowSort: { field: NetworkTopNFlowFields.bytes, direction: Direction.desc },
topNFlowSort: { field: NetworkTopNFlowFields.bytes_out, direction: Direction.desc },
},
topNFlowDestination: {
activePage: 0,
limit: 10,
topNFlowSort: { field: NetworkTopNFlowFields.bytes_out, direction: Direction.desc },
},
dns: {
activePage: 0,

View file

@ -41,25 +41,25 @@ export const mockFrameworks: Readonly<Record<string, MockFrameworks>> = {
timezone: 'America/Denver',
},
default_browser: {
bytesFormat: '0,0.[000]b',
bytesFormat: '0,0.[0]b',
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
dateFormatTz: 'Browser',
timezone: 'America/Denver',
},
default_ET: {
bytesFormat: '0,0.[000]b',
bytesFormat: '0,0.[0]b',
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
dateFormatTz: 'America/New_York',
timezone: 'America/New_York',
},
default_MT: {
bytesFormat: '0,0.[000]b',
bytesFormat: '0,0.[0]b',
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
dateFormatTz: 'America/Denver',
timezone: 'America/Denver',
},
default_UTC: {
bytesFormat: '0,0.[000]b',
bytesFormat: '0,0.[0]b',
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
dateFormatTz: 'UTC',
timezone: 'UTC',

View file

@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiSpacer } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { getOr } from 'lodash/fp';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { StickyContainer } from 'react-sticky';
import { pure } from 'recompose';
import { ActionCreator } from 'typescript-fsa';
import { FiltersGlobal } from '../../components/filters_global';
@ -24,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 { LastEventIndexKey } from '../../graphql/types';
import { FlowTargetNew, LastEventIndexKey } from '../../graphql/types';
import { networkModel, networkSelectors, State } from '../../store';
import { NetworkKql } from './kql';
@ -48,165 +48,238 @@ interface NetworkComponentReduxProps {
}
type NetworkComponentProps = NetworkComponentReduxProps;
const NetworkComponent = pure<NetworkComponentProps>(
({ filterQuery, setAbsoluteRangeDatePicker }) => (
<WithSource sourceId="default">
{({ indicesExist, indexPattern }) =>
indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<StickyContainer>
<FiltersGlobal>
<NetworkKql indexPattern={indexPattern} type={networkModel.NetworkType.page} />
</FiltersGlobal>
const mediaMatch = window.matchMedia(
'screen and (min-width: ' + euiLightVars.euiBreakpoints.xl + ')'
);
const getFlexDirectionByMediaMatch = (): 'row' | 'column' => {
const { matches } = mediaMatch;
return matches ? 'row' : 'column';
};
export const getFlexDirection = () => {
const [display, setDisplay] = useState(getFlexDirectionByMediaMatch());
<HeaderPage
subtitle={<LastEventTime indexKey={LastEventIndexKey.network} />}
title={i18n.PAGE_TITLE}
/>
useEffect(() => {
const setFromEvent = () => setDisplay(getFlexDirectionByMediaMatch());
window.addEventListener('resize', setFromEvent);
<GlobalTime>
{({ to, from, setQuery }) => (
<UseUrlState indexPattern={indexPattern}>
{({ isInitializing }) => (
<>
<KpiNetworkQuery
endDate={to}
filterQuery={filterQuery}
skip={isInitializing}
sourceId="default"
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) => {
setTimeout(() => {
setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
}, 500);
}}
/>
)}
</KpiNetworkQuery>
return () => {
window.removeEventListener('resize', setFromEvent);
};
}, []);
<EuiSpacer />
return display;
};
<NetworkTopNFlowQuery
endDate={to}
filterQuery={filterQuery}
skip={isInitializing}
sourceId="default"
startDate={from}
type={networkModel.NetworkType.page}
>
{({
totalCount,
loading,
networkTopNFlow,
pageInfo,
loadPage,
id,
inspect,
refetch,
}) => (
<NetworkTopNFlowTableManage
data={networkTopNFlow}
fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
id={id}
indexPattern={indexPattern}
inspect={inspect}
loading={loading}
loadPage={loadPage}
refetch={refetch}
setQuery={setQuery}
showMorePagesIndicator={getOr(
false,
'showMorePagesIndicator',
pageInfo
)}
totalCount={totalCount}
type={networkModel.NetworkType.page}
/>
)}
</NetworkTopNFlowQuery>
const NetworkComponent = React.memo<NetworkComponentProps>(
({ filterQuery, setAbsoluteRangeDatePicker }) => {
return (
<WithSource sourceId="default">
{({ indicesExist, indexPattern }) =>
indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
<StickyContainer>
<FiltersGlobal>
<NetworkKql indexPattern={indexPattern} type={networkModel.NetworkType.page} />
</FiltersGlobal>
<EuiSpacer />
<HeaderPage
subtitle={<LastEventTime indexKey={LastEventIndexKey.network} />}
title={i18n.PAGE_TITLE}
/>
<NetworkDnsQuery
endDate={to}
filterQuery={filterQuery}
skip={isInitializing}
sourceId="default"
startDate={from}
type={networkModel.NetworkType.page}
>
{({
totalCount,
loading,
networkDns,
pageInfo,
loadPage,
id,
inspect,
refetch,
}) => (
<NetworkDnsTableManage
data={networkDns}
fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
id={id}
inspect={inspect}
loading={loading}
loadPage={loadPage}
refetch={refetch}
setQuery={setQuery}
showMorePagesIndicator={getOr(
false,
'showMorePagesIndicator',
pageInfo
)}
totalCount={totalCount}
type={networkModel.NetworkType.page}
/>
)}
</NetworkDnsQuery>
<GlobalTime>
{({ to, from, setQuery }) => (
<UseUrlState indexPattern={indexPattern}>
{({ isInitializing }) => (
<>
<KpiNetworkQuery
endDate={to}
filterQuery={filterQuery}
skip={isInitializing}
sourceId="default"
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) => {
setTimeout(() => {
setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
}, 500);
}}
/>
)}
</KpiNetworkQuery>
<EuiSpacer />
<EuiSpacer />
<AnomaliesNetworkTable
startDate={from}
endDate={to}
skip={isInitializing}
type={networkModel.NetworkType.page}
narrowDateRange={(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
setAbsoluteRangeDatePicker({
id: 'global',
from: fromTo.from,
to: fromTo.to,
});
}}
/>
</>
)}
</UseUrlState>
)}
</GlobalTime>
</StickyContainer>
) : (
<>
<HeaderPage title={i18n.PAGE_TITLE} />
<EuiFlexGroup direction={getFlexDirection()}>
<EuiFlexItem>
<NetworkTopNFlowQuery
endDate={to}
flowTarget={FlowTargetNew.source}
filterQuery={filterQuery}
skip={isInitializing}
sourceId="default"
startDate={from}
type={networkModel.NetworkType.page}
>
{({
id,
inspect,
loading,
loadPage,
networkTopNFlow,
pageInfo,
refetch,
totalCount,
}) => (
<NetworkTopNFlowTableManage
data={networkTopNFlow}
fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
flowTargeted={FlowTargetNew.source}
id={id}
indexPattern={indexPattern}
inspect={inspect}
loading={loading}
loadPage={loadPage}
refetch={refetch}
setQuery={setQuery}
showMorePagesIndicator={getOr(
false,
'showMorePagesIndicator',
pageInfo
)}
totalCount={totalCount}
type={networkModel.NetworkType.page}
/>
)}
</NetworkTopNFlowQuery>
</EuiFlexItem>
<NetworkEmptyPage />
</>
)
}
</WithSource>
)
<EuiFlexItem>
<NetworkTopNFlowQuery
endDate={to}
flowTarget={FlowTargetNew.destination}
filterQuery={filterQuery}
skip={isInitializing}
sourceId="default"
startDate={from}
type={networkModel.NetworkType.page}
>
{({
id,
inspect,
loading,
loadPage,
networkTopNFlow,
pageInfo,
refetch,
totalCount,
}) => (
<NetworkTopNFlowTableManage
data={networkTopNFlow}
fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
flowTargeted={FlowTargetNew.destination}
id={id}
indexPattern={indexPattern}
inspect={inspect}
loading={loading}
loadPage={loadPage}
refetch={refetch}
setQuery={setQuery}
showMorePagesIndicator={getOr(
false,
'showMorePagesIndicator',
pageInfo
)}
totalCount={totalCount}
type={networkModel.NetworkType.page}
/>
)}
</NetworkTopNFlowQuery>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<NetworkDnsQuery
endDate={to}
filterQuery={filterQuery}
skip={isInitializing}
sourceId="default"
startDate={from}
type={networkModel.NetworkType.page}
>
{({
totalCount,
loading,
networkDns,
pageInfo,
loadPage,
id,
inspect,
refetch,
}) => (
<NetworkDnsTableManage
data={networkDns}
fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
id={id}
inspect={inspect}
loading={loading}
loadPage={loadPage}
refetch={refetch}
setQuery={setQuery}
showMorePagesIndicator={getOr(
false,
'showMorePagesIndicator',
pageInfo
)}
totalCount={totalCount}
type={networkModel.NetworkType.page}
/>
)}
</NetworkDnsQuery>
<EuiSpacer />
<AnomaliesNetworkTable
startDate={from}
endDate={to}
skip={isInitializing}
type={networkModel.NetworkType.page}
narrowDateRange={(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
setAbsoluteRangeDatePicker({
id: 'global',
from: fromTo.from,
to: fromTo.to,
});
}}
/>
</>
)}
</UseUrlState>
)}
</GlobalTime>
</StickyContainer>
) : (
<>
<HeaderPage title={i18n.PAGE_TITLE} />
<NetworkEmptyPage />
</>
)
}
</WithSource>
);
}
);
NetworkComponent.displayName = 'NetworkComponent';

View file

@ -15,7 +15,7 @@ import {
TlsSortField,
UsersSortField,
} from '../../graphql/types';
import { KueryFilterQuery, SerializedFilterQuery } from '../model';
import { KueryFilterQuery, networkModel, SerializedFilterQuery } from '../model';
import { IpDetailsTableType, NetworkTableType, NetworkType } from './model';
@ -49,22 +49,15 @@ export const updateIsPtrIncluded = actionCreator<{
export const updateTopNFlowLimit = actionCreator<{
limit: number;
networkType: NetworkType;
tableType: networkModel.TopNTableType;
}>('UPDATE_TOP_N_FLOW_LIMIT');
export const updateTopNFlowSort = actionCreator<{
topNFlowSort: NetworkTopNFlowSortField;
networkType: NetworkType;
tableType: networkModel.NetworkTableType;
}>('UPDATE_TOP_N_FLOW_SORT');
export const updateTopNFlowTarget = actionCreator<{
flowTarget: FlowTarget;
}>('UPDATE_TOP_N_FLOW_TARGET');
export const updateTopNFlowDirection = actionCreator<{
flowDirection: FlowDirection;
networkType: NetworkType;
}>('UPDATE_TOP_N_FLOW_DIRECTION');
export const setNetworkFilterQueryDraft = actionCreator<{
filterQueryDraft: KueryFilterQuery;
networkType: NetworkType;

View file

@ -1,30 +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 {
Direction,
FlowDirection,
FlowTarget,
NetworkTopNFlowFields,
NetworkTopNFlowSortField,
} from '../../graphql/types';
export const helperUpdateTopNFlowDirection = (
flowTarget: FlowTarget,
flowDirection: FlowDirection
) => {
const topNFlowSort: NetworkTopNFlowSortField = {
field: NetworkTopNFlowFields.bytes,
direction: Direction.desc,
};
if (
flowDirection === FlowDirection.uniDirectional &&
[FlowTarget.client, FlowTarget.server].includes(flowTarget)
) {
return { flowDirection, flowTarget: FlowTarget.source, topNFlowSort };
}
return { flowDirection, topNFlowSort };
};

View file

@ -13,7 +13,7 @@ import {
TlsSortField,
UsersSortField,
} from '../../graphql/types';
import { KueryFilterQuery, SerializedFilterQuery } from '../model';
import { KueryFilterQuery, networkModel, SerializedFilterQuery } from '../model';
export enum NetworkType {
page = 'page',
@ -22,9 +22,14 @@ export enum NetworkType {
export enum NetworkTableType {
dns = 'dns',
topNFlow = 'topNFlow',
topNFlowSource = 'topNFlowSource',
topNFlowDestination = 'topNFlowDestination',
}
export type TopNTableType =
| networkModel.NetworkTableType.topNFlowDestination
| networkModel.NetworkTableType.topNFlowSource;
export enum IpDetailsTableType {
domains = 'domains',
tls = 'tls',
@ -38,9 +43,7 @@ export interface BasicQueryPaginated {
// Network Page Models
export interface TopNFlowQuery extends BasicQueryPaginated {
flowTarget: FlowTarget;
topNFlowSort: NetworkTopNFlowSortField;
flowDirection: FlowDirection;
}
export interface DnsQuery extends BasicQueryPaginated {
@ -50,7 +53,8 @@ export interface DnsQuery extends BasicQueryPaginated {
interface NetworkQueries {
[NetworkTableType.dns]: DnsQuery;
[NetworkTableType.topNFlow]: TopNFlowQuery;
[NetworkTableType.topNFlowSource]: TopNFlowQuery;
[NetworkTableType.topNFlowDestination]: TopNFlowQuery;
}
export interface NetworkPageModel {

View file

@ -31,15 +31,12 @@ import {
updateIsPtrIncluded,
updateIpDetailsTableActivePage,
updateNetworkPageTableActivePage,
updateTopNFlowDirection,
updateTopNFlowLimit,
updateTopNFlowSort,
updateTopNFlowTarget,
updateTlsSort,
updateUsersLimit,
updateUsersSort,
} from './actions';
import { helperUpdateTopNFlowDirection } from './helper';
import { IpDetailsTableType, NetworkModel, NetworkTableType, NetworkType } from './model';
export type NetworkState = NetworkModel;
@ -47,15 +44,21 @@ export type NetworkState = NetworkModel;
export const initialNetworkState: NetworkState = {
page: {
queries: {
[NetworkTableType.topNFlow]: {
[NetworkTableType.topNFlowSource]: {
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
limit: DEFAULT_TABLE_LIMIT,
topNFlowSort: {
field: NetworkTopNFlowFields.bytes,
field: NetworkTopNFlowFields.bytes_out,
direction: Direction.desc,
},
},
[NetworkTableType.topNFlowDestination]: {
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
limit: DEFAULT_TABLE_LIMIT,
topNFlowSort: {
field: NetworkTopNFlowFields.bytes_out,
direction: Direction.desc,
},
flowTarget: FlowTarget.source,
flowDirection: FlowDirection.uniDirectional,
},
[NetworkTableType.dns]: {
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
@ -170,65 +173,32 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
},
},
}))
.case(updateTopNFlowLimit, (state, { limit, networkType }) => ({
.case(updateTopNFlowLimit, (state, { limit, networkType, tableType }) => ({
...state,
[networkType]: {
...state[networkType],
queries: {
...state[networkType].queries,
[NetworkTableType.topNFlow]: {
...state[NetworkType.page].queries.topNFlow,
[tableType]: {
...state[NetworkType.page].queries[tableType],
limit,
},
},
},
}))
.case(updateTopNFlowDirection, (state, { flowDirection, networkType }) => ({
.case(updateTopNFlowSort, (state, { topNFlowSort, networkType, tableType }) => ({
...state,
[networkType]: {
...state[networkType],
queries: {
...state[networkType].queries,
[NetworkTableType.topNFlow]: {
...state[NetworkType.page].queries.topNFlow,
...helperUpdateTopNFlowDirection(
state[NetworkType.page].queries.topNFlow.flowTarget,
flowDirection
),
},
},
},
}))
.case(updateTopNFlowSort, (state, { topNFlowSort, networkType }) => ({
...state,
[networkType]: {
...state[networkType],
queries: {
...state[networkType].queries,
[NetworkTableType.topNFlow]: {
...state[NetworkType.page].queries.topNFlow,
[tableType]: {
...state[NetworkType.page].queries[tableType],
topNFlowSort,
},
},
},
}))
.case(updateTopNFlowTarget, (state, { flowTarget }) => ({
...state,
[NetworkType.page]: {
...state[NetworkType.page],
queries: {
...state[NetworkType.page].queries,
[NetworkTableType.topNFlow]: {
...state[NetworkType.page].queries.topNFlow,
flowTarget,
topNFlowSort: {
field: NetworkTopNFlowFields.bytes,
direction: Direction.desc,
},
},
},
},
}))
.case(setNetworkFilterQueryDraft, (state, { filterQueryDraft, networkType }) => ({
...state,
[networkType]: {

View file

@ -11,6 +11,7 @@ import { isFromKueryExpressionValid } from '../../lib/keury';
import { State } from '../reducer';
import { NetworkDetailsModel, NetworkPageModel, NetworkType } from './model';
import { FlowTargetNew } from '../../graphql/types';
const selectNetworkPage = (state: State): NetworkPageModel => state.network.page;
@ -25,11 +26,18 @@ export const dnsSelector = () =>
selectNetworkPage,
network => network.queries.dns
);
export const topNFlowSelector = () =>
export enum NetworkTableType {
dns = 'dns',
topNFlowSource = 'topNFlowSource',
topNFlowDestination = 'topNFlowDestination',
}
export const topNFlowSelector = (flowTarget: FlowTargetNew) =>
createSelector(
selectNetworkPage,
network => network.queries.topNFlow
network =>
flowTarget === FlowTargetNew.source
? network.queries[NetworkTableType.topNFlowSource]
: network.queries[NetworkTableType.topNFlowDestination]
);
// Filter Query Selectors

View file

@ -38,7 +38,6 @@ export const createNetworkResolvers = (
...createOptionsPaginated(source, args, info),
flowTarget: args.flowTarget,
networkTopNFlowSort: args.sort,
flowDirection: args.flowDirection,
};
return libs.network.getNetworkTopNFlow(req, options);
},

View file

@ -19,22 +19,44 @@ export const networkSchema = gql`
}
type TopNFlowNetworkEcsField {
bytes: Float
packets: Float
transport: String
direction: [NetworkDirectionEcs!]
bytes_in: Float
bytes_out: Float
}
type TopNFlowItem {
count: Float
type GeoItem {
geo: GeoEcsFields
flowTarget: FlowTarget
}
type AutonomousSystemItem {
name: String
number: Float
}
type TopNFlowItemSource {
autonomous_system: AutonomousSystemItem
domain: [String!]
ip: String
location: GeoItem
flows: Float
destination_ips: Float
}
type TopNFlowItemDestination {
autonomous_system: AutonomousSystemItem
domain: [String!]
ip: String
location: GeoItem
flows: Float
source_ips: Float
}
enum NetworkTopNFlowFields {
bytes
packets
ipCount
bytes_in
bytes_out
flows
destination_ips
source_ips
}
input NetworkTopNFlowSortField {
@ -44,10 +66,8 @@ export const networkSchema = gql`
type NetworkTopNFlowItem {
_id: String
source: TopNFlowItem
destination: TopNFlowItem
client: TopNFlowItem
server: TopNFlowItem
source: TopNFlowItemSource
destination: TopNFlowItemDestination
network: TopNFlowNetworkEcsField
}
@ -102,8 +122,7 @@ export const networkSchema = gql`
NetworkTopNFlow(
id: String
filterQuery: String
flowDirection: FlowDirection!
flowTarget: FlowTarget!
flowTarget: FlowTargetNew!
pagination: PaginationInputPaginated!
sort: NetworkTopNFlowSortField!
timerange: TimerangeInput!

View file

@ -1195,33 +1195,57 @@ export interface NetworkTopNFlowEdges {
export interface NetworkTopNFlowItem {
_id?: string | null;
source?: TopNFlowItem | null;
source?: TopNFlowItemSource | null;
destination?: TopNFlowItem | null;
client?: TopNFlowItem | null;
server?: TopNFlowItem | null;
destination?: TopNFlowItemDestination | null;
network?: TopNFlowNetworkEcsField | null;
}
export interface TopNFlowItem {
count?: number | null;
export interface TopNFlowItemSource {
autonomous_system?: AutonomousSystemItem | null;
domain?: string[] | null;
ip?: string | null;
location?: GeoItem | null;
flows?: number | null;
destination_ips?: number | null;
}
export interface AutonomousSystemItem {
name?: string | null;
number?: number | null;
}
export interface GeoItem {
geo?: GeoEcsFields | null;
flowTarget?: FlowTarget | null;
}
export interface TopNFlowItemDestination {
autonomous_system?: AutonomousSystemItem | null;
domain?: string[] | null;
ip?: string | null;
location?: GeoItem | null;
flows?: number | null;
source_ips?: number | null;
}
export interface TopNFlowNetworkEcsField {
bytes?: number | null;
bytes_in?: number | null;
packets?: number | null;
transport?: string | null;
direction?: NetworkDirectionEcs[] | null;
bytes_out?: number | null;
}
export interface NetworkDnsData {
@ -1984,9 +2008,7 @@ export interface NetworkTopNFlowSourceArgs {
filterQuery?: string | null;
flowDirection: FlowDirection;
flowTarget: FlowTarget;
flowTarget: FlowTargetNew;
pagination: PaginationInputPaginated;
@ -2152,10 +2174,17 @@ export enum UsersFields {
count = 'count',
}
export enum FlowTargetNew {
destination = 'destination',
source = 'source',
}
export enum NetworkTopNFlowFields {
bytes = 'bytes',
packets = 'packets',
ipCount = 'ipCount',
bytes_in = 'bytes_in',
bytes_out = 'bytes_out',
flows = 'flows',
destination_ips = 'destination_ips',
source_ips = 'source_ips',
}
export enum NetworkDnsFields {
@ -2815,9 +2844,7 @@ export namespace SourceResolvers {
filterQuery?: string | null;
flowDirection: FlowDirection;
flowTarget: FlowTarget;
flowTarget: FlowTargetNew;
pagination: PaginationInputPaginated;
@ -6303,13 +6330,9 @@ export namespace NetworkTopNFlowItemResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = NetworkTopNFlowItem> {
_id?: IdResolver<string | null, TypeParent, Context>;
source?: SourceResolver<TopNFlowItem | null, TypeParent, Context>;
source?: SourceResolver<TopNFlowItemSource | null, TypeParent, Context>;
destination?: DestinationResolver<TopNFlowItem | null, TypeParent, Context>;
client?: ClientResolver<TopNFlowItem | null, TypeParent, Context>;
server?: ServerResolver<TopNFlowItem | null, TypeParent, Context>;
destination?: DestinationResolver<TopNFlowItemDestination | null, TypeParent, Context>;
network?: NetworkResolver<TopNFlowNetworkEcsField | null, TypeParent, Context>;
}
@ -6320,22 +6343,12 @@ export namespace NetworkTopNFlowItemResolvers {
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type SourceResolver<
R = TopNFlowItem | null,
R = TopNFlowItemSource | null,
Parent = NetworkTopNFlowItem,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type DestinationResolver<
R = TopNFlowItem | null,
Parent = NetworkTopNFlowItem,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type ClientResolver<
R = TopNFlowItem | null,
Parent = NetworkTopNFlowItem,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type ServerResolver<
R = TopNFlowItem | null,
R = TopNFlowItemDestination | null,
Parent = NetworkTopNFlowItem,
Context = SiemContext
> = Resolver<R, Parent, Context>;
@ -6346,63 +6359,155 @@ export namespace NetworkTopNFlowItemResolvers {
> = Resolver<R, Parent, Context>;
}
export namespace TopNFlowItemResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = TopNFlowItem> {
count?: CountResolver<number | null, TypeParent, Context>;
export namespace TopNFlowItemSourceResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = TopNFlowItemSource> {
autonomous_system?: AutonomousSystemResolver<AutonomousSystemItem | null, TypeParent, Context>;
domain?: DomainResolver<string[] | null, TypeParent, Context>;
ip?: IpResolver<string | null, TypeParent, Context>;
location?: LocationResolver<GeoItem | null, TypeParent, Context>;
flows?: FlowsResolver<number | null, TypeParent, Context>;
destination_ips?: DestinationIpsResolver<number | null, TypeParent, Context>;
}
export type CountResolver<
R = number | null,
Parent = TopNFlowItem,
export type AutonomousSystemResolver<
R = AutonomousSystemItem | null,
Parent = TopNFlowItemSource,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type DomainResolver<
R = string[] | null,
Parent = TopNFlowItem,
Parent = TopNFlowItemSource,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type IpResolver<
R = string | null,
Parent = TopNFlowItem,
Parent = TopNFlowItemSource,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type LocationResolver<
R = GeoItem | null,
Parent = TopNFlowItemSource,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type FlowsResolver<
R = number | null,
Parent = TopNFlowItemSource,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type DestinationIpsResolver<
R = number | null,
Parent = TopNFlowItemSource,
Context = SiemContext
> = Resolver<R, Parent, Context>;
}
export namespace AutonomousSystemItemResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = AutonomousSystemItem> {
name?: NameResolver<string | null, TypeParent, Context>;
number?: NumberResolver<number | null, TypeParent, Context>;
}
export type NameResolver<
R = string | null,
Parent = AutonomousSystemItem,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type NumberResolver<
R = number | null,
Parent = AutonomousSystemItem,
Context = SiemContext
> = Resolver<R, Parent, Context>;
}
export namespace GeoItemResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = GeoItem> {
geo?: GeoResolver<GeoEcsFields | null, TypeParent, Context>;
flowTarget?: FlowTargetResolver<FlowTarget | null, TypeParent, Context>;
}
export type GeoResolver<
R = GeoEcsFields | null,
Parent = GeoItem,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type FlowTargetResolver<
R = FlowTarget | null,
Parent = GeoItem,
Context = SiemContext
> = Resolver<R, Parent, Context>;
}
export namespace TopNFlowItemDestinationResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = TopNFlowItemDestination> {
autonomous_system?: AutonomousSystemResolver<AutonomousSystemItem | null, TypeParent, Context>;
domain?: DomainResolver<string[] | null, TypeParent, Context>;
ip?: IpResolver<string | null, TypeParent, Context>;
location?: LocationResolver<GeoItem | null, TypeParent, Context>;
flows?: FlowsResolver<number | null, TypeParent, Context>;
source_ips?: SourceIpsResolver<number | null, TypeParent, Context>;
}
export type AutonomousSystemResolver<
R = AutonomousSystemItem | null,
Parent = TopNFlowItemDestination,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type DomainResolver<
R = string[] | null,
Parent = TopNFlowItemDestination,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type IpResolver<
R = string | null,
Parent = TopNFlowItemDestination,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type LocationResolver<
R = GeoItem | null,
Parent = TopNFlowItemDestination,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type FlowsResolver<
R = number | null,
Parent = TopNFlowItemDestination,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type SourceIpsResolver<
R = number | null,
Parent = TopNFlowItemDestination,
Context = SiemContext
> = Resolver<R, Parent, Context>;
}
export namespace TopNFlowNetworkEcsFieldResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = TopNFlowNetworkEcsField> {
bytes?: BytesResolver<number | null, TypeParent, Context>;
bytes_in?: BytesInResolver<number | null, TypeParent, Context>;
packets?: PacketsResolver<number | null, TypeParent, Context>;
transport?: TransportResolver<string | null, TypeParent, Context>;
direction?: DirectionResolver<NetworkDirectionEcs[] | null, TypeParent, Context>;
bytes_out?: BytesOutResolver<number | null, TypeParent, Context>;
}
export type BytesResolver<
export type BytesInResolver<
R = number | null,
Parent = TopNFlowNetworkEcsField,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type PacketsResolver<
export type BytesOutResolver<
R = number | null,
Parent = TopNFlowNetworkEcsField,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type TransportResolver<
R = string | null,
Parent = TopNFlowNetworkEcsField,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type DirectionResolver<
R = NetworkDirectionEcs[] | null,
Parent = TopNFlowNetworkEcsField,
Context = SiemContext
> = Resolver<R, Parent, Context>;
}
export namespace NetworkDnsDataResolvers {

View file

@ -6,19 +6,21 @@
import { cloneDeep } from 'lodash/fp';
import { NetworkTopNFlowData } from '../../graphql/types';
import { FlowTargetNew, NetworkTopNFlowData } from '../../graphql/types';
import { FrameworkAdapter, FrameworkRequest } from '../framework';
import { ElasticsearchNetworkAdapter } from './elasticsearch_adapter';
import { mockOptions, mockRequest, mockResponse, mockResult, mockTopNFlowQueryDsl } from './mock';
jest.mock('./query_top_n_flow.dsl', () => {
const r = jest.requireActual('./query_top_n_flow.dsl');
return {
...r,
buildTopNFlowQuery: jest.fn(() => mockTopNFlowQueryDsl),
};
});
describe('Network Top N flow elasticsearch_adapter with FlowTarget=source and FlowDirection=uniDirectional', () => {
describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () => {
describe('Happy Path - get Data', () => {
const mockCallWithRequest = jest.fn();
mockCallWithRequest.mockResolvedValue(mockResponse);
@ -47,7 +49,7 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source and Fl
describe('Unhappy Path - No data', () => {
const mockNoDataResponse = cloneDeep(mockResponse);
mockNoDataResponse.aggregations.top_n_flow_count.value = 0;
mockNoDataResponse.aggregations.top_uni_flow.buckets = [];
mockNoDataResponse.aggregations[FlowTargetNew.source].buckets = [];
const mockCallWithRequest = jest.fn();
mockCallWithRequest.mockResolvedValue(mockNoDataResponse);
const mockFramework: FrameworkAdapter = {
@ -87,10 +89,9 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source and Fl
describe('No pagination', () => {
const mockNoPaginationResponse = cloneDeep(mockResponse);
mockNoPaginationResponse.aggregations.top_n_flow_count.value = 10;
mockNoPaginationResponse.aggregations.top_uni_flow.buckets = mockNoPaginationResponse.aggregations.top_uni_flow.buckets.slice(
0,
-1
);
mockNoPaginationResponse.aggregations[
FlowTargetNew.source
].buckets = mockNoPaginationResponse.aggregations[FlowTargetNew.source].buckets.slice(0, -1);
const mockCallWithRequest = jest.fn();
mockCallWithRequest.mockResolvedValue(mockNoPaginationResponse);
const mockFramework: FrameworkAdapter = {

View file

@ -7,8 +7,10 @@
import { get, getOr } from 'lodash/fp';
import {
FlowDirection,
FlowTargetNew,
AutonomousSystemItem,
FlowTarget,
GeoItem,
NetworkDnsData,
NetworkDnsEdges,
NetworkTopNFlowData,
@ -21,7 +23,7 @@ import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants';
import { NetworkDnsRequestOptions, NetworkTopNFlowRequestOptions } from './index';
import { buildDnsQuery } from './query_dns.dsl';
import { buildTopNFlowQuery } from './query_top_n_flow.dsl';
import { buildTopNFlowQuery, getOppositeField } from './query_top_n_flow.dsl';
import { NetworkAdapter, NetworkDnsBuckets, NetworkTopNFlowBuckets } from './types';
export class ElasticsearchNetworkAdapter implements NetworkAdapter {
@ -31,6 +33,9 @@ export class ElasticsearchNetworkAdapter implements NetworkAdapter {
request: FrameworkRequest,
options: NetworkTopNFlowRequestOptions
): Promise<NetworkTopNFlowData> {
if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) {
throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`);
}
const dsl = buildTopNFlowQuery(options);
const response = await this.framework.callWithRequest<NetworkTopNFlowData, TermAggregation>(
request,
@ -64,6 +69,9 @@ export class ElasticsearchNetworkAdapter implements NetworkAdapter {
request: FrameworkRequest,
options: NetworkDnsRequestOptions
): Promise<NetworkDnsData> {
if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) {
throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`);
}
const dsl = buildDnsQuery(options);
const response = await this.framework.callWithRequest<NetworkDnsData, TermAggregation>(
request,
@ -99,37 +107,73 @@ const getTopNFlowEdges = (
response: DatabaseSearchResponse<NetworkTopNFlowData, TermAggregation>,
options: NetworkTopNFlowRequestOptions
): NetworkTopNFlowEdges[] => {
if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) {
throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`);
}
if (options.flowDirection === FlowDirection.uniDirectional) {
return formatTopNFlowEdges(
getOr([], 'aggregations.top_uni_flow.buckets', response),
options.flowTarget
);
}
return formatTopNFlowEdges(
getOr([], 'aggregations.top_bi_flow.buckets', response),
getOr([], `aggregations.${options.flowTarget}.buckets`, response),
options.flowTarget
);
};
const getFlowTargetFromString = (flowAsString: string) =>
flowAsString === 'source' ? FlowTarget.source : FlowTarget.destination;
const getGeoItem = (result: NetworkTopNFlowBuckets): GeoItem | null =>
result.location.top_geo.hits.hits.length > 0
? {
geo: getOr(
'',
`location.top_geo.hits.hits[0]._source.${
Object.keys(result.location.top_geo.hits.hits[0]._source)[0]
}.geo`,
result
),
flowTarget: getFlowTargetFromString(
Object.keys(result.location.top_geo.hits.hits[0]._source)[0]
),
}
: null;
const getAsItem = (result: NetworkTopNFlowBuckets): AutonomousSystemItem | null =>
result.autonomous_system.top_as.hits.hits.length > 0
? {
number: getOr(
null,
`autonomous_system.top_as.hits.hits[0]._source.${
Object.keys(result.location.top_geo.hits.hits[0]._source)[0]
}.as.number`,
result
),
name: getOr(
'',
`autonomous_system.top_as.hits.hits[0]._source.${
Object.keys(result.location.top_geo.hits.hits[0]._source)[0]
}.as.organization.name`,
result
),
}
: null;
const formatTopNFlowEdges = (
buckets: NetworkTopNFlowBuckets[],
flowTarget: FlowTarget
flowTarget: FlowTargetNew
): NetworkTopNFlowEdges[] =>
buckets.map((bucket: NetworkTopNFlowBuckets) => ({
node: {
_id: bucket.key,
[flowTarget]: {
count: getOrNumber('ip_count.value', bucket),
domain: bucket.domain.buckets.map(bucketDomain => bucketDomain.key),
ip: bucket.key,
location: getGeoItem(bucket),
autonomous_system: getAsItem(bucket),
flows: getOr(0, 'flows.value', bucket),
[`${getOppositeField(flowTarget)}_ips`]: getOr(
0,
`${getOppositeField(flowTarget)}_ips.value`,
bucket
),
},
network: {
bytes: getOrNumber('bytes.value', bucket),
packets: getOrNumber('packets.value', bucket),
direction: bucket.direction.buckets.map(bucketDir => bucketDir.key),
bytes_in: getOr(0, 'bytes_in.value', bucket),
bytes_out: getOr(0, 'bytes_out.value', bucket),
},
},
cursor: {

View file

@ -5,8 +5,7 @@
*/
import {
FlowDirection,
FlowTarget,
FlowTargetNew,
NetworkDnsSortField,
NetworkTopNFlowData,
NetworkTopNFlowSortField,
@ -19,8 +18,7 @@ export * from './types';
export interface NetworkTopNFlowRequestOptions extends RequestOptionsPaginated {
networkTopNFlowSort: NetworkTopNFlowSortField;
flowTarget: FlowTarget;
flowDirection: FlowDirection;
flowTarget: FlowTargetNew;
}
export interface NetworkDnsRequestOptions extends RequestOptionsPaginated {

File diff suppressed because it is too large Load diff

View file

@ -6,66 +6,15 @@
import {
Direction,
FlowDirection,
FlowTarget,
NetworkTopNFlowFields,
FlowTargetNew,
NetworkTopNFlowSortField,
NetworkTopNFlowFields,
} from '../../graphql/types';
import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query';
import { NetworkTopNFlowRequestOptions } from './index';
const getUniDirectionalFilter = (flowDirection: FlowDirection) =>
flowDirection === FlowDirection.uniDirectional
? {
must_not: [
{
exists: {
field: 'destination.bytes',
},
},
],
}
: {};
const getBiDirectionalFilter = (flowDirection: FlowDirection, flowTarget: FlowTarget) => {
if (
flowDirection === FlowDirection.biDirectional &&
[FlowTarget.source, FlowTarget.destination].includes(flowTarget)
) {
return [
{
exists: {
field: 'source.bytes',
},
},
{
exists: {
field: 'destination.bytes',
},
},
];
} else if (
flowDirection === FlowDirection.biDirectional &&
[FlowTarget.client, FlowTarget.server].includes(flowTarget)
) {
return [
{
exists: {
field: 'client.bytes',
},
},
{
exists: {
field: 'server.bytes',
},
},
];
}
return [];
};
const getCountAgg = (flowTarget: FlowTarget) => ({
const getCountAgg = (flowTarget: FlowTargetNew) => ({
top_n_flow_count: {
cardinality: {
field: `${flowTarget}.ip`,
@ -76,7 +25,6 @@ const getCountAgg = (flowTarget: FlowTarget) => ({
export const buildTopNFlowQuery = ({
defaultIndex,
filterQuery,
flowDirection,
flowTarget,
networkTopNFlowSort,
pagination: { querySize },
@ -88,7 +36,6 @@ export const buildTopNFlowQuery = ({
const filter = [
...createQueryFilterClauses(filterQuery),
{ range: { [timestamp]: { gte: from, lte: to } } },
...getBiDirectionalFilter(flowDirection, flowTarget),
];
const dslQuery = {
@ -98,13 +45,11 @@ export const buildTopNFlowQuery = ({
body: {
aggregations: {
...getCountAgg(flowTarget),
...getUniDirectionAggs(flowDirection, networkTopNFlowSort, flowTarget, querySize),
...getBiDirectionAggs(flowDirection, networkTopNFlowSort, flowTarget, querySize),
...getFlowTargetAggs(networkTopNFlowSort, flowTarget, querySize),
},
query: {
bool: {
filter,
...getUniDirectionalFilter(flowDirection),
},
},
},
@ -114,146 +59,118 @@ export const buildTopNFlowQuery = ({
return dslQuery;
};
const getUniDirectionAggs = (
flowDirection: FlowDirection,
const getFlowTargetAggs = (
networkTopNFlowSortField: NetworkTopNFlowSortField,
flowTarget: FlowTarget,
flowTarget: FlowTargetNew,
querySize: number
) =>
flowDirection === FlowDirection.uniDirectional
? {
top_uni_flow: {
terms: {
field: `${flowTarget}.ip`,
size: querySize,
order: {
...getQueryOrder(networkTopNFlowSortField),
},
) => ({
[flowTarget]: {
terms: {
field: `${flowTarget}.ip`,
size: querySize,
order: {
...getQueryOrder(networkTopNFlowSortField),
},
},
aggs: {
bytes_in: {
sum: {
field: `${getOppositeField(flowTarget)}.bytes`,
},
},
bytes_out: {
sum: {
field: `${flowTarget}.bytes`,
},
},
domain: {
terms: {
field: `${flowTarget}.domain`,
order: {
timestamp: 'desc',
},
aggs: {
bytes: {
sum: {
field: 'network.bytes',
},
},
direction: {
terms: {
field: 'network.direction',
},
},
domain: {
terms: {
field: `${flowTarget}.domain`,
order: {
timestamp: 'desc',
},
},
aggs: {
timestamp: {
max: {
field: '@timestamp',
},
},
},
},
ip_count: {
cardinality: {
field: `${
flowTarget === FlowTarget.source ? FlowTarget.destination : FlowTarget.source
}.ip`,
},
},
packets: {
sum: {
field: 'network.packets',
},
},
aggs: {
timestamp: {
max: {
field: '@timestamp',
},
},
},
}
: {};
const getBiDirectionAggs = (
flowDirection: FlowDirection,
networkTopNFlowSortField: NetworkTopNFlowSortField,
flowTarget: FlowTarget,
querySize: number
) =>
flowDirection === FlowDirection.biDirectional
? {
top_bi_flow: {
terms: {
field: `${flowTarget}.ip`,
size: querySize,
order: {
...getQueryOrder(networkTopNFlowSortField),
},
},
location: {
filter: {
exists: {
field: `${flowTarget}.geo`,
},
aggs: {
bytes: {
sum: {
field: `${flowTarget}.bytes`,
},
},
direction: {
terms: {
field: 'network.direction',
},
},
domain: {
terms: {
field: `${flowTarget}.domain`,
order: {
timestamp: 'desc',
},
},
aggs: {
timestamp: {
max: {
field: '@timestamp',
},
},
},
},
ip_count: {
cardinality: {
field: `${getOppositeField(flowTarget)}.ip`,
},
},
packets: {
sum: {
field: `${flowTarget}.packets`,
},
},
aggs: {
top_geo: {
top_hits: {
_source: `${flowTarget}.geo.*`,
size: 1,
},
},
},
}
: {};
},
autonomous_system: {
filter: {
exists: {
field: `${flowTarget}.as`,
},
},
aggs: {
top_as: {
top_hits: {
_source: `${flowTarget}.as.*`,
size: 1,
},
},
},
},
flows: {
cardinality: {
field: 'network.community_id',
},
},
[`${getOppositeField(flowTarget)}_ips`]: {
cardinality: {
field: `${getOppositeField(flowTarget)}.ip`,
},
},
},
},
});
const getOppositeField = (flowTarget: FlowTarget): FlowTarget => {
export const getOppositeField = (flowTarget: FlowTargetNew): FlowTargetNew => {
switch (flowTarget) {
case FlowTarget.source:
return FlowTarget.destination;
case FlowTarget.destination:
return FlowTarget.source;
case FlowTarget.server:
return FlowTarget.client;
case FlowTarget.client:
return FlowTarget.server;
case FlowTargetNew.source:
return FlowTargetNew.destination;
case FlowTargetNew.destination:
return FlowTargetNew.source;
}
assertUnreachable(flowTarget);
};
type QueryOrder = { bytes: Direction } | { packets: Direction } | { ip_count: Direction };
type QueryOrder =
| { bytes_in: Direction }
| { bytes_out: Direction }
| { flows: Direction }
| { destination_ips: Direction }
| { source_ips: Direction };
const getQueryOrder = (networkTopNFlowSortField: NetworkTopNFlowSortField): QueryOrder => {
switch (networkTopNFlowSortField.field) {
case NetworkTopNFlowFields.bytes:
return { bytes: networkTopNFlowSortField.direction };
case NetworkTopNFlowFields.packets:
return { packets: networkTopNFlowSortField.direction };
case NetworkTopNFlowFields.ipCount:
return { ip_count: networkTopNFlowSortField.direction };
case NetworkTopNFlowFields.bytes_in:
return { bytes_in: networkTopNFlowSortField.direction };
case NetworkTopNFlowFields.bytes_out:
return { bytes_out: networkTopNFlowSortField.direction };
case NetworkTopNFlowFields.flows:
return { flows: networkTopNFlowSortField.direction };
case NetworkTopNFlowFields.destination_ips:
return { destination_ips: networkTopNFlowSortField.direction };
case NetworkTopNFlowFields.source_ips:
return { source_ips: networkTopNFlowSortField.direction };
}
assertUnreachable(networkTopNFlowSortField.field);
};

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { NetworkDirectionEcs, NetworkDnsData, NetworkTopNFlowData } from '../../graphql/types';
import { NetworkDnsData, NetworkTopNFlowData } from '../../graphql/types';
import { FrameworkRequest, RequestOptionsPaginated } from '../framework';
import { SearchHit } from '../types';
import { SearchHit, TotalValue } from '../types';
export interface NetworkAdapter {
getNetworkTopNFlow(
@ -21,27 +21,58 @@ export interface GenericBuckets {
doc_count: number;
}
export interface DirectionBuckets {
key: NetworkDirectionEcs;
interface LocationHit<T> {
doc_count: number;
top_geo: {
hits: {
total: TotalValue | number;
max_score: number | null;
hits: Array<{
_source: T;
sort?: [number];
_index?: string;
_type?: string;
_id?: string;
_score?: number | null;
}>;
};
};
}
interface AutonomousSystemHit<T> {
doc_count: number;
top_as: {
hits: {
total: TotalValue | number;
max_score: number | null;
hits: Array<{
_source: T;
sort?: [number];
_index?: string;
_type?: string;
_id?: string;
_score?: number | null;
}>;
};
};
}
export interface NetworkTopNFlowBuckets {
key: string;
bytes: {
autonomous_system: AutonomousSystemHit<object>;
bytes_in: {
value: number;
};
packets: {
value: number;
};
ip_count: {
bytes_out: {
value: number;
};
domain: {
buckets: GenericBuckets[];
};
direction: {
buckets: DirectionBuckets[];
};
location: LocationHit<object>;
flows: number;
destination_ips?: number;
source_ips?: number;
}
export interface NetworkTopNFlowData extends SearchHit {
@ -49,10 +80,10 @@ export interface NetworkTopNFlowData extends SearchHit {
top_n_flow_count?: {
value: number;
};
top_uni_flow?: {
destination?: {
buckets: NetworkTopNFlowBuckets[];
};
top_bi_flow?: {
source?: {
buckets: NetworkTopNFlowBuckets[];
};
};

View file

@ -255,6 +255,7 @@
"history": "4.9.0",
"history-extra": "^5.0.1",
"humps": "2.0.1",
"i18n-iso-countries": "^4.3.1",
"icalendar": "0.7.1",
"idx": "^2.5.2",
"immer": "^1.5.0",

View file

@ -9409,24 +9409,9 @@
"xpack.siem.networkDnsTable.select.includePtrRecords": "PTR記録を含める",
"xpack.siem.networkDnsTable.title": "トップDNSドメイン",
"xpack.siem.networkDnsTable.unit": "{totalCount, plural, =1 {Domain} other {Domains}}",
"xpack.siem.networkTopNFlowTable.column.bytesTitle": "バイト",
"xpack.siem.networkTopNFlowTable.column.clientIpTitle": "クライアント IP",
"xpack.siem.networkTopNFlowTable.column.destinationIpTitle": "送信先 IP",
"xpack.siem.networkTopNFlowTable.column.directionTitle": "方向",
"xpack.siem.networkTopNFlowTable.column.lastDomainTitle": "最後のドメイン",
"xpack.siem.networkTopNFlowTable.column.packetsTitle": "パケット",
"xpack.siem.networkTopNFlowTable.column.serverIpTitle": "サーバーIP",
"xpack.siem.networkTopNFlowTable.column.sourceIpTitle": "送信元IP",
"xpack.siem.networkTopNFlowTable.column.uniqueClientIpsTitle": "固有のクライアントIP",
"xpack.siem.networkTopNFlowTable.column.uniqueDestinationIpsTitle": "固有の送信先IP",
"xpack.siem.networkTopNFlowTable.column.uniqueServerIpsTitle": "固有のサーバーIP",
"xpack.siem.networkTopNFlowTable.column.uniqueSourceIpsTitle": "固有の送信元 IP",
"xpack.siem.networkTopNFlowTable.rows": "{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}",
"xpack.siem.networkTopNFlowTable.select.byClientIpDropDownOptionLabel": "クライアントIP",
"xpack.siem.networkTopNFlowTable.select.byDestinationIpDropDownOptionLabel": "送信先 IP 別",
"xpack.siem.networkTopNFlowTable.select.byServerIpDropDownOptionLabel": "サーバー IP 別",
"xpack.siem.networkTopNFlowTable.select.bySourceIpDropDownOptionLabel": "送信元 IP 別",
"xpack.siem.networkTopNFlowTable.title": "トップトーカー",
"xpack.siem.networkTopNFlowTable.unit": "{totalCount, plural, =1 {IP} other {IPs}}",
"xpack.siem.notes.addANotePlaceholder": "メモを追加",
"xpack.siem.notes.addedANoteLabel": "メモを追加しました",

View file

@ -9551,24 +9551,9 @@
"xpack.siem.networkDnsTable.select.includePtrRecords": "包括 PTR 记录",
"xpack.siem.networkDnsTable.title": "排名靠前的 DNS 域",
"xpack.siem.networkDnsTable.unit": "{totalCount, plural, =1 {Domain} other {Domains}}",
"xpack.siem.networkTopNFlowTable.column.bytesTitle": "字节",
"xpack.siem.networkTopNFlowTable.column.clientIpTitle": "客户端 IP",
"xpack.siem.networkTopNFlowTable.column.destinationIpTitle": "目标 IP",
"xpack.siem.networkTopNFlowTable.column.directionTitle": "方向",
"xpack.siem.networkTopNFlowTable.column.lastDomainTitle": "最后域",
"xpack.siem.networkTopNFlowTable.column.packetsTitle": "数据包",
"xpack.siem.networkTopNFlowTable.column.serverIpTitle": "服务器 IP",
"xpack.siem.networkTopNFlowTable.column.sourceIpTitle": "源 IP",
"xpack.siem.networkTopNFlowTable.column.uniqueClientIpsTitle": "唯一客户端 IP",
"xpack.siem.networkTopNFlowTable.column.uniqueDestinationIpsTitle": "唯一目标 IP",
"xpack.siem.networkTopNFlowTable.column.uniqueServerIpsTitle": "唯一服务器 IP",
"xpack.siem.networkTopNFlowTable.column.uniqueSourceIpsTitle": "唯一源 IP",
"xpack.siem.networkTopNFlowTable.rows": "{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}",
"xpack.siem.networkTopNFlowTable.select.byClientIpDropDownOptionLabel": "按客户端 IP",
"xpack.siem.networkTopNFlowTable.select.byDestinationIpDropDownOptionLabel": "按目标 IP",
"xpack.siem.networkTopNFlowTable.select.byServerIpDropDownOptionLabel": "按服务器 IP",
"xpack.siem.networkTopNFlowTable.select.bySourceIpDropDownOptionLabel": "按源 IP",
"xpack.siem.networkTopNFlowTable.title": "排名靠前的网络流量生成者",
"xpack.siem.networkTopNFlowTable.unit": "{totalCount, plural, =1 {IP} other {IPs}}",
"xpack.siem.notes.addANotePlaceholder": "添加备注",
"xpack.siem.notes.addedANoteLabel": "已添加备注",

View file

@ -8,8 +8,7 @@ import expect from '@kbn/expect';
import { networkTopNFlowQuery } from '../../../../legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query';
import {
Direction,
FlowDirection,
FlowTarget,
FlowTargetNew,
GetNetworkTopNFlowQuery,
NetworkTopNFlowFields,
} from '../../../../legacy/plugins/siem/public/graphql/types';
@ -28,7 +27,7 @@ export default function({ getService }: FtrProviderContext) {
const FROM = new Date('2019-02-09T01:57:24.870Z').valueOf();
const TO = new Date('2019-02-12T01:57:24.870Z').valueOf();
it('Make sure that we get unidirectional Source NetworkTopNFlow data with bytes descending sort', () => {
it('Make sure that we get Source NetworkTopNFlow data with bytes_in descending sort', () => {
return client
.query<GetNetworkTopNFlowQuery.Query>({
query: networkTopNFlowQuery,
@ -39,9 +38,8 @@ export default function({ getService }: FtrProviderContext) {
to: TO,
from: FROM,
},
flowTarget: FlowTarget.source,
sort: { field: NetworkTopNFlowFields.bytes, direction: Direction.desc },
flowDirection: FlowDirection.uniDirectional,
flowTarget: FlowTargetNew.source,
sort: { field: NetworkTopNFlowFields.bytes_in, direction: Direction.desc },
pagination: {
activePage: 0,
cursorStart: 0,
@ -57,14 +55,16 @@ export default function({ getService }: FtrProviderContext) {
expect(networkTopNFlow.edges.length).to.be(EDGE_LENGTH);
expect(networkTopNFlow.totalCount).to.be(121);
expect(networkTopNFlow.edges.map(i => i.node.source!.ip).join(',')).to.be(
'8.250.107.245,10.100.7.198,8.248.211.247,8.253.157.240,151.205.0.21,8.254.254.117,54.239.220.40,151.205.0.23,8.248.223.246,151.205.0.17'
'10.100.7.196,10.100.7.199,10.100.7.197,10.100.7.198,3.82.33.170,17.249.172.100,10.100.4.1,8.248.209.244,8.248.211.247,8.248.213.244'
);
expect(networkTopNFlow.edges[0].node.destination).to.be(null);
expect(networkTopNFlow.edges[0].node.source!.flows).to.be(498);
expect(networkTopNFlow.edges[0].node.source!.destination_ips).to.be(132);
expect(networkTopNFlow.pageInfo.fakeTotalCount).to.equal(50);
});
});
it('Make sure that we get unidirectional Source NetworkTopNFlow data with bytes ascending sort ', () => {
it('Make sure that we get Source NetworkTopNFlow data with bytes_in ascending sort ', () => {
return client
.query<GetNetworkTopNFlowQuery.Query>({
query: networkTopNFlowQuery,
@ -75,9 +75,8 @@ export default function({ getService }: FtrProviderContext) {
to: TO,
from: FROM,
},
flowTarget: FlowTarget.source,
sort: { field: NetworkTopNFlowFields.bytes, direction: Direction.asc },
flowDirection: FlowDirection.uniDirectional,
flowTarget: FlowTargetNew.source,
sort: { field: NetworkTopNFlowFields.bytes_in, direction: Direction.asc },
pagination: {
activePage: 0,
cursorStart: 0,
@ -93,14 +92,16 @@ export default function({ getService }: FtrProviderContext) {
expect(networkTopNFlow.edges.length).to.be(EDGE_LENGTH);
expect(networkTopNFlow.totalCount).to.be(121);
expect(networkTopNFlow.edges.map(i => i.node.source!.ip).join(',')).to.be(
'10.100.4.1,54.239.219.220,54.239.219.228,54.239.220.94,54.239.220.138,54.239.220.184,54.239.220.186,54.239.221.253,35.167.45.163,52.5.171.20'
'8.248.209.244,8.248.211.247,8.248.213.244,8.248.223.246,8.250.107.245,8.250.121.236,8.250.125.244,8.253.38.231,8.253.157.112,8.253.157.240'
);
expect(networkTopNFlow.edges[0].node.destination).to.be(null);
expect(networkTopNFlow.edges[0].node.source!.flows).to.be(12);
expect(networkTopNFlow.edges[0].node.source!.destination_ips).to.be(1);
expect(networkTopNFlow.pageInfo.fakeTotalCount).to.equal(50);
});
});
it('Make sure that we get bidirectional Source NetworkTopNFlow data', () => {
it('Make sure that we get Destination NetworkTopNFlow data', () => {
return client
.query<GetNetworkTopNFlowQuery.Query>({
query: networkTopNFlowQuery,
@ -111,42 +112,8 @@ export default function({ getService }: FtrProviderContext) {
to: TO,
from: FROM,
},
sort: { field: NetworkTopNFlowFields.bytes, direction: Direction.desc },
flowTarget: FlowTarget.source,
flowDirection: FlowDirection.biDirectional,
pagination: {
activePage: 0,
cursorStart: 0,
fakePossibleCount: 10,
querySize: 10,
},
defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
inspect: false,
},
})
.then(resp => {
const networkTopNFlow = resp.data.source.NetworkTopNFlow;
expect(networkTopNFlow.edges.length).to.be(EDGE_LENGTH);
expect(networkTopNFlow.totalCount).to.be(10);
expect(networkTopNFlow.edges[0].node.destination).to.be(null);
expect(networkTopNFlow.pageInfo.fakeTotalCount).to.equal(10);
});
});
it('Make sure that we get unidirectional Destination NetworkTopNFlow data', () => {
return client
.query<GetNetworkTopNFlowQuery.Query>({
query: networkTopNFlowQuery,
variables: {
sourceId: 'default',
timerange: {
interval: '12h',
to: TO,
from: FROM,
},
sort: { field: NetworkTopNFlowFields.bytes, direction: Direction.desc },
flowTarget: FlowTarget.destination,
flowDirection: FlowDirection.uniDirectional,
sort: { field: NetworkTopNFlowFields.bytes_in, direction: Direction.desc },
flowTarget: FlowTargetNew.destination,
pagination: {
activePage: 0,
cursorStart: 0,
@ -160,40 +127,9 @@ export default function({ getService }: FtrProviderContext) {
.then(resp => {
const networkTopNFlow = resp.data.source.NetworkTopNFlow;
expect(networkTopNFlow.edges.length).to.be(EDGE_LENGTH);
expect(networkTopNFlow.totalCount).to.be(144);
expect(networkTopNFlow.edges[0].node.source).to.be(null);
expect(networkTopNFlow.pageInfo.fakeTotalCount).to.equal(50);
});
});
it('Make sure that we get bidirectional Destination NetworkTopNFlow data', () => {
return client
.query<GetNetworkTopNFlowQuery.Query>({
query: networkTopNFlowQuery,
variables: {
sourceId: 'default',
timerange: {
interval: '12h',
to: TO,
from: FROM,
},
sort: { field: NetworkTopNFlowFields.bytes, direction: Direction.desc },
flowTarget: FlowTarget.destination,
flowDirection: FlowDirection.biDirectional,
pagination: {
activePage: 0,
cursorStart: 0,
fakePossibleCount: 50,
querySize: 10,
},
defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
inspect: false,
},
})
.then(resp => {
const networkTopNFlow = resp.data.source.NetworkTopNFlow;
expect(networkTopNFlow.edges.length).to.be(EDGE_LENGTH);
expect(networkTopNFlow.totalCount).to.be(89);
expect(networkTopNFlow.totalCount).to.be(154);
expect(networkTopNFlow.edges[0].node.destination!.flows).to.be(19);
expect(networkTopNFlow.edges[0].node.destination!.source_ips).to.be(1);
expect(networkTopNFlow.edges[0].node.source).to.be(null);
expect(networkTopNFlow.pageInfo.fakeTotalCount).to.equal(50);
});
@ -210,9 +146,8 @@ export default function({ getService }: FtrProviderContext) {
to: TO,
from: FROM,
},
sort: { field: NetworkTopNFlowFields.bytes, direction: Direction.desc },
flowTarget: FlowTarget.source,
flowDirection: FlowDirection.uniDirectional,
sort: { field: NetworkTopNFlowFields.bytes_in, direction: Direction.desc },
flowTarget: FlowTargetNew.source,
pagination: {
activePage: 1,
cursorStart: 10,
@ -228,81 +163,7 @@ export default function({ getService }: FtrProviderContext) {
expect(networkTopNFlow.edges.length).to.be(EDGE_LENGTH);
expect(networkTopNFlow.totalCount).to.be(121);
expect(networkTopNFlow.edges[0].node.source!.ip).to.be('151.205.0.19');
});
});
});
describe('With packetbeat', () => {
before(() => esArchiver.load('packetbeat/default'));
after(() => esArchiver.unload('packetbeat/default'));
const FROM = new Date('2019-02-19T23:22:09.675Z').valueOf();
const TO = new Date('2019-02-19T23:26:50.001Z').valueOf();
it('Make sure that we get bidirectional Client NetworkTopNFlow data', () => {
return client
.query<GetNetworkTopNFlowQuery.Query>({
query: networkTopNFlowQuery,
variables: {
sourceId: 'default',
timerange: {
interval: '12h',
to: TO,
from: FROM,
},
sort: { field: NetworkTopNFlowFields.bytes, direction: Direction.desc },
flowTarget: FlowTarget.client,
flowDirection: FlowDirection.biDirectional,
pagination: {
activePage: 0,
cursorStart: 0,
fakePossibleCount: 50,
querySize: 10,
},
defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
inspect: false,
},
})
.then(resp => {
const networkTopNFlow = resp.data.source.NetworkTopNFlow;
expect(networkTopNFlow.edges.length).to.be(1);
expect(networkTopNFlow.totalCount).to.be(1);
expect(networkTopNFlow.edges[0].node.server).to.be(null);
expect(networkTopNFlow.pageInfo.fakeTotalCount).to.equal(1);
});
});
it('Make sure that we get bidirectional Server NetworkTopNFlow data', () => {
return client
.query<GetNetworkTopNFlowQuery.Query>({
query: networkTopNFlowQuery,
variables: {
sourceId: 'default',
timerange: {
interval: '12h',
to: TO,
from: FROM,
},
sort: { field: NetworkTopNFlowFields.bytes, direction: Direction.desc },
flowTarget: FlowTarget.server,
flowDirection: FlowDirection.biDirectional,
pagination: {
activePage: 0,
cursorStart: 0,
fakePossibleCount: 50,
querySize: 10,
},
defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
inspect: false,
},
})
.then(resp => {
const networkTopNFlow = resp.data.source.NetworkTopNFlow;
expect(networkTopNFlow.edges.length).to.be(1);
expect(networkTopNFlow.totalCount).to.be(1);
expect(networkTopNFlow.edges[0].node.client).to.be(null);
expect(networkTopNFlow.pageInfo.fakeTotalCount).to.equal(1);
expect(networkTopNFlow.edges[0].node.source!.ip).to.be('8.248.223.246');
});
});
});

View file

@ -10096,6 +10096,11 @@ di@^0.0.1:
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=
diacritics@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1"
integrity sha1-PvqHMj67hj5mls67AILUj/PW96E=
diagnostics@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/diagnostics/-/diagnostics-1.1.1.tgz#cab6ac33df70c9d9a727490ae43ac995a769b22a"
@ -14974,6 +14979,13 @@ hyperlinker@^1.0.0:
resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e"
integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==
i18n-iso-countries@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-4.3.1.tgz#f110a8824ce14edbb0eb8f3b0bd817ff950af37c"
integrity sha512-yxeCvmT8yO1p/epv93c1OHnnYNNMOX6NUNpNfuvzSIcDyripS7OGeKXgzYGd5QI31UK+GBrMG0nPFNv0jrHggw==
dependencies:
diacritics "^1.3.0"
icalendar@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/icalendar/-/icalendar-0.7.1.tgz#d0d3486795f8f1c5cf4f8cafac081b4b4e7a32ae"