Figure out some bottleneck, in the hosts tab (#35387)

* Figure out some blottleneck, this is making the app more responsive

* example how to remove the re-render on the host table

* review I

* review II

* review III
This commit is contained in:
Xavier Mouligneau 2019-04-22 17:43:57 -04:00 committed by GitHub
parent ea5b009b72
commit 8fdf129544
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 634 additions and 219 deletions

View file

@ -34,7 +34,7 @@ interface AutocompleteFieldState {
selectedIndex: number | null;
}
export class AutocompleteField extends React.Component<
export class AutocompleteField extends React.PureComponent<
AutocompleteFieldProps,
AutocompleteFieldState
> {

View file

@ -18,7 +18,7 @@ interface SuggestionItemProps {
suggestion: AutocompleteSuggestion;
}
export class SuggestionItem extends React.Component<SuggestionItemProps> {
export class SuggestionItem extends React.PureComponent<SuggestionItemProps> {
public static defaultProps: Partial<SuggestionItemProps> = {
isSelected: false,
};

View file

@ -4,11 +4,9 @@
* 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 * as React from 'react';
import { ThemeProvider } from 'styled-components';
import { Direction } from '../../graphql/types';
@ -61,21 +59,21 @@ describe('Load More Table Component', () => {
test('it renders the over loading panel after data has been in the table ', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<LoadMoreTable
columns={getHostsColumns()}
loadingTitle="Hosts"
loading={true}
pageOfItems={mockData.Hosts.edges}
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
limit={1}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
itemsPerRow={rowItems}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
title={<h3>Hosts</h3>}
/>
</ThemeProvider>
<LoadMoreTable
columns={getHostsColumns()}
loadingTitle="Hosts"
loading={false}
pageOfItems={mockData.Hosts.edges}
loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)}
limit={1}
hasNextPage={mockData.Hosts.pageInfo.hasNextPage!}
itemsPerRow={rowItems}
updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })}
title={<h3>Hosts</h3>}
/>
);
wrapper.setState({ isEmptyTable: false });
wrapper.setProps({ loading: true });
expect(wrapper.find('[data-test-subj="LoadingPanelLoadMoreTable"]').exists()).toBeTruthy();
});
@ -119,8 +117,9 @@ describe('Load More Table Component', () => {
title={<h3>Hosts</h3>}
/>
);
wrapper.setState({ paginationLoading: true, isEmptyTable: false });
wrapper.setProps({ loading: true });
expect(
wrapper.find('[data-test-subj="InitialLoadingPanelLoadMoreTable"]').exists()
).toBeFalsy();

View file

@ -15,7 +15,7 @@ import {
EuiPopover,
EuiTitle,
} from '@elastic/eui';
import { isEmpty, noop } from 'lodash/fp';
import { isEmpty, noop, getOr } from 'lodash/fp';
import React from 'react';
import styled from 'styled-components';
@ -79,21 +79,14 @@ export class LoadMoreTable<T> extends React.PureComponent<BasicTableProps<T>, Ba
paginationLoading: false,
};
public componentDidUpdate(prevProps: BasicTableProps<T>) {
const { paginationLoading, isEmptyTable } = this.state;
const { loading, pageOfItems } = this.props;
if (paginationLoading && prevProps.loading && !loading) {
this.setState({
...this.state,
paginationLoading: false,
});
}
if (isEmpty(prevProps.pageOfItems) && !isEmpty(pageOfItems) && isEmptyTable) {
this.setState({
...this.state,
static getDerivedStateFromProps(props: BasicTableProps<any>, state: BasicTableState) {
if (state.isEmptyTable && !isEmpty(props.pageOfItems)) {
return {
...state,
isEmptyTable: false,
});
};
}
return null;
}
public render() {
@ -110,7 +103,7 @@ export class LoadMoreTable<T> extends React.PureComponent<BasicTableProps<T>, Ba
title,
updateLimitPagination,
} = this.props;
const { isEmptyTable, paginationLoading } = this.state;
const { isEmptyTable } = this.state;
if (loading && isEmptyTable) {
return (
@ -149,10 +142,9 @@ export class LoadMoreTable<T> extends React.PureComponent<BasicTableProps<T>, Ba
{item.text}
</EuiContextMenuItem>
));
return (
<BasicTableContainer>
{!paginationLoading && loading && (
{loading && (
<>
<BackgroundRefetch />
<LoadingPanel
@ -216,7 +208,7 @@ export class LoadMoreTable<T> extends React.PureComponent<BasicTableProps<T>, Ba
<EuiButton
data-test-subj="loadingMoreButton"
isLoading={loading}
onClick={this.loadMore}
onClick={this.props.loadMore}
>
{loading ? `${i18n.LOADING}...` : i18n.LOAD_MORE}
</EuiButton>
@ -230,14 +222,6 @@ export class LoadMoreTable<T> extends React.PureComponent<BasicTableProps<T>, Ba
);
}
private loadMore = () => {
this.setState({
...this.state,
paginationLoading: true,
});
this.props.loadMore();
};
private onButtonClick = () => {
this.setState({
...this.state,
@ -266,8 +250,12 @@ const FooterAction = styled.div`
width: 100%;
`;
/*
* The getOr is just there to simplify the test
* So we do NOT need to wrap it around TestProvider
*/
const BackgroundRefetch = styled.div`
background-color: ${props => props.theme.eui.euiColorLightShade};
background-color: ${props => getOr('#ffffff', 'theme.eui.euiColorLightShade', props)};
margin: -5px;
height: calc(100% + 10px);
opacity: 0.7;

View file

@ -17,6 +17,7 @@ export class SiemNavigationComponent extends React.Component<RouteComponentProps
}
return true;
}
public componentWillMount(): void {
const { location } = this.props;
if (location.pathname) {

View file

@ -15,6 +15,91 @@ exports[`AddToKql Component Rendering 1`] = `
<pure(Component)
componentFilterType="hosts"
expression="host.name: siem-kibana"
indexPattern={
Object {
"fields": Array [
Object {
"aggregatable": true,
"name": "@timestamp",
"searchable": true,
"type": "date",
},
Object {
"aggregatable": true,
"name": "@version",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.ephemeral_id",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.hostname",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.id",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test1",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test2",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test3",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test4",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test5",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test6",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test7",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test8",
"searchable": true,
"type": "string",
},
],
"title": "filebeat-*,auditbeat-*,packetbeat-*",
}
}
type="page"
>
siem-kibana

View file

@ -9,7 +9,7 @@ import toJson from 'enzyme-to-json';
import * as React from 'react';
import { escapeQueryValue } from '../../../lib/keury';
import { mockGlobalState, TestProviders } from '../../../mock';
import { mockGlobalState, TestProviders, mockIndexPattern } from '../../../mock';
import { createStore, hostsModel, networkModel, State } from '../../../store';
import { AddToKql } from '.';
@ -26,6 +26,7 @@ describe('AddToKql Component', async () => {
const wrapper = shallow(
<TestProviders store={store}>
<AddToKql
indexPattern={mockIndexPattern}
expression={`host.name: ${escapeQueryValue('siem-kibana')}`}
componentFilterType="hosts"
type={hostsModel.HostsType.page}
@ -42,6 +43,7 @@ describe('AddToKql Component', async () => {
const wrapper = shallow(
<TestProviders store={store}>
<AddToKql
indexPattern={mockIndexPattern}
expression={`host.name: ${escapeQueryValue('siem-kibana')}`}
componentFilterType="hosts"
type={hostsModel.HostsType.page}
@ -60,6 +62,7 @@ describe('AddToKql Component', async () => {
const wrapper = mount(
<TestProviders store={store}>
<AddToKql
indexPattern={mockIndexPattern}
expression={`host.name: ${escapeQueryValue('siem-kibana')}`}
componentFilterType="hosts"
type={hostsModel.HostsType.page}
@ -112,6 +115,7 @@ describe('AddToKql Component', async () => {
const wrapper = mount(
<TestProviders store={store}>
<AddToKql
indexPattern={mockIndexPattern}
expression={`host.name: ${escapeQueryValue('siem-kibana')}`}
componentFilterType="network"
type={networkModel.NetworkType.page}

View file

@ -9,11 +9,10 @@ import { isEmpty } from 'lodash/fp';
import React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
import { StaticIndexPattern } from 'ui/index_patterns';
import { HostsFilter } from '../../../containers/hosts';
import { NetworkFilter } from '../../../containers/network';
import { WithSource } from '../../../containers/source';
import { IndexType } from '../../../graphql/types';
import { assertUnreachable } from '../../../lib/helpers';
import { hostsModel, KueryFilterQuery, networkModel } from '../../../store';
import { WithHoverActions } from '../../with_hover_actions';
@ -69,47 +68,44 @@ const HoverActionsContainer = styled(EuiPanel)`
interface AddToKqlProps {
children: JSX.Element;
indexPattern: StaticIndexPattern;
expression: string;
componentFilterType: 'network' | 'hosts';
type: networkModel.NetworkType | hostsModel.HostsType;
}
export const AddToKql = pure<AddToKqlProps>(
({ children, expression, type, componentFilterType }) => (
<WithSource sourceId="default" indexTypes={[IndexType.FILEBEAT, IndexType.PACKETBEAT]}>
{({ indexPattern }) => {
switch (componentFilterType) {
case 'hosts':
return (
<HostsFilter indexPattern={indexPattern} type={type as hostsModel.HostsType}>
{({ applyFilterQueryFromKueryExpression, filterQueryDraft }) => (
<AddToKqlComponent
applyFilterQueryFromKueryExpression={applyFilterQueryFromKueryExpression}
expression={expression}
filterQueryDraft={filterQueryDraft}
>
{children}
</AddToKqlComponent>
)}
</HostsFilter>
);
case 'network':
return (
<NetworkFilter indexPattern={indexPattern} type={type as networkModel.NetworkType}>
{({ applyFilterQueryFromKueryExpression, filterQueryDraft }) => (
<AddToKqlComponent
applyFilterQueryFromKueryExpression={applyFilterQueryFromKueryExpression}
expression={expression}
filterQueryDraft={filterQueryDraft}
>
{children}
</AddToKqlComponent>
)}
</NetworkFilter>
);
}
assertUnreachable(componentFilterType, 'Unknown Filter Type in switch statement');
}}
</WithSource>
)
({ children, expression, type, componentFilterType, indexPattern }) => {
switch (componentFilterType) {
case 'hosts':
return (
<HostsFilter indexPattern={indexPattern} type={type as hostsModel.HostsType}>
{({ applyFilterQueryFromKueryExpression, filterQueryDraft }) => (
<AddToKqlComponent
applyFilterQueryFromKueryExpression={applyFilterQueryFromKueryExpression}
expression={expression}
filterQueryDraft={filterQueryDraft}
>
{children}
</AddToKqlComponent>
)}
</HostsFilter>
);
case 'network':
return (
<NetworkFilter indexPattern={indexPattern} type={type as networkModel.NetworkType}>
{({ applyFilterQueryFromKueryExpression, filterQueryDraft }) => (
<AddToKqlComponent
applyFilterQueryFromKueryExpression={applyFilterQueryFromKueryExpression}
expression={expression}
filterQueryDraft={filterQueryDraft}
>
{children}
</AddToKqlComponent>
)}
</NetworkFilter>
);
}
assertUnreachable(componentFilterType, 'Unknown Filter Type in switch statement');
}
);

View file

@ -46,6 +46,91 @@ exports[`Load More Table Component rendering it renders the default Hosts table
]
}
hasNextPage={true}
indexPattern={
Object {
"fields": Array [
Object {
"aggregatable": true,
"name": "@timestamp",
"searchable": true,
"type": "date",
},
Object {
"aggregatable": true,
"name": "@version",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.ephemeral_id",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.hostname",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.id",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test1",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test2",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test3",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test4",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test5",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test6",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test7",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test8",
"searchable": true,
"type": "string",
},
],
"title": "filebeat-*,auditbeat-*,packetbeat-*",
}
}
loadMore={[MockFunction]}
loading={false}
nextCursor="aa7ca589f1b8220002f2fc61c64cfbf1"

View file

@ -7,6 +7,7 @@
import { EuiIcon, EuiToolTip } from '@elastic/eui';
import moment from 'moment';
import React from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
import { HostItem } from '../../../../graphql/types';
import { ValueOf } from '../../../../lib/helpers';
@ -24,7 +25,10 @@ import { AddToKql } from '../../add_to_kql';
import * as i18n from './translations';
export const getHostsColumns = (type: hostsModel.HostsType): Array<Columns<ValueOf<HostItem>>> => [
export const getHostsColumns = (
type: hostsModel.HostsType,
indexPattern: StaticIndexPattern
): Array<Columns<ValueOf<HostItem>>> => [
{
field: 'node.host.name',
name: i18n.NAME,
@ -53,6 +57,7 @@ export const getHostsColumns = (type: hostsModel.HostsType): Array<Columns<Value
</DragEffects>
) : (
<AddToKql
indexPattern={indexPattern}
expression={`host.name: ${escapeQueryValue(hostName)}`}
componentFilterType="hosts"
type={type}
@ -101,6 +106,7 @@ export const getHostsColumns = (type: hostsModel.HostsType): Array<Columns<Value
if (hostOsName != null) {
return (
<AddToKql
indexPattern={indexPattern}
expression={`host.os.name: ${escapeQueryValue(hostOsName)}`}
componentFilterType="hosts"
type={type}
@ -122,6 +128,7 @@ export const getHostsColumns = (type: hostsModel.HostsType): Array<Columns<Value
if (hostOsVersion != null) {
return (
<AddToKql
indexPattern={indexPattern}
expression={`host.os.version: ${escapeQueryValue(hostOsVersion)}`}
componentFilterType="hosts"
type={type}

View file

@ -11,7 +11,7 @@ import * as React from 'react';
import { MockedProvider } from 'react-apollo/test-utils';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { mockFrameworks, mockGlobalState, TestProviders } from '../../../../mock';
import { mockFrameworks, mockIndexPattern, mockGlobalState, TestProviders } from '../../../../mock';
import { createStore, hostsModel, State } from '../../../../store';
import { KibanaConfigContext } from '../../../formatted_date';
@ -34,6 +34,7 @@ describe('Load More Table Component', () => {
<ReduxStoreProvider store={store}>
<KibanaConfigContext.Provider value={mockFrameworks.default_UTC}>
<HostsTable
indexPattern={mockIndexPattern}
loading={false}
data={mockData.Hosts.edges}
totalCount={mockData.Hosts.totalCount}
@ -54,6 +55,7 @@ describe('Load More Table Component', () => {
<MockedProvider>
<TestProviders store={store}>
<HostsTable
indexPattern={mockIndexPattern}
loading={false}
data={mockData.Hosts.edges}
totalCount={mockData.Hosts.totalCount}
@ -71,6 +73,7 @@ describe('Load More Table Component', () => {
<MockedProvider>
<TestProviders store={store}>
<HostsTable
indexPattern={mockIndexPattern}
loading={false}
data={mockData.Hosts.edges}
totalCount={mockData.Hosts.totalCount}

View file

@ -4,24 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiIconTip } from '@elastic/eui';
import memoizeOne from 'memoize-one';
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import { ActionCreator } from 'typescript-fsa';
import { StaticIndexPattern } from 'ui/index_patterns';
import { Direction, HostsEdges, HostsFields, HostsSortField } from '../../../../graphql/types';
import { assertUnreachable } from '../../../../lib/helpers';
import {
Direction,
HostsEdges,
HostsFields,
HostsSortField,
HostItem,
} from '../../../../graphql/types';
import { assertUnreachable, ValueOf } from '../../../../lib/helpers';
import { hostsActions, hostsModel, hostsSelectors, State } from '../../../../store';
import { Criteria, ItemsPerRow, LoadMoreTable } from '../../../load_more_table';
import { CountBadge } from '../../index';
import {
Criteria,
ItemsPerRow,
LoadMoreTable,
Columns,
SortingBasicTable,
} from '../../../load_more_table';
import { getHostsColumns } from './columns';
import * as i18n from './translations';
import { TableTitle } from '../../table_title';
interface OwnProps {
data: HostsEdges[];
loading: boolean;
indexPattern: StaticIndexPattern;
hasNextPage: boolean;
nextCursor: string;
totalCount: number;
@ -68,54 +81,75 @@ const rowItems: ItemsPerRow[] = [
},
];
const Sup = styled.sup`
vertical-align: super;
padding: 0 5px;
`;
class HostsTableComponent extends React.PureComponent<HostsTableProps> {
private memoizedColumns: (
type: hostsModel.HostsType,
indexPattern: StaticIndexPattern
) => Array<Columns<ValueOf<HostItem>>>;
private memoizedTitle: (totalCount: number) => JSX.Element;
private memoizedSorting: (
trigger: string,
sortField: HostsFields,
direction: Direction
) => SortingBasicTable;
constructor(props: HostsTableProps) {
super(props);
this.memoizedColumns = memoizeOne(this.getMemoizeHostsColumns);
this.memoizedTitle = memoizeOne(this.getTitle);
this.memoizedSorting = memoizeOne(this.getSorting);
}
public render() {
const {
data,
direction,
hasNextPage,
indexPattern,
limit,
loading,
loadMore,
totalCount,
nextCursor,
updateLimitPagination,
sortField,
type,
} = this.props;
return (
<LoadMoreTable
columns={getHostsColumns(type)}
columns={this.memoizedColumns(type, indexPattern)}
loadingTitle={i18n.HOSTS}
loading={loading}
pageOfItems={data}
loadMore={() => loadMore(nextCursor)}
loadMore={this.wrappedLoadMore}
limit={limit}
hasNextPage={hasNextPage}
itemsPerRow={rowItems}
onChange={this.onChange}
updateLimitPagination={newLimit =>
updateLimitPagination({ limit: newLimit, hostsType: type })
}
sorting={{ field: getNodeField(sortField), direction }}
title={
<h3>
{i18n.HOSTS}
<Sup>
<EuiIconTip content={i18n.TOOLTIP} position="right" />
</Sup>
<CountBadge color="hollow">{totalCount}</CountBadge>
</h3>
}
updateLimitPagination={this.wrappedUpdateLimitPagination}
sorting={this.memoizedSorting(`${sortField}-${direction}`, sortField, direction)}
title={this.memoizedTitle(totalCount)}
/>
);
}
private getSorting = (
trigger: string,
sortField: HostsFields,
direction: Direction
): SortingBasicTable => ({ field: getNodeField(sortField), direction });
private getTitle = (totalCount: number): JSX.Element => (
<TableTitle title={i18n.HOSTS} infoTooltip={i18n.TOOLTIP} totalCount={totalCount} />
);
private wrappedUpdateLimitPagination = (newLimit: number) =>
this.props.updateLimitPagination({ limit: newLimit, hostsType: this.props.type });
private wrappedLoadMore = () => this.props.loadMore(this.props.nextCursor);
private getMemoizeHostsColumns = (
type: hostsModel.HostsType,
indexPattern: StaticIndexPattern
): Array<Columns<ValueOf<HostItem>>> => getHostsColumns(type, indexPattern);
private onChange = (criteria: Criteria) => {
if (criteria.sort != null) {
const sort: HostsSortField = {

View file

@ -65,6 +65,91 @@ exports[`Domains Table Component Rendering it renders the default Domains table
}
flowTarget="source"
hasNextPage={false}
indexPattern={
Object {
"fields": Array [
Object {
"aggregatable": true,
"name": "@timestamp",
"searchable": true,
"type": "date",
},
Object {
"aggregatable": true,
"name": "@version",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.ephemeral_id",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.hostname",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.id",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test1",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test2",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test3",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test4",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test5",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test6",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test7",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test8",
"searchable": true,
"type": "string",
},
],
"title": "filebeat-*,auditbeat-*,packetbeat-*",
}
}
ip="10.10.10.10"
loadMore={[MockFunction]}
loading={false}

View file

@ -9,6 +9,7 @@ import numeral from '@elastic/numeral';
import { getOr, isEmpty } from 'lodash/fp';
import moment from 'moment';
import React from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
import {
DomainsItem,
@ -32,6 +33,7 @@ import { AddToKql } from '../../add_to_kql';
import * as i18n from './translations';
export const getDomainsColumns = (
indexPattern: StaticIndexPattern,
ip: string,
flowDirection: FlowDirection,
flowTarget: FlowTarget,
@ -89,6 +91,7 @@ export const getDomainsColumns = (
: directions &&
directions.map((direction, index) => (
<AddToKql
indexPattern={indexPattern}
key={escapeDataProviderId(
`${tableId}-table-${flowTarget}-${flowDirection}-direction-${direction}`
)}

View file

@ -12,7 +12,7 @@ import { MockedProvider } from 'react-apollo/test-utils';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { FlowTarget } from '../../../../graphql/types';
import { mockGlobalState, TestProviders } from '../../../../mock';
import { mockIndexPattern, mockGlobalState, TestProviders } from '../../../../mock';
import { createStore, networkModel, State } from '../../../../store';
import { DomainsTable } from '.';
@ -34,6 +34,7 @@ describe('Domains Table Component', () => {
const wrapper = shallow(
<ReduxStoreProvider store={store}>
<DomainsTable
indexPattern={mockIndexPattern}
ip={ip}
totalCount={1}
loading={false}
@ -57,6 +58,7 @@ describe('Domains Table Component', () => {
<MockedProvider>
<TestProviders store={store}>
<DomainsTable
indexPattern={mockIndexPattern}
ip={ip}
totalCount={1}
loading={false}

View file

@ -9,6 +9,7 @@ import { isEqual } from 'lodash/fp';
import React from 'react';
import { connect } from 'react-redux';
import { ActionCreator } from 'redux';
import { StaticIndexPattern } from 'ui/index_patterns';
import {
Direction,
@ -31,6 +32,7 @@ interface OwnProps {
flowTarget: FlowTarget;
loading: boolean;
hasNextPage: boolean;
indexPattern: StaticIndexPattern;
ip: string;
nextCursor: string;
totalCount: number;
@ -88,6 +90,7 @@ class DomainsTableComponent extends React.PureComponent<DomainsTableProps> {
data,
domainsSortField,
hasNextPage,
indexPattern,
ip,
limit,
loading,
@ -102,7 +105,14 @@ class DomainsTableComponent extends React.PureComponent<DomainsTableProps> {
return (
<LoadMoreTable
columns={getDomainsColumns(ip, flowDirection, flowTarget, type, DomainsTableId)}
columns={getDomainsColumns(
indexPattern,
ip,
flowDirection,
flowTarget,
type,
DomainsTableId
)}
loadingTitle={i18n.DOMAINS}
loading={loading}
pageOfItems={data}

View file

@ -52,6 +52,91 @@ exports[`NetworkTopNFlow Table Component rendering it renders the default Networ
]
}
hasNextPage={true}
indexPattern={
Object {
"fields": Array [
Object {
"aggregatable": true,
"name": "@timestamp",
"searchable": true,
"type": "date",
},
Object {
"aggregatable": true,
"name": "@version",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.ephemeral_id",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.hostname",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.id",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test1",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test2",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test3",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test4",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test5",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test6",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test7",
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"name": "agent.test8",
"searchable": true,
"type": "string",
},
],
"title": "filebeat-*,auditbeat-*,packetbeat-*",
}
}
loadMore={[MockFunction]}
loading={false}
nextCursor="10"

View file

@ -9,6 +9,7 @@ import numeral from '@elastic/numeral';
import { get, isEmpty } from 'lodash/fp';
import React from 'react';
import styled from 'styled-components';
import { StaticIndexPattern } from 'ui/index_patterns';
import {
FlowDirection,
@ -31,6 +32,7 @@ import { AddToKql } from '../../add_to_kql';
import * as i18n from './translations';
export const getNetworkTopNFlowColumns = (
indexPattern: StaticIndexPattern,
flowDirection: FlowDirection,
flowTarget: FlowTarget,
type: networkModel.NetworkType,
@ -151,6 +153,7 @@ export const getNetworkTopNFlowColumns = (
: directions &&
directions.map((direction, index) => (
<AddToKql
indexPattern={indexPattern}
key={escapeDataProviderId(
`${tableId}-table-${flowTarget}-${flowDirection}-direction-${direction}`
)}

View file

@ -12,7 +12,7 @@ import { MockedProvider } from 'react-apollo/test-utils';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { FlowDirection } from '../../../../graphql/types';
import { mockGlobalState, TestProviders } from '../../../../mock';
import { mockIndexPattern, mockGlobalState, TestProviders } from '../../../../mock';
import { createStore, networkModel, State } from '../../../../store';
import { NetworkTopNFlowTable, NetworkTopNFlowTableId } from '.';
@ -33,6 +33,7 @@ describe('NetworkTopNFlow Table Component', () => {
const wrapper = shallow(
<ReduxStoreProvider store={store}>
<NetworkTopNFlowTable
indexPattern={mockIndexPattern}
loading={false}
data={mockData.NetworkTopNFlow.edges}
totalCount={mockData.NetworkTopNFlow.totalCount}
@ -58,6 +59,7 @@ describe('NetworkTopNFlow Table Component', () => {
<MockedProvider>
<TestProviders store={store}>
<NetworkTopNFlowTable
indexPattern={mockIndexPattern}
loading={false}
data={mockData.NetworkTopNFlow.edges}
totalCount={mockData.NetworkTopNFlow.totalCount}
@ -91,6 +93,7 @@ describe('NetworkTopNFlow Table Component', () => {
<MockedProvider>
<TestProviders store={store}>
<NetworkTopNFlowTable
indexPattern={mockIndexPattern}
loading={false}
data={mockData.NetworkTopNFlow.edges}
totalCount={mockData.NetworkTopNFlow.totalCount}
@ -131,6 +134,7 @@ describe('NetworkTopNFlow Table Component', () => {
<MockedProvider>
<TestProviders store={store}>
<NetworkTopNFlowTable
indexPattern={mockIndexPattern}
loading={false}
data={mockData.NetworkTopNFlow.edges}
totalCount={mockData.NetworkTopNFlow.totalCount}

View file

@ -10,6 +10,7 @@ import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import { ActionCreator } from 'typescript-fsa';
import { StaticIndexPattern } from 'ui/index_patterns';
import {
FlowDirection,
@ -29,6 +30,7 @@ import * as i18n from './translations';
interface OwnProps {
data: NetworkTopNFlowEdges[];
indexPattern: StaticIndexPattern;
loading: boolean;
hasNextPage: boolean;
nextCursor: string;
@ -92,6 +94,7 @@ class NetworkTopNFlowTableComponent extends React.PureComponent<NetworkTopNFlowT
const {
data,
hasNextPage,
indexPattern,
limit,
loading,
loadMore,
@ -112,7 +115,13 @@ class NetworkTopNFlowTableComponent extends React.PureComponent<NetworkTopNFlowT
return (
<LoadMoreTable
columns={getNetworkTopNFlowColumns(flowDirection, flowTarget, type, NetworkTopNFlowTableId)}
columns={getNetworkTopNFlowColumns(
indexPattern,
flowDirection,
flowTarget,
type,
NetworkTopNFlowTableId
)}
loadingTitle={i18n.TOP_TALKERS}
loading={loading}
pageOfItems={data}

View file

@ -0,0 +1,36 @@
/*
* 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 { EuiIconTip } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
import { CountBadge } from '../index';
interface OwnProps {
title: string;
infoTooltip: string;
totalCount: number;
}
export const TableTitle = pure<OwnProps>(({ title, infoTooltip, totalCount }) => (
<h3>
{title}
{!isEmpty(infoTooltip) && (
<Sup>
<EuiIconTip content={infoTooltip} position="right" />
</Sup>
)}
<CountBadge color="hollow">{totalCount}</CountBadge>
</h3>
));
const Sup = styled.sup`
vertical-align: super;
padding: 0 5px;
`;

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getOr } from 'lodash/fp';
import { get, getOr } from 'lodash/fp';
import memoizeOne from 'memoize-one';
import React from 'react';
import { Query } from 'react-apollo';
import { connect } from 'react-redux';
@ -56,6 +57,16 @@ class HostsComponentQuery extends QueryTemplate<
GetHostsTableQuery.Query,
GetHostsTableQuery.Variables
> {
private memoizedHosts: (
variables: string,
data: GetHostsTableQuery.Source | undefined
) => HostsEdges[];
constructor(props: HostsProps) {
super(props);
this.memoizedHosts = memoizeOne(this.getHosts);
}
public render() {
const {
id = 'hostsQuery',
@ -68,32 +79,32 @@ class HostsComponentQuery extends QueryTemplate<
sourceId,
sortField,
} = this.props;
const variables: GetHostsTableQuery.Variables = {
sourceId,
timerange: {
interval: '12h',
from: startDate,
to: endDate,
},
sort: {
direction,
field: sortField,
},
pagination: {
limit,
cursor: null,
tiebreaker: null,
},
filterQuery: createFilter(filterQuery),
};
return (
<Query<GetHostsTableQuery.Query, GetHostsTableQuery.Variables>
query={HostsTableQuery}
fetchPolicy="cache-first"
notifyOnNetworkStatusChange
variables={{
sourceId,
timerange: {
interval: '12h',
from: startDate,
to: endDate,
},
sort: {
direction,
field: sortField,
},
pagination: {
limit,
cursor: null,
tiebreaker: null,
},
filterQuery: createFilter(filterQuery),
}}
variables={variables}
>
{({ data, loading, fetchMore, refetch }) => {
const hosts = getOr([], 'source.Hosts.edges', data);
this.setFetchMore(fetchMore);
this.setFetchMoreOptions((newCursor: string) => ({
variables: {
@ -123,7 +134,7 @@ class HostsComponentQuery extends QueryTemplate<
refetch,
loading,
totalCount: getOr(0, 'source.Hosts.totalCount', data),
hosts,
hosts: this.memoizedHosts(JSON.stringify(variables), get('source', data)),
startDate,
endDate,
pageInfo: getOr({}, 'source.Hosts.pageInfo', data),
@ -133,6 +144,11 @@ class HostsComponentQuery extends QueryTemplate<
</Query>
);
}
private getHosts = (
variables: string,
source: GetHostsTableQuery.Source | undefined
): HostsEdges[] => getOr([], 'Hosts.edges', source);
}
const makeMapStateToProps = () => {

View file

@ -33,7 +33,7 @@ interface KueryAutocompletionLifecycleState {
suggestions: AutocompleteSuggestion[];
}
export class KueryAutocompletion extends React.Component<
export class KueryAutocompletion extends React.PureComponent<
KueryAutocompletionLifecycleProps,
KueryAutocompletionLifecycleState
> {

View file

@ -5,7 +5,7 @@
*/
import { isUndefined } from 'lodash';
import { get, memoize, pick, set } from 'lodash/fp';
import { get, memoize, pick, set, difference } from 'lodash/fp';
import React from 'react';
import { Query } from 'react-apollo';
import { StaticIndexPattern, StaticIndexPatternField } from 'ui/index_patterns';
@ -43,8 +43,11 @@ interface WithSourceProps {
}
export class WithSource extends React.PureComponent<WithSourceProps> {
private memoizedIndexFields: (title: string, fields: IndexField[]) => StaticIndexPatternField[];
private memoizedBrowserFields: (title: string, fields: IndexField[]) => BrowserFields;
private memoizedIndexFields: (
indexTypes: string[],
fields: IndexField[]
) => StaticIndexPatternField[];
private memoizedBrowserFields: (indexTypes: string[], fields: IndexField[]) => BrowserFields;
constructor(props: WithSourceProps) {
super(props);
@ -59,7 +62,7 @@ export class WithSource extends React.PureComponent<WithSourceProps> {
query={sourceQuery}
fetchPolicy="cache-first"
notifyOnNetworkStatusChange
variables={{ sourceId, indexTypes }}
variables={{ sourceId, indexTypes: [IndexType.ANY] }}
>
{({ data }) => {
const logAlias = get('source.configuration.logAlias', data);
@ -89,20 +92,21 @@ export class WithSource extends React.PureComponent<WithSourceProps> {
indexPatternTitle = [...indexPatternTitle, winlogbeatAlias];
}
}
const indexTypesLowerCase = indexTypes.map(i => i.toLocaleLowerCase());
return children({
auditbeatIndicesExist: get('source.status.auditbeatIndicesExist', data),
filebeatIndicesExist: get('source.status.filebeatIndicesExist', data),
winlogbeatIndicesExist: get('source.status.winlogbeatIndicesExist', data),
browserFields: get('source.status.indexFields', data)
? this.memoizedBrowserFields(
indexPatternTitle.join(),
indexTypesLowerCase,
get('source.status.indexFields', data)
)
: {},
indexPattern: {
fields: get('source.status.indexFields', data)
? this.memoizedIndexFields(
indexPatternTitle.join(),
indexTypesLowerCase,
get('source.status.indexFields', data)
)
: [],
@ -114,15 +118,30 @@ export class WithSource extends React.PureComponent<WithSourceProps> {
);
}
private getIndexFields = (title: string, fields: IndexField[]): StaticIndexPatternField[] =>
fields.map(field => pick(['name', 'searchable', 'type', 'aggregatable'], field));
private getIndexFields = (
indexTypes: string[],
fields: IndexField[]
): StaticIndexPatternField[] =>
fields
.filter(
item =>
indexTypes.includes('any') ||
difference(item.indexes, indexTypes).length !== item.indexes.length
)
.map(field => pick(['name', 'searchable', 'type', 'aggregatable'], field));
private getBrowserFields = (title: string, fields: IndexField[]): BrowserFields =>
fields.reduce(
(accumulator: BrowserFields, field: IndexField) =>
set([field.category, 'fields', field.name], field, accumulator),
{} as BrowserFields
);
private getBrowserFields = (indexTypes: string[], fields: IndexField[]): BrowserFields =>
fields
.filter(
item =>
indexTypes.includes('any') ||
difference(item.indexes, indexTypes).length !== item.indexes.length
)
.reduce(
(accumulator: BrowserFields, field: IndexField) =>
set([field.category, 'fields', field.name], field, accumulator),
{} as BrowserFields
);
}
export const indicesExistOrDataTemporarilyUnavailable = (indicesExist: boolean | undefined) =>

View file

@ -2184,44 +2184,6 @@ export namespace GetIpOverviewQuery {
};
}
export namespace GetKpiEventsQuery {
export type Variables = {
sourceId: string;
timerange: TimerangeInput;
filterQuery?: string | null;
pagination: PaginationInput;
sortField: SortField;
};
export type Query = {
__typename?: 'Query';
source: Source;
};
export type Source = {
__typename?: 'Source';
id: string;
Events: Events;
};
export type Events = {
__typename?: 'EventsData';
kpiEventType?: KpiEventType[] | null;
};
export type KpiEventType = {
__typename?: 'KpiItem';
value?: string | null;
count: number;
};
}
export namespace GetKpiNetworkQuery {
export type Variables = {
sourceId: string;
@ -3440,27 +3402,3 @@ export namespace GetUncommonProcessesQuery {
value: string;
};
}
export namespace WhoAmIQuery {
export type Variables = {
sourceId: string;
};
export type Query = {
__typename?: 'Query';
source: Source;
};
export type Source = {
__typename?: 'Source';
whoAmI?: WhoAmI | null;
};
export type WhoAmI = {
__typename?: 'SayMyName';
appName: string;
};
}

View file

@ -61,6 +61,7 @@ const HostsComponent = pure<HostsComponentProps>(({ filterQuery }) => (
{({ hosts, totalCount, loading, pageInfo, loadMore, id, refetch }) => (
<HostsTableManage
id={id}
indexPattern={indexPattern}
refetch={refetch}
setQuery={setQuery}
loading={loading}

View file

@ -102,6 +102,7 @@ const IPDetailsComponent = pure<IPDetailsComponentProps>(
{({ id, domains, totalCount, pageInfo, loading, loadMore, refetch }) => (
<DomainsTableManage
data={domains}
indexPattern={indexPattern}
id={id}
flowTarget={flowTarget}
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}

View file

@ -83,6 +83,7 @@ const NetworkComponent = pure<NetworkComponentProps>(({ filterQuery }) => (
}) => (
<NetworkTopNFlowTableManage
data={networkTopNFlow}
indexPattern={indexPattern}
id={id}
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
loading={loading}