mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Example of Integration of timeline with EventQuery (#25452)
* add two widget for hosts pages * fix spelling mistake * add pr review * Add _id to the EventsQuery * integrate query with timeline + test * PR review-1 * add explanation commented text * remove unuseful tslint garbage
This commit is contained in:
parent
b671b9df3f
commit
c2425229e8
19 changed files with 303 additions and 208 deletions
|
@ -414,6 +414,14 @@
|
|||
"name": "EventItem",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "_id",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "destination",
|
||||
"description": "",
|
||||
|
|
|
@ -66,6 +66,7 @@ export interface KpiItem {
|
|||
}
|
||||
|
||||
export interface EventItem {
|
||||
_id?: string | null;
|
||||
destination?: DestinationEcsFields | null;
|
||||
event?: EventEcsFields | null;
|
||||
geo?: GeoEcsFields | null;
|
||||
|
@ -304,6 +305,7 @@ export namespace KpiItemResolvers {
|
|||
|
||||
export namespace EventItemResolvers {
|
||||
export interface Resolvers<Context = any> {
|
||||
_id?: IdResolver<string | null, any, Context>;
|
||||
destination?: DestinationResolver<DestinationEcsFields | null, any, Context>;
|
||||
event?: EventResolver<EventEcsFields | null, any, Context>;
|
||||
geo?: GeoResolver<GeoEcsFields | null, any, Context>;
|
||||
|
@ -313,6 +315,11 @@ export namespace EventItemResolvers {
|
|||
timestamp?: TimestampResolver<string | null, any, Context>;
|
||||
}
|
||||
|
||||
export type IdResolver<R = string | null, Parent = any, Context = any> = Resolver<
|
||||
R,
|
||||
Parent,
|
||||
Context
|
||||
>;
|
||||
export type DestinationResolver<
|
||||
R = DestinationEcsFields | null,
|
||||
Parent = any,
|
||||
|
@ -549,6 +556,7 @@ export namespace GetEventsQuery {
|
|||
|
||||
export type Events = {
|
||||
__typename?: 'EventItem';
|
||||
_id?: string | null;
|
||||
timestamp?: string | null;
|
||||
event?: Event | null;
|
||||
host?: Host | null;
|
||||
|
|
|
@ -9,7 +9,6 @@ import { noop } from 'lodash/fp';
|
|||
import * as React from 'react';
|
||||
import { Body } from '.';
|
||||
import { mockECSData } from '../../../pages/mock/mock_ecs';
|
||||
import { mockDataProviders } from '../data_providers/mock/mock_data_providers';
|
||||
import { headers } from './column_headers/headers';
|
||||
import { columnRenderers, rowRenderers } from './renderers';
|
||||
import { Sort } from './sort';
|
||||
|
@ -27,7 +26,6 @@ describe('ColumnHeaders', () => {
|
|||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockECSData}
|
||||
dataProviders={mockDataProviders}
|
||||
sort={sort}
|
||||
onColumnSorted={noop}
|
||||
onDataProviderRemoved={noop}
|
||||
|
|
|
@ -10,7 +10,6 @@ import { pure } from 'recompose';
|
|||
import styled from 'styled-components';
|
||||
|
||||
import { Range } from '../body/column_headers/range_picker/ranges';
|
||||
import { DataProvider } from '../data_providers/data_provider';
|
||||
import { ECS } from '../ecs';
|
||||
import { OnColumnSorted, OnDataProviderRemoved, OnFilterChange, OnRangeSelected } from '../events';
|
||||
import { ColumnHeaders } from './column_headers';
|
||||
|
@ -22,7 +21,6 @@ interface Props {
|
|||
columnHeaders: ColumnHeader[];
|
||||
columnRenderers: ColumnRenderer[];
|
||||
data: ECS[];
|
||||
dataProviders: DataProvider[];
|
||||
height?: string;
|
||||
onColumnSorted: OnColumnSorted;
|
||||
onDataProviderRemoved: OnDataProviderRemoved;
|
||||
|
|
|
@ -31,7 +31,7 @@ export const columnRenderers: ColumnRenderer[] = [
|
|||
|
||||
export const getSuricataCVEFromSignature = (signature: string): string | null => {
|
||||
const regex = /CVE-[0-9]*-[0-9]*/;
|
||||
const found = signature.match(regex);
|
||||
const found = (signature && signature.match(regex)) || false;
|
||||
if (found) {
|
||||
return found[0];
|
||||
} else {
|
||||
|
|
|
@ -43,13 +43,13 @@ export const plainColumnRenderer: ColumnRenderer = {
|
|||
case 'timestamp':
|
||||
return <React.Fragment>{moment(data.timestamp).format('YYYY-MM-DD')}</React.Fragment>;
|
||||
case 'severity':
|
||||
return <React.Fragment>{data.event.severity}</React.Fragment>;
|
||||
return <React.Fragment>{getOr('--', 'event.severity', data)}</React.Fragment>;
|
||||
case 'category':
|
||||
return <React.Fragment>{data.event.category}</React.Fragment>;
|
||||
return <React.Fragment>{getOr('--', 'event.category', data)}</React.Fragment>;
|
||||
case 'type':
|
||||
return <React.Fragment>{data.event.type}</React.Fragment>;
|
||||
return <React.Fragment>{getOr('--', 'event.type', data)}</React.Fragment>;
|
||||
case 'source':
|
||||
return <React.Fragment>{data.source.ip}</React.Fragment>;
|
||||
return <React.Fragment>{getOr('--', 'event.source.ip', data)}</React.Fragment>;
|
||||
case 'user':
|
||||
return <React.Fragment>{getOr('--', 'user.name', data)}</React.Fragment>;
|
||||
case 'event':
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash/fp';
|
||||
import { get, getOr } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { ColumnRenderer, getSuricataCVEFromSignature } from '.';
|
||||
import { ECS } from '../../ecs';
|
||||
|
@ -12,8 +12,18 @@ import { ECS } from '../../ecs';
|
|||
const columnsOverriden = ['event'];
|
||||
|
||||
export const suricataColumnRenderer: ColumnRenderer = {
|
||||
isInstance: (columnName: string, ecs: ECS) =>
|
||||
columnsOverriden.includes(columnName) && ecs.event.module.toLowerCase() === 'suricata',
|
||||
isInstance: (columnName: string, ecs: ECS) => {
|
||||
if (
|
||||
columnsOverriden.includes(columnName) &&
|
||||
ecs &&
|
||||
ecs.event &&
|
||||
ecs.event.module &&
|
||||
ecs.event.module.toLowerCase() === 'suricata'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
renderColumn: (columnName: string, data: ECS) => {
|
||||
switch (columnName) {
|
||||
|
@ -23,7 +33,7 @@ export const suricataColumnRenderer: ColumnRenderer = {
|
|||
if (cve != null) {
|
||||
return <React.Fragment>{cve}</React.Fragment>;
|
||||
} else {
|
||||
return <React.Fragment>{data.event.id}</React.Fragment>;
|
||||
return <React.Fragment>{getOr('--', 'event.id', data)}</React.Fragment>;
|
||||
}
|
||||
default:
|
||||
// unknown column name
|
||||
|
|
|
@ -61,22 +61,25 @@ const SuricataRow = styled.div`
|
|||
`;
|
||||
|
||||
export const suricataRowRenderer: RowRenderer = {
|
||||
isInstance: (ecs: ECS) => (ecs.event.module.toLowerCase() === 'suricata' ? true : false),
|
||||
isInstance: (ecs: ECS) => {
|
||||
if (ecs && ecs.event && ecs.event.module && ecs.event.module.toLowerCase() === 'suricata') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
renderRow: (data: ECS, children: React.ReactNode) => {
|
||||
const signature = get('suricata.eve.alert.signature', data) as string;
|
||||
if (signature != null) {
|
||||
return (
|
||||
<SuricataRow>
|
||||
{children}
|
||||
return (
|
||||
<SuricataRow>
|
||||
{children}
|
||||
{signature != null ? (
|
||||
<SuricataSignature>
|
||||
<EuiButton fill size="s" href={createLinkWithSignature(signature)}>
|
||||
{signature}
|
||||
</EuiButton>
|
||||
</SuricataSignature>
|
||||
</SuricataRow>
|
||||
);
|
||||
} else {
|
||||
return <span />;
|
||||
}
|
||||
) : null}
|
||||
</SuricataRow>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -18,9 +18,11 @@ export interface DataProvider {
|
|||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Returns a query that, when executed, returns the data for this provider
|
||||
* Returns a Component query that, when executed, returns the data for this provider
|
||||
*/
|
||||
getQuery: () => string;
|
||||
componentQuery: React.ReactNode;
|
||||
componentQueryProps: {};
|
||||
componentResultParam: string;
|
||||
/**
|
||||
* When `true`, boolean logic is applied to the data provider to negate it.
|
||||
* default: `false`
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
import { EuiBadge, EuiText } from '@elastic/eui';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { EventsQuery } from '../../../../containers/events';
|
||||
import { DataProvider } from '../data_provider';
|
||||
|
||||
interface NameToEventCount<TValue> {
|
||||
|
@ -49,9 +51,16 @@ const Text = styled(EuiText)`
|
|||
export const mockDataProviders: DataProvider[] = Object.keys(mockSourceNameToEventCount).map(
|
||||
name => ({
|
||||
enabled: true,
|
||||
getQuery: () => `query-for-provider-id-${name}`,
|
||||
id: `id-${name}`,
|
||||
name,
|
||||
componentResultParam: 'events',
|
||||
componentQuery: EventsQuery,
|
||||
componentQueryProps: {
|
||||
sourceId: 'default',
|
||||
startDate: 1521830963132,
|
||||
endDate: 1521862432253,
|
||||
filterQuery: '',
|
||||
},
|
||||
negated: false,
|
||||
render: () => (
|
||||
<div data-test-subj="mockDataProvider">
|
||||
|
|
|
@ -8,37 +8,37 @@ export interface ECS {
|
|||
_id: string;
|
||||
timestamp: string;
|
||||
host: {
|
||||
hostname: string;
|
||||
ip: string;
|
||||
hostname?: string;
|
||||
ip?: string;
|
||||
};
|
||||
event: {
|
||||
id: string;
|
||||
category: string;
|
||||
type: string;
|
||||
module: string;
|
||||
severity: number;
|
||||
id?: string;
|
||||
category?: string;
|
||||
type?: string;
|
||||
module?: string;
|
||||
severity?: number;
|
||||
};
|
||||
suricata?: {
|
||||
eve: {
|
||||
flow_id: number;
|
||||
proto: string;
|
||||
alert: {
|
||||
eve?: {
|
||||
flow_id?: number;
|
||||
proto?: string;
|
||||
alert?: {
|
||||
signature: string;
|
||||
signature_id: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
source: {
|
||||
ip: string;
|
||||
port: number;
|
||||
ip?: string;
|
||||
port?: number;
|
||||
};
|
||||
destination: {
|
||||
ip: string;
|
||||
port: number;
|
||||
ip?: string;
|
||||
port?: number;
|
||||
};
|
||||
geo: {
|
||||
region_name: string;
|
||||
country_iso_code: string;
|
||||
region_name?: string;
|
||||
country_iso_code?: string;
|
||||
};
|
||||
user?: {
|
||||
id: string;
|
||||
|
|
|
@ -30,7 +30,6 @@ export interface OwnProps {
|
|||
|
||||
interface StateReduxProps {
|
||||
dataProviders?: DataProvider[];
|
||||
data?: ECS[];
|
||||
range?: Range;
|
||||
sort?: Sort;
|
||||
}
|
||||
|
@ -74,7 +73,6 @@ class StatefulTimelineComponent extends React.PureComponent<Props> {
|
|||
|
||||
public render() {
|
||||
const {
|
||||
data,
|
||||
dataProviders,
|
||||
headers,
|
||||
id,
|
||||
|
@ -103,7 +101,6 @@ class StatefulTimelineComponent extends React.PureComponent<Props> {
|
|||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
dataProviders={dataProviders!}
|
||||
data={data!}
|
||||
onColumnSorted={onColumnSorted}
|
||||
onDataProviderRemoved={onDataProviderRemoved}
|
||||
onFilterChange={noop} // TODO: this is the callback for column filters, which is out scope for this phase of delivery
|
||||
|
@ -119,9 +116,9 @@ class StatefulTimelineComponent extends React.PureComponent<Props> {
|
|||
|
||||
const mapStateToProps = (state: State, { id }: OwnProps) => {
|
||||
const timeline = timelineByIdSelector(state)[id];
|
||||
const { dataProviders, data, sort } = timeline || timelineDefaults;
|
||||
const { dataProviders, sort } = timeline || timelineDefaults;
|
||||
|
||||
return defaultTo({ id, dataProviders, data, sort }, timeline);
|
||||
return defaultTo({ id, dataProviders, sort }, timeline);
|
||||
};
|
||||
|
||||
export const StatefulTimeline = connect(
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
import { mount } from 'enzyme';
|
||||
import { noop, pick } from 'lodash/fp';
|
||||
import * as React from 'react';
|
||||
import { MockedProvider } from 'react-apollo/test-utils';
|
||||
|
||||
import { eventsQuery } from '../../containers/events/events.gql_query';
|
||||
import { mockECSData } from '../../pages/mock/mock_ecs';
|
||||
import { ColumnHeaderType } from './body/column_headers/column_header';
|
||||
import { headers } from './body/column_headers/headers';
|
||||
|
@ -22,23 +24,35 @@ describe('Timeline', () => {
|
|||
sortDirection: 'descending',
|
||||
};
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: { query: eventsQuery },
|
||||
result: {
|
||||
data: {
|
||||
events: mockECSData,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('rendering', () => {
|
||||
test('it renders the timeline header', () => {
|
||||
const wrapper = mount(
|
||||
<Timeline
|
||||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockECSData}
|
||||
dataProviders={mockDataProviders}
|
||||
onColumnSorted={noop}
|
||||
onDataProviderRemoved={noop}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
/>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<Timeline
|
||||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
dataProviders={mockDataProviders}
|
||||
onColumnSorted={noop}
|
||||
onDataProviderRemoved={noop}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
/>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="timelineHeader"]').exists()).toEqual(true);
|
||||
|
@ -46,20 +60,21 @@ describe('Timeline', () => {
|
|||
|
||||
test('it renders the timeline body', () => {
|
||||
const wrapper = mount(
|
||||
<Timeline
|
||||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockECSData}
|
||||
dataProviders={mockDataProviders}
|
||||
onColumnSorted={noop}
|
||||
onDataProviderRemoved={noop}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
/>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<Timeline
|
||||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
dataProviders={mockDataProviders}
|
||||
onColumnSorted={noop}
|
||||
onDataProviderRemoved={noop}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
/>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="body"]').exists()).toEqual(true);
|
||||
|
@ -72,20 +87,21 @@ describe('Timeline', () => {
|
|||
const mockOnColumnSorted = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<Timeline
|
||||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockECSData}
|
||||
dataProviders={mockDataProviders}
|
||||
onColumnSorted={mockOnColumnSorted}
|
||||
onDataProviderRemoved={noop}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
/>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<Timeline
|
||||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
dataProviders={mockDataProviders}
|
||||
onColumnSorted={mockOnColumnSorted}
|
||||
onDataProviderRemoved={noop}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
/>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
wrapper
|
||||
|
@ -105,20 +121,21 @@ describe('Timeline', () => {
|
|||
const mockOnDataProviderRemoved = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<Timeline
|
||||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockECSData}
|
||||
dataProviders={mockDataProviders}
|
||||
onColumnSorted={noop}
|
||||
onDataProviderRemoved={mockOnDataProviderRemoved}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
/>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<Timeline
|
||||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
dataProviders={mockDataProviders}
|
||||
onColumnSorted={noop}
|
||||
onDataProviderRemoved={mockOnDataProviderRemoved}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
/>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
wrapper
|
||||
|
@ -152,20 +169,21 @@ describe('Timeline', () => {
|
|||
}));
|
||||
|
||||
const wrapper = mount(
|
||||
<Timeline
|
||||
columnHeaders={allColumnsHaveTextFilters}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockECSData}
|
||||
dataProviders={mockDataProviders}
|
||||
onColumnSorted={noop}
|
||||
onDataProviderRemoved={noop}
|
||||
onFilterChange={mockOnFilterChange}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
/>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<Timeline
|
||||
columnHeaders={allColumnsHaveTextFilters}
|
||||
columnRenderers={columnRenderers}
|
||||
dataProviders={mockDataProviders}
|
||||
onColumnSorted={noop}
|
||||
onDataProviderRemoved={noop}
|
||||
onFilterChange={mockOnFilterChange}
|
||||
onRangeSelected={noop}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
/>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
wrapper
|
||||
|
@ -179,35 +197,5 @@ describe('Timeline', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('onRangeSelected', () => {
|
||||
test('it invokes the onRangeSelected callback when a new range is selected', () => {
|
||||
const newSelection = '1 Day';
|
||||
const mockOnRangeSelected = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<Timeline
|
||||
columnHeaders={headers}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockECSData}
|
||||
dataProviders={mockDataProviders}
|
||||
onColumnSorted={noop}
|
||||
onDataProviderRemoved={noop}
|
||||
onFilterChange={noop}
|
||||
onRangeSelected={mockOnRangeSelected}
|
||||
range={'1 Day'}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={1000}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="rangePicker"] select')
|
||||
.simulate('change', { target: { value: newSelection } });
|
||||
|
||||
expect(mockOnRangeSelected).toBeCalledWith(newSelection);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 { getOr } from 'lodash/fp';
|
||||
import * as React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import styled from 'styled-components';
|
||||
|
@ -22,7 +22,6 @@ import { TimelineHeader } from './header/timeline_header';
|
|||
interface Props {
|
||||
columnHeaders: ColumnHeader[];
|
||||
columnRenderers: ColumnRenderer[];
|
||||
data: ECS[];
|
||||
dataProviders: DataProvider[];
|
||||
height?: string;
|
||||
onColumnSorted: OnColumnSorted;
|
||||
|
@ -53,7 +52,6 @@ export const Timeline = pure<Props>(
|
|||
columnHeaders,
|
||||
columnRenderers,
|
||||
dataProviders,
|
||||
data,
|
||||
height = defaultHeight,
|
||||
onColumnSorted,
|
||||
onDataProviderRemoved,
|
||||
|
@ -70,20 +68,30 @@ export const Timeline = pure<Props>(
|
|||
onDataProviderRemoved={onDataProviderRemoved}
|
||||
width={width}
|
||||
/>
|
||||
<Body
|
||||
columnHeaders={columnHeaders}
|
||||
columnRenderers={columnRenderers}
|
||||
dataProviders={dataProviders}
|
||||
data={data}
|
||||
onColumnSorted={onColumnSorted}
|
||||
onDataProviderRemoved={onDataProviderRemoved}
|
||||
onFilterChange={onFilterChange}
|
||||
onRangeSelected={onRangeSelected}
|
||||
range={range}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={width}
|
||||
/>
|
||||
{dataProviders.map(provider => {
|
||||
const QueryComponent = provider.componentQuery as React.ComponentClass;
|
||||
const queryProps = provider.componentQueryProps;
|
||||
const resParm = provider.componentResultParam;
|
||||
return (
|
||||
<QueryComponent {...queryProps} key={provider.id}>
|
||||
{(resData: {}) => (
|
||||
<Body
|
||||
columnHeaders={columnHeaders}
|
||||
columnRenderers={columnRenderers}
|
||||
data={getOr([], resParm, resData) as ECS[]}
|
||||
onColumnSorted={onColumnSorted}
|
||||
onDataProviderRemoved={onDataProviderRemoved}
|
||||
onFilterChange={onFilterChange}
|
||||
onRangeSelected={onRangeSelected}
|
||||
range={range}
|
||||
rowRenderers={rowRenderers}
|
||||
sort={sort}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
</QueryComponent>
|
||||
);
|
||||
})}
|
||||
</TimelineDiv>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -11,6 +11,7 @@ export const eventsQuery = gql`
|
|||
source(id: $sourceId) {
|
||||
getEvents(timerange: $timerange, filterQuery: $filterQuery) {
|
||||
events {
|
||||
_id
|
||||
timestamp
|
||||
event {
|
||||
type
|
||||
|
|
|
@ -19,8 +19,8 @@ interface EventsArgs {
|
|||
loading: boolean;
|
||||
}
|
||||
|
||||
interface EventsProps {
|
||||
children: (args: EventsArgs) => React.ReactNode;
|
||||
export interface EventsProps {
|
||||
children?: (args: EventsArgs) => React.ReactNode;
|
||||
sourceId: string;
|
||||
startDate: number;
|
||||
endDate: number;
|
||||
|
@ -44,7 +44,7 @@ export const EventsQuery = pure<EventsProps>(
|
|||
}}
|
||||
>
|
||||
{({ data, loading }) =>
|
||||
children({
|
||||
children!({
|
||||
loading,
|
||||
events: getOr([], 'source.getEvents.events', data),
|
||||
kpiEventType: getOr([], 'source.getEvents.kpiEventType', data),
|
||||
|
|
|
@ -3,66 +3,121 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiBadge, EuiText } from '@elastic/eui';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { pure } from 'recompose';
|
||||
import { Dispatch } from 'redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { EventItem, KpiItem } from '../../../common/graphql/types';
|
||||
import { BasicTable, Columns } from '../../components/basic_table';
|
||||
import { BasicTable } from '../../components/basic_table';
|
||||
import { HorizontalBarChart, HorizontalBarChartData } from '../../components/horizontal_bar_chart';
|
||||
import { Pane1FlexContent } from '../../components/page';
|
||||
import { Placeholders, VisualizationPlaceholder } from '../../components/visualization_placeholder';
|
||||
import { EventsQuery } from '../../containers/events';
|
||||
import { timelineActions } from '../../store';
|
||||
|
||||
export const Hosts = pure(() => (
|
||||
<EventsQuery sourceId="default" startDate={1541044800000} endDate={1543640399999}>
|
||||
{({ events, kpiEventType, loading }) => (
|
||||
<Pane1FlexContent data-test-subj="pane1FlexContent">
|
||||
<VisualizationPlaceholder>
|
||||
<HorizontalBarChart
|
||||
loading={loading}
|
||||
title="KPI event types"
|
||||
width={490}
|
||||
height={279}
|
||||
barChartdata={
|
||||
kpiEventType.map((i: KpiItem) => ({
|
||||
x: i.count,
|
||||
y: i.value,
|
||||
})) as HorizontalBarChartData[]
|
||||
}
|
||||
/>
|
||||
</VisualizationPlaceholder>
|
||||
<VisualizationPlaceholder>
|
||||
<BasicTable
|
||||
columns={eventsColumns}
|
||||
loading={loading}
|
||||
pageOfItems={events}
|
||||
sortField="host.hostname"
|
||||
title="Events"
|
||||
/>
|
||||
</VisualizationPlaceholder>
|
||||
<Placeholders timelineId="pane2-timeline" count={8} myRoute="Hosts" />
|
||||
</Pane1FlexContent>
|
||||
)}
|
||||
</EventsQuery>
|
||||
));
|
||||
// start/end date to show good alert in the timeline
|
||||
const startDate = 1521830963132;
|
||||
const endDate = 1521862432253;
|
||||
|
||||
const eventsColumns: Columns[] = [
|
||||
// start/end date to show good data in the KPI event type
|
||||
// const startDate = 1541044800000;
|
||||
// const endDate = 1543640399999;
|
||||
|
||||
interface Props {
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
export const Hosts = connect()(
|
||||
pure<Props>(({ dispatch }) => (
|
||||
<EventsQuery sourceId="default" startDate={startDate} endDate={endDate}>
|
||||
{({ events, kpiEventType, loading }) => (
|
||||
<Pane1FlexContent data-test-subj="pane1FlexContent">
|
||||
<VisualizationPlaceholder>
|
||||
<HorizontalBarChart
|
||||
loading={loading}
|
||||
title="KPI event types"
|
||||
width={490}
|
||||
height={279}
|
||||
barChartdata={
|
||||
kpiEventType.map((i: KpiItem) => ({
|
||||
x: i.count,
|
||||
y: i.value,
|
||||
})) as HorizontalBarChartData[]
|
||||
}
|
||||
/>
|
||||
</VisualizationPlaceholder>
|
||||
<VisualizationPlaceholder>
|
||||
<BasicTable
|
||||
columns={getEventsColumns(dispatch)}
|
||||
loading={loading}
|
||||
pageOfItems={events}
|
||||
sortField="host.hostname"
|
||||
title="Events"
|
||||
/>
|
||||
</VisualizationPlaceholder>
|
||||
<Placeholders timelineId="pane2-timeline" count={8} myRoute="Hosts" />
|
||||
</Pane1FlexContent>
|
||||
)}
|
||||
</EventsQuery>
|
||||
))
|
||||
);
|
||||
|
||||
const getEventsColumns = (dispatch: Dispatch) => [
|
||||
{
|
||||
name: 'Host name',
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
hideForMobile: false,
|
||||
render: (item: EventItem) => (
|
||||
<React.Fragment>{getOr('--', 'host.hostname', item)}</React.Fragment>
|
||||
),
|
||||
render: (item: EventItem) => {
|
||||
const hostName = getOr('--', 'host.hostname', item);
|
||||
return (
|
||||
<ProviderContainer
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
timelineActions.addProvider({
|
||||
id: 'pane2-timeline',
|
||||
provider: {
|
||||
enabled: true,
|
||||
id: `id-${hostName}`,
|
||||
name,
|
||||
negated: false,
|
||||
componentResultParam: 'events',
|
||||
componentQuery: EventsQuery,
|
||||
componentQueryProps: {
|
||||
sourceId: 'default',
|
||||
startDate,
|
||||
endDate,
|
||||
filterQuery: `{"bool":{"should":[{"match":{"host.name":"${hostName}"}}],"minimum_should_match":1}}`,
|
||||
},
|
||||
render: () => (
|
||||
<div data-test-subj="mockDataProvider">
|
||||
<EuiBadge color="primary">n/a</EuiBadge>
|
||||
<Text> {hostName} </Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{hostName}
|
||||
</ProviderContainer>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Event type',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
hideForMobile: true,
|
||||
render: (item: EventItem) => <React.Fragment>{getOr('--', 'event.type', item)}</React.Fragment>,
|
||||
render: (item: EventItem) => (
|
||||
<ProviderContainer>{getOr('--', 'event.type', item)}</ProviderContainer>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Source',
|
||||
|
@ -94,3 +149,13 @@ const eventsColumns: Columns[] = [
|
|||
),
|
||||
},
|
||||
];
|
||||
|
||||
const ProviderContainer = styled.div`
|
||||
user-select: none;
|
||||
cursor: grab;
|
||||
`;
|
||||
|
||||
const Text = styled(EuiText)`
|
||||
display: inline;
|
||||
padding-left: 5px;
|
||||
`;
|
||||
|
|
|
@ -65,6 +65,7 @@ export const eventsSchema = gql`
|
|||
}
|
||||
|
||||
type EventItem {
|
||||
_id: String
|
||||
destination: DestinationEcsFields
|
||||
event: EventEcsFields
|
||||
geo: GeoEcsFields
|
||||
|
|
|
@ -140,10 +140,8 @@ export class ElasticsearchEventsAdapter implements EventsAdapter {
|
|||
count: item.doc_count,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const hits = response.hits.hits;
|
||||
const events = hits.map(formatEventsData(Fields)) as [EventItem];
|
||||
|
||||
return {
|
||||
events,
|
||||
kpiEventType,
|
||||
|
@ -154,6 +152,7 @@ export class ElasticsearchEventsAdapter implements EventsAdapter {
|
|||
const formatEventsData = (fields: string[]) => (hit: EventData) =>
|
||||
fields.reduce(
|
||||
(flattenedFields, fieldName) => {
|
||||
flattenedFields._id = get('_id', hit);
|
||||
if (EventFieldsMap.hasOwnProperty(fieldName)) {
|
||||
const esField = Object.getOwnPropertyDescriptor(EventFieldsMap, fieldName);
|
||||
return has(esField && esField.value, hit._source)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue