mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* add events histogram * move away from auto date histogram aggregation * fallback to auto-date-histogram if interval is not available * styling for histogram * add add narrowDownTimerange event * isolate matrixOverTimeHistogram component * split events series * add lable for missing group * fix styled component syntax + change prop name * add unit test * fix props passed to styled component * add integration test * pass filterQuery prop to events tab on Host details page * reduce bucket number for bar chart * update types * display barchart only when data is available * change function style * fix types * add unit test * add spacer * pass updateDateRange down to host details components * update snapshot
This commit is contained in:
parent
4ec27fb691
commit
c878857d36
34 changed files with 2027 additions and 406 deletions
|
@ -7,15 +7,16 @@
|
|||
import { ShallowWrapper, shallow } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
|
||||
import { AreaChartBaseComponent, AreaChartWithCustomPrompt } from './areachart';
|
||||
import { ChartConfigsData, ChartHolder } from './common';
|
||||
import { AreaChartBaseComponent, AreaChartWithCustomPrompt, AreaChart } from './areachart';
|
||||
import { ChartHolder, ChartSeriesData } from './common';
|
||||
import { ScaleType, AreaSeries, Axis } from '@elastic/charts';
|
||||
|
||||
jest.mock('@elastic/charts');
|
||||
|
||||
const customHeight = '100px';
|
||||
const customWidth = '120px';
|
||||
describe('AreaChartBaseComponent', () => {
|
||||
let shallowWrapper: ShallowWrapper;
|
||||
const mockAreaChartData: ChartConfigsData[] = [
|
||||
const mockAreaChartData: ChartSeriesData[] = [
|
||||
{
|
||||
key: 'uniqueSourceIpsHistogram',
|
||||
value: [
|
||||
|
@ -39,7 +40,11 @@ describe('AreaChartBaseComponent', () => {
|
|||
describe('render', () => {
|
||||
beforeAll(() => {
|
||||
shallowWrapper = shallow(
|
||||
<AreaChartBaseComponent height={100} width={120} data={mockAreaChartData} />
|
||||
<AreaChartBaseComponent
|
||||
height={customHeight}
|
||||
width={customWidth}
|
||||
data={mockAreaChartData}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -65,8 +70,8 @@ describe('AreaChartBaseComponent', () => {
|
|||
beforeAll(() => {
|
||||
shallowWrapper = shallow(
|
||||
<AreaChartBaseComponent
|
||||
height={100}
|
||||
width={120}
|
||||
height={customHeight}
|
||||
width={customWidth}
|
||||
data={mockAreaChartData}
|
||||
configs={configs}
|
||||
/>
|
||||
|
@ -118,7 +123,11 @@ describe('AreaChartBaseComponent', () => {
|
|||
describe('render with default configs if no customized configs given', () => {
|
||||
beforeAll(() => {
|
||||
shallowWrapper = shallow(
|
||||
<AreaChartBaseComponent height={100} width={120} data={mockAreaChartData} />
|
||||
<AreaChartBaseComponent
|
||||
height={customHeight}
|
||||
width={customWidth}
|
||||
data={mockAreaChartData}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -220,9 +229,11 @@ describe('AreaChartWithCustomPrompt', () => {
|
|||
],
|
||||
],
|
||||
],
|
||||
])('renders areachart', (data: ChartConfigsData[] | [] | null | undefined) => {
|
||||
])('renders areachart', (data: ChartSeriesData[] | [] | null | undefined) => {
|
||||
beforeAll(() => {
|
||||
shallowWrapper = shallow(<AreaChartWithCustomPrompt height={100} width={120} data={data} />);
|
||||
shallowWrapper = shallow(
|
||||
<AreaChartWithCustomPrompt height={customHeight} width={customWidth} data={data} />
|
||||
);
|
||||
});
|
||||
|
||||
it('render AreaChartBaseComponent', () => {
|
||||
|
@ -310,9 +321,11 @@ describe('AreaChartWithCustomPrompt', () => {
|
|||
},
|
||||
],
|
||||
],
|
||||
])('renders prompt', (data: ChartConfigsData[] | [] | null | undefined) => {
|
||||
])('renders prompt', (data: ChartSeriesData[] | [] | null | undefined) => {
|
||||
beforeAll(() => {
|
||||
shallowWrapper = shallow(<AreaChartWithCustomPrompt height={100} width={120} data={data} />);
|
||||
shallowWrapper = shallow(
|
||||
<AreaChartWithCustomPrompt height={customHeight} width={customWidth} data={data} />
|
||||
);
|
||||
});
|
||||
|
||||
it('render Chart Holder', () => {
|
||||
|
@ -321,3 +334,36 @@ describe('AreaChartWithCustomPrompt', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AreaChart', () => {
|
||||
let shallowWrapper: ShallowWrapper;
|
||||
const mockConfig = {
|
||||
series: {
|
||||
xScaleType: ScaleType.Time,
|
||||
yScaleType: ScaleType.Linear,
|
||||
stackAccessors: ['g'],
|
||||
},
|
||||
axis: {
|
||||
xTickFormatter: jest.fn(),
|
||||
yTickFormatter: jest.fn(),
|
||||
tickSize: 8,
|
||||
},
|
||||
customHeight: 324,
|
||||
};
|
||||
|
||||
it('should render if data exist', () => {
|
||||
const mockData = [
|
||||
{ key: 'uniqueSourceIps', value: [{ y: 100, x: 100, g: 'group' }], color: '#DB1374' },
|
||||
];
|
||||
shallowWrapper = shallow(<AreaChart configs={mockConfig} areaChart={mockData} />);
|
||||
expect(shallowWrapper.find('AutoSizer')).toHaveLength(1);
|
||||
expect(shallowWrapper.find('ChartHolder')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should render a chartHolder if no data given', () => {
|
||||
const mockData = [{ key: 'uniqueSourceIps', value: [], color: '#DB1374' }];
|
||||
shallowWrapper = shallow(<AreaChart configs={mockConfig} areaChart={mockData} />);
|
||||
expect(shallowWrapper.find('AutoSizer')).toHaveLength(0);
|
||||
expect(shallowWrapper.find('ChartHolder')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,14 +19,15 @@ import {
|
|||
} from '@elastic/charts';
|
||||
import { getOr, get } from 'lodash/fp';
|
||||
import {
|
||||
ChartConfigsData,
|
||||
ChartSeriesData,
|
||||
ChartHolder,
|
||||
getSeriesStyle,
|
||||
WrappedByAutoSizer,
|
||||
getTheme,
|
||||
ChartSeriesConfigs,
|
||||
browserTimezone,
|
||||
chartDefaultSettings,
|
||||
getChartHeight,
|
||||
getChartWidth,
|
||||
} from './common';
|
||||
import { AutoSizer } from '../auto_sizer';
|
||||
|
||||
|
@ -52,9 +53,9 @@ const getSeriesLineStyle = (): RecursivePartial<AreaSeriesStyle> => {
|
|||
|
||||
// https://ela.st/multi-areaseries
|
||||
export const AreaChartBaseComponent = React.memo<{
|
||||
data: ChartConfigsData[];
|
||||
width: number | null | undefined;
|
||||
height: number | null | undefined;
|
||||
data: ChartSeriesData[];
|
||||
width: string | null | undefined;
|
||||
height: string | null | undefined;
|
||||
configs?: ChartSeriesConfigs | undefined;
|
||||
}>(({ data, ...chartConfigs }) => {
|
||||
const xTickFormatter = get('configs.axis.xTickFormatter', chartConfigs);
|
||||
|
@ -68,7 +69,7 @@ export const AreaChartBaseComponent = React.memo<{
|
|||
return chartConfigs.width && chartConfigs.height ? (
|
||||
<div style={{ height: chartConfigs.height, width: chartConfigs.width, position: 'relative' }}>
|
||||
<Chart>
|
||||
<Settings {...settings} theme={getTheme()} />
|
||||
<Settings {...settings} />
|
||||
{data.map(series => {
|
||||
const seriesKey = series.key;
|
||||
const seriesSpecId = getSpecId(seriesKey);
|
||||
|
@ -89,23 +90,15 @@ export const AreaChartBaseComponent = React.memo<{
|
|||
) : null;
|
||||
})}
|
||||
|
||||
{xTickFormatter ? (
|
||||
<Axis
|
||||
id={xAxisId}
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks={false}
|
||||
tickFormat={xTickFormatter}
|
||||
tickSize={0}
|
||||
/>
|
||||
) : (
|
||||
<Axis id={xAxisId} position={Position.Bottom} showOverlappingTicks={false} tickSize={0} />
|
||||
)}
|
||||
<Axis
|
||||
id={xAxisId}
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks={false}
|
||||
tickFormat={xTickFormatter}
|
||||
tickSize={0}
|
||||
/>
|
||||
|
||||
{yTickFormatter ? (
|
||||
<Axis id={yAxisId} position={Position.Left} tickSize={0} tickFormat={yTickFormatter} />
|
||||
) : (
|
||||
<Axis id={yAxisId} position={Position.Left} tickSize={0} />
|
||||
)}
|
||||
<Axis id={yAxisId} position={Position.Left} tickSize={0} tickFormat={yTickFormatter} />
|
||||
</Chart>
|
||||
</div>
|
||||
) : null;
|
||||
|
@ -114,9 +107,9 @@ export const AreaChartBaseComponent = React.memo<{
|
|||
AreaChartBaseComponent.displayName = 'AreaChartBaseComponent';
|
||||
|
||||
export const AreaChartWithCustomPrompt = React.memo<{
|
||||
data: ChartConfigsData[] | null | undefined;
|
||||
height: number | null | undefined;
|
||||
width: number | null | undefined;
|
||||
data: ChartSeriesData[] | null | undefined;
|
||||
height: string | null | undefined;
|
||||
width: string | null | undefined;
|
||||
configs?: ChartSeriesConfigs | undefined;
|
||||
}>(({ data, height, width, configs }) => {
|
||||
return data != null &&
|
||||
|
@ -129,28 +122,35 @@ export const AreaChartWithCustomPrompt = React.memo<{
|
|||
) ? (
|
||||
<AreaChartBaseComponent height={height} width={width} data={data} configs={configs} />
|
||||
) : (
|
||||
<ChartHolder />
|
||||
<ChartHolder height={height} width={width} />
|
||||
);
|
||||
});
|
||||
|
||||
AreaChartWithCustomPrompt.displayName = 'AreaChartWithCustomPrompt';
|
||||
|
||||
export const AreaChart = React.memo<{
|
||||
areaChart: ChartConfigsData[] | null | undefined;
|
||||
areaChart: ChartSeriesData[] | null | undefined;
|
||||
configs?: ChartSeriesConfigs | undefined;
|
||||
}>(({ areaChart, configs }) => (
|
||||
<AutoSizer detectAnyWindowResize={false} content>
|
||||
{({ measureRef, content: { height, width } }) => (
|
||||
<WrappedByAutoSizer innerRef={measureRef}>
|
||||
<AreaChartWithCustomPrompt
|
||||
data={areaChart}
|
||||
height={height}
|
||||
width={width}
|
||||
configs={configs}
|
||||
/>
|
||||
</WrappedByAutoSizer>
|
||||
)}
|
||||
</AutoSizer>
|
||||
));
|
||||
}>(({ areaChart, configs }) => {
|
||||
const customHeight = get('customHeight', configs);
|
||||
const customWidth = get('customWidth', configs);
|
||||
|
||||
return get(`0.value.length`, areaChart) ? (
|
||||
<AutoSizer detectAnyWindowResize={false} content>
|
||||
{({ measureRef, content: { height, width } }) => (
|
||||
<WrappedByAutoSizer innerRef={measureRef} height={getChartHeight(customHeight, height)}>
|
||||
<AreaChartWithCustomPrompt
|
||||
data={areaChart}
|
||||
height={getChartHeight(customHeight, height)}
|
||||
width={getChartWidth(customWidth, width)}
|
||||
configs={configs}
|
||||
/>
|
||||
</WrappedByAutoSizer>
|
||||
)}
|
||||
</AutoSizer>
|
||||
) : (
|
||||
<ChartHolder height={getChartHeight(customHeight)} width={getChartWidth(customWidth)} />
|
||||
);
|
||||
});
|
||||
|
||||
AreaChart.displayName = 'AreaChart';
|
||||
|
|
|
@ -7,15 +7,16 @@
|
|||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
|
||||
import { BarChartBaseComponent, BarChartWithCustomPrompt } from './barchart';
|
||||
import { ChartConfigsData, ChartHolder } from './common';
|
||||
import { BarChartBaseComponent, BarChartWithCustomPrompt, BarChart } from './barchart';
|
||||
import { ChartSeriesData, ChartHolder } from './common';
|
||||
import { BarSeries, ScaleType, Axis } from '@elastic/charts';
|
||||
|
||||
jest.mock('@elastic/charts');
|
||||
|
||||
const customHeight = '100px';
|
||||
const customWidth = '120px';
|
||||
describe('BarChartBaseComponent', () => {
|
||||
let shallowWrapper: ShallowWrapper;
|
||||
const mockBarChartData: ChartConfigsData[] = [
|
||||
const mockBarChartData: ChartSeriesData[] = [
|
||||
{
|
||||
key: 'uniqueSourceIps',
|
||||
value: [{ y: 1714, x: 'uniqueSourceIps', g: 'uniqueSourceIps' }],
|
||||
|
@ -31,7 +32,7 @@ describe('BarChartBaseComponent', () => {
|
|||
describe('render', () => {
|
||||
beforeAll(() => {
|
||||
shallowWrapper = shallow(
|
||||
<BarChartBaseComponent height={100} width={120} data={mockBarChartData} />
|
||||
<BarChartBaseComponent height={customHeight} width={customWidth} data={mockBarChartData} />
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -54,7 +55,12 @@ describe('BarChartBaseComponent', () => {
|
|||
|
||||
beforeAll(() => {
|
||||
shallowWrapper = shallow(
|
||||
<BarChartBaseComponent height={100} width={120} data={mockBarChartData} configs={configs} />
|
||||
<BarChartBaseComponent
|
||||
height={customHeight}
|
||||
width={customWidth}
|
||||
data={mockBarChartData}
|
||||
configs={configs}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -103,7 +109,7 @@ describe('BarChartBaseComponent', () => {
|
|||
describe('render with default configs if no customized configs given', () => {
|
||||
beforeAll(() => {
|
||||
shallowWrapper = shallow(
|
||||
<BarChartBaseComponent height={100} width={120} data={mockBarChartData} />
|
||||
<BarChartBaseComponent height={customHeight} width={customWidth} data={mockBarChartData} />
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -198,7 +204,11 @@ describe.each([
|
|||
describe('renders barchart', () => {
|
||||
beforeAll(() => {
|
||||
shallowWrapper = shallow(
|
||||
<BarChartWithCustomPrompt height={100} width={120} data={mockBarChartData} />
|
||||
<BarChartWithCustomPrompt
|
||||
height={customHeight}
|
||||
width={customWidth}
|
||||
data={mockBarChartData}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -271,10 +281,12 @@ describe.each([
|
|||
},
|
||||
],
|
||||
],
|
||||
])('renders prompt', (data: ChartConfigsData[] | [] | null | undefined) => {
|
||||
])('renders prompt', (data: ChartSeriesData[] | [] | null | undefined) => {
|
||||
let shallowWrapper: ShallowWrapper;
|
||||
beforeAll(() => {
|
||||
shallowWrapper = shallow(<BarChartWithCustomPrompt height={100} width={120} data={data} />);
|
||||
shallowWrapper = shallow(
|
||||
<BarChartWithCustomPrompt height={customHeight} width={customWidth} data={data} />
|
||||
);
|
||||
});
|
||||
|
||||
it('render Chart Holder', () => {
|
||||
|
@ -282,3 +294,36 @@ describe.each([
|
|||
expect(shallowWrapper.find(ChartHolder)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BarChart', () => {
|
||||
let shallowWrapper: ShallowWrapper;
|
||||
const mockConfig = {
|
||||
series: {
|
||||
xScaleType: ScaleType.Time,
|
||||
yScaleType: ScaleType.Linear,
|
||||
stackAccessors: ['g'],
|
||||
},
|
||||
axis: {
|
||||
xTickFormatter: jest.fn(),
|
||||
yTickFormatter: jest.fn(),
|
||||
tickSize: 8,
|
||||
},
|
||||
customHeight: 324,
|
||||
};
|
||||
|
||||
it('should render if data exist', () => {
|
||||
const mockData = [
|
||||
{ key: 'uniqueSourceIps', value: [{ y: 100, x: 100, g: 'group' }], color: '#DB1374' },
|
||||
];
|
||||
shallowWrapper = shallow(<BarChart configs={mockConfig} barChart={mockData} />);
|
||||
expect(shallowWrapper.find('AutoSizer')).toHaveLength(1);
|
||||
expect(shallowWrapper.find('ChartHolder')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should render a chartHolder if no data given', () => {
|
||||
const mockData = [{ key: 'uniqueSourceIps', value: [], color: '#DB1374' }];
|
||||
shallowWrapper = shallow(<BarChart configs={mockConfig} barChart={mockData} />);
|
||||
expect(shallowWrapper.find('AutoSizer')).toHaveLength(0);
|
||||
expect(shallowWrapper.find('ChartHolder')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,27 +18,29 @@ import {
|
|||
} from '@elastic/charts';
|
||||
import { getOr, get } from 'lodash/fp';
|
||||
import {
|
||||
ChartConfigsData,
|
||||
ChartSeriesData,
|
||||
WrappedByAutoSizer,
|
||||
ChartHolder,
|
||||
SeriesType,
|
||||
getSeriesStyle,
|
||||
getTheme,
|
||||
ChartSeriesConfigs,
|
||||
browserTimezone,
|
||||
chartDefaultSettings,
|
||||
getChartHeight,
|
||||
getChartWidth,
|
||||
} from './common';
|
||||
import { AutoSizer } from '../auto_sizer';
|
||||
|
||||
// Bar chart rotation: https://ela.st/chart-rotations
|
||||
export const BarChartBaseComponent = React.memo<{
|
||||
data: ChartConfigsData[];
|
||||
width: number | null | undefined;
|
||||
height: number | null | undefined;
|
||||
data: ChartSeriesData[];
|
||||
width: string | null | undefined;
|
||||
height: string | null | undefined;
|
||||
configs?: ChartSeriesConfigs | undefined;
|
||||
}>(({ data, ...chartConfigs }) => {
|
||||
const xTickFormatter = get('configs.axis.xTickFormatter', chartConfigs);
|
||||
const yTickFormatter = get('configs.axis.yTickFormatter', chartConfigs);
|
||||
const tickSize = getOr(0, 'configs.axis.tickSize', chartConfigs);
|
||||
const xAxisId = getAxisId(`stat-items-barchart-${data[0].key}-x`);
|
||||
const yAxisId = getAxisId(`stat-items-barchart-${data[0].key}-y`);
|
||||
const settings = {
|
||||
|
@ -47,7 +49,7 @@ export const BarChartBaseComponent = React.memo<{
|
|||
};
|
||||
return chartConfigs.width && chartConfigs.height ? (
|
||||
<Chart>
|
||||
<Settings {...settings} theme={getTheme()} />
|
||||
<Settings {...settings} />
|
||||
{data.map(series => {
|
||||
const barSeriesKey = series.key;
|
||||
const barSeriesSpecId = getSpecId(barSeriesKey);
|
||||
|
@ -64,29 +66,21 @@ export const BarChartBaseComponent = React.memo<{
|
|||
timeZone={browserTimezone}
|
||||
splitSeriesAccessors={['g']}
|
||||
data={series.value!}
|
||||
stackAccessors={['y']}
|
||||
stackAccessors={get('configs.series.stackAccessors', chartConfigs)}
|
||||
customSeriesColors={getSeriesStyle(barSeriesKey, series.color, seriesType)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{xTickFormatter ? (
|
||||
<Axis
|
||||
id={xAxisId}
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks={false}
|
||||
tickSize={0}
|
||||
tickFormat={xTickFormatter}
|
||||
/>
|
||||
) : (
|
||||
<Axis id={xAxisId} position={Position.Bottom} showOverlappingTicks={false} tickSize={0} />
|
||||
)}
|
||||
<Axis
|
||||
id={xAxisId}
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks={false}
|
||||
tickSize={tickSize}
|
||||
tickFormat={xTickFormatter}
|
||||
/>
|
||||
|
||||
{yTickFormatter ? (
|
||||
<Axis id={yAxisId} position={Position.Left} tickSize={0} tickFormat={yTickFormatter} />
|
||||
) : (
|
||||
<Axis id={yAxisId} position={Position.Left} tickSize={0} />
|
||||
)}
|
||||
<Axis id={yAxisId} position={Position.Left} tickSize={tickSize} tickFormat={yTickFormatter} />
|
||||
</Chart>
|
||||
) : null;
|
||||
});
|
||||
|
@ -94,36 +88,47 @@ export const BarChartBaseComponent = React.memo<{
|
|||
BarChartBaseComponent.displayName = 'BarChartBaseComponent';
|
||||
|
||||
export const BarChartWithCustomPrompt = React.memo<{
|
||||
data: ChartConfigsData[] | null | undefined;
|
||||
height: number | null | undefined;
|
||||
width: number | null | undefined;
|
||||
data: ChartSeriesData[] | null | undefined;
|
||||
height: string | null | undefined;
|
||||
width: string | null | undefined;
|
||||
configs?: ChartSeriesConfigs | undefined;
|
||||
}>(({ data, height, width, configs }) => {
|
||||
return data &&
|
||||
data.length &&
|
||||
data.some(
|
||||
({ value }) =>
|
||||
value != null && value.length > 0 && value.every(chart => chart.y != null && chart.y > 0)
|
||||
value != null && value.length > 0 && value.every(chart => chart.y != null && chart.y >= 0)
|
||||
) ? (
|
||||
<BarChartBaseComponent height={height} width={width} data={data} configs={configs} />
|
||||
) : (
|
||||
<ChartHolder />
|
||||
<ChartHolder height={height} width={width} />
|
||||
);
|
||||
});
|
||||
|
||||
BarChartWithCustomPrompt.displayName = 'BarChartWithCustomPrompt';
|
||||
|
||||
export const BarChart = React.memo<{
|
||||
barChart: ChartConfigsData[] | null | undefined;
|
||||
barChart: ChartSeriesData[] | null | undefined;
|
||||
configs?: ChartSeriesConfigs | undefined;
|
||||
}>(({ barChart, configs }) => (
|
||||
<AutoSizer detectAnyWindowResize={false} content>
|
||||
{({ measureRef, content: { height, width } }) => (
|
||||
<WrappedByAutoSizer innerRef={measureRef}>
|
||||
<BarChartWithCustomPrompt height={height} width={width} data={barChart} configs={configs} />
|
||||
</WrappedByAutoSizer>
|
||||
)}
|
||||
</AutoSizer>
|
||||
));
|
||||
}>(({ barChart, configs }) => {
|
||||
const customHeight = get('customHeight', configs);
|
||||
const customWidth = get('customWidth', configs);
|
||||
return get(`0.value.length`, barChart) ? (
|
||||
<AutoSizer detectAnyWindowResize={false} content>
|
||||
{({ measureRef, content: { height, width } }) => (
|
||||
<WrappedByAutoSizer innerRef={measureRef} height={getChartHeight(customHeight, height)}>
|
||||
<BarChartWithCustomPrompt
|
||||
height={getChartHeight(customHeight, height)}
|
||||
width={getChartWidth(customWidth, width)}
|
||||
data={barChart}
|
||||
configs={configs}
|
||||
/>
|
||||
</WrappedByAutoSizer>
|
||||
)}
|
||||
</AutoSizer>
|
||||
) : (
|
||||
<ChartHolder height={getChartHeight(customHeight)} width={getChartWidth(customWidth)} />
|
||||
);
|
||||
});
|
||||
|
||||
BarChart.displayName = 'BarChart';
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ChartHolder,
|
||||
getChartHeight,
|
||||
getChartWidth,
|
||||
WrappedByAutoSizer,
|
||||
defaultChartHeight,
|
||||
getSeriesStyle,
|
||||
SeriesType,
|
||||
getTheme,
|
||||
} from './common';
|
||||
import 'jest-styled-components';
|
||||
import { mergeWithDefaultTheme, LIGHT_THEME } from '@elastic/charts';
|
||||
|
||||
jest.mock('@elastic/charts', () => {
|
||||
return {
|
||||
getSpecId: jest.fn(() => {}),
|
||||
mergeWithDefaultTheme: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('ChartHolder', () => {
|
||||
let shallowWrapper: ShallowWrapper;
|
||||
|
||||
it('should render with default props', () => {
|
||||
const height = `100%`;
|
||||
const width = `100%`;
|
||||
shallowWrapper = shallow(<ChartHolder />);
|
||||
expect(shallowWrapper.props()).toMatchObject({
|
||||
height,
|
||||
width,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render with given props', () => {
|
||||
const height = `100px`;
|
||||
const width = `100px`;
|
||||
shallowWrapper = shallow(<ChartHolder height={height} width={width} />);
|
||||
expect(shallowWrapper.props()).toMatchObject({
|
||||
height,
|
||||
width,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('WrappedByAutoSizer', () => {
|
||||
it('should render correct default height', () => {
|
||||
const wrapper = shallow(<WrappedByAutoSizer />);
|
||||
expect(wrapper).toHaveStyleRule('height', defaultChartHeight);
|
||||
});
|
||||
|
||||
it('should render correct given height', () => {
|
||||
const wrapper = shallow(<WrappedByAutoSizer height="100px" />);
|
||||
expect(wrapper).toHaveStyleRule('height', '100px');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSeriesStyle', () => {
|
||||
it('should not create style mapping if color is not given', () => {
|
||||
const mockSeriesKey = 'mockSeriesKey';
|
||||
const color = '';
|
||||
const customSeriesColors = getSeriesStyle(mockSeriesKey, color, SeriesType.BAR);
|
||||
expect(customSeriesColors).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create correct style mapping for series of a chart', () => {
|
||||
const mockSeriesKey = 'mockSeriesKey';
|
||||
const color = 'red';
|
||||
const customSeriesColors = getSeriesStyle(mockSeriesKey, color, SeriesType.BAR);
|
||||
const expectedKey = { colorValues: [mockSeriesKey] };
|
||||
customSeriesColors!.forEach((value, key) => {
|
||||
expect(JSON.stringify(key)).toEqual(JSON.stringify(expectedKey));
|
||||
expect(value).toEqual(color);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTheme', () => {
|
||||
it('should merge custom theme with default theme', () => {
|
||||
const defaultTheme = {
|
||||
chartMargins: { bottom: 0, left: 0, right: 0, top: 4 },
|
||||
chartPaddings: { bottom: 0, left: 0, right: 0, top: 0 },
|
||||
scales: {
|
||||
barsPadding: 0.5,
|
||||
},
|
||||
};
|
||||
getTheme();
|
||||
expect((mergeWithDefaultTheme as jest.Mock).mock.calls[0][0]).toMatchObject(defaultTheme);
|
||||
expect((mergeWithDefaultTheme as jest.Mock).mock.calls[0][1]).toEqual(LIGHT_THEME);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChartHeight', () => {
|
||||
it('should render customHeight', () => {
|
||||
const height = getChartHeight(10, 100);
|
||||
expect(height).toEqual('10px');
|
||||
});
|
||||
|
||||
it('should render autoSizerHeight if customHeight is not given', () => {
|
||||
const height = getChartHeight(undefined, 100);
|
||||
expect(height).toEqual('100px');
|
||||
});
|
||||
|
||||
it('should render defaultChartHeight if no custom data is given', () => {
|
||||
const height = getChartHeight();
|
||||
expect(height).toEqual(defaultChartHeight);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChartWidth', () => {
|
||||
it('should render customWidth', () => {
|
||||
const height = getChartWidth(10, 100);
|
||||
expect(height).toEqual('10px');
|
||||
});
|
||||
|
||||
it('should render autoSizerHeight if customHeight is not given', () => {
|
||||
const height = getChartWidth(undefined, 100);
|
||||
expect(height).toEqual('100px');
|
||||
});
|
||||
|
||||
it('should render defaultChartHeight if no custom data is given', () => {
|
||||
const height = getChartWidth();
|
||||
expect(height).toEqual(defaultChartHeight);
|
||||
});
|
||||
});
|
|
@ -24,20 +24,27 @@ import { i18n } from '@kbn/i18n';
|
|||
import chrome from 'ui/chrome';
|
||||
import moment from 'moment-timezone';
|
||||
import { DEFAULT_DATE_FORMAT_TZ, DEFAULT_DARK_MODE } from '../../../common/constants';
|
||||
|
||||
const chartHeight = 74;
|
||||
export const defaultChartHeight = '100%';
|
||||
export const defaultChartWidth = '100%';
|
||||
const chartDefaultRotation: Rotation = 0;
|
||||
const chartDefaultRendering: Rendering = 'canvas';
|
||||
const FlexGroup = styled(EuiFlexGroup)`
|
||||
height: 100%;
|
||||
const FlexGroup = styled(EuiFlexGroup)<{ height?: string | null; width?: string | null }>`
|
||||
height: ${({ height }) => (height ? height : '100%')};
|
||||
width: ${({ width }) => (width ? width : '100%')};
|
||||
`;
|
||||
|
||||
FlexGroup.displayName = 'FlexGroup';
|
||||
|
||||
export type UpdateDateRange = (min: number, max: number) => void;
|
||||
|
||||
export const ChartHolder = () => (
|
||||
<FlexGroup justifyContent="center" alignItems="center">
|
||||
export const ChartHolder = ({
|
||||
height = '100%',
|
||||
width = '100%',
|
||||
}: {
|
||||
height?: string | null;
|
||||
width?: string | null;
|
||||
}) => (
|
||||
<FlexGroup justifyContent="center" alignItems="center" height={height} width={width}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" textAlign="center" color="subdued">
|
||||
{i18n.translate('xpack.siem.chart.dataNotAvailableTitle', {
|
||||
|
@ -48,15 +55,6 @@ export const ChartHolder = () => (
|
|||
</FlexGroup>
|
||||
);
|
||||
|
||||
export const chartDefaultSettings = {
|
||||
rotation: chartDefaultRotation,
|
||||
rendering: chartDefaultRendering,
|
||||
animatedData: false,
|
||||
showLegend: false,
|
||||
showLegendDisplayValue: false,
|
||||
debug: false,
|
||||
};
|
||||
|
||||
export interface ChartData {
|
||||
x: number | string | null;
|
||||
y: number | string | null;
|
||||
|
@ -65,6 +63,7 @@ export interface ChartData {
|
|||
}
|
||||
|
||||
export interface ChartSeriesConfigs {
|
||||
customHeight?: number;
|
||||
series?: {
|
||||
xScaleType?: ScaleType | undefined;
|
||||
yScaleType?: ScaleType | undefined;
|
||||
|
@ -76,16 +75,17 @@ export interface ChartSeriesConfigs {
|
|||
settings?: Partial<SettingSpecProps>;
|
||||
}
|
||||
|
||||
export interface ChartConfigsData {
|
||||
export interface ChartSeriesData {
|
||||
key: string;
|
||||
value: ChartData[] | [] | null;
|
||||
color?: string | undefined;
|
||||
areachartConfigs?: ChartSeriesConfigs | undefined;
|
||||
barchartConfigs?: ChartSeriesConfigs | undefined;
|
||||
}
|
||||
|
||||
export const WrappedByAutoSizer = styled.div`
|
||||
height: ${chartHeight}px;
|
||||
export const WrappedByAutoSizer = styled.div<{ height?: string }>`
|
||||
${style =>
|
||||
`
|
||||
height: ${style.height != null ? style.height : defaultChartHeight};
|
||||
`}
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
|
@ -144,5 +144,25 @@ export const getTheme = () => {
|
|||
return mergeWithDefaultTheme(theme, defaultTheme);
|
||||
};
|
||||
|
||||
export const chartDefaultSettings = {
|
||||
rotation: chartDefaultRotation,
|
||||
rendering: chartDefaultRendering,
|
||||
animatedData: false,
|
||||
showLegend: false,
|
||||
showLegendDisplayValue: false,
|
||||
debug: false,
|
||||
theme: getTheme(),
|
||||
};
|
||||
|
||||
const kibanaTimezone: string = chrome.getUiSettingsClient().get(DEFAULT_DATE_FORMAT_TZ);
|
||||
export const browserTimezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone;
|
||||
|
||||
export const getChartHeight = (customHeight?: number, autoSizerHeight?: number): string => {
|
||||
const height = customHeight || autoSizerHeight;
|
||||
return height ? `${height}px` : defaultChartHeight;
|
||||
};
|
||||
|
||||
export const getChartWidth = (customWidth?: number, autoSizerWidth?: number): string => {
|
||||
const height = customWidth || autoSizerWidth;
|
||||
return height ? `${height}px` : defaultChartWidth;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
|
||||
import { MatrixOverTimeHistogram } from '.';
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
return {
|
||||
EuiPanel: (children: JSX.Element) => <>{children}</>,
|
||||
EuiLoadingContent: () => <div className="euiLoadingContent"></div>,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../loader', () => {
|
||||
return {
|
||||
Loader: () => <div className="loader"></div>,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../lib/settings/use_kibana_ui_setting', () => {
|
||||
return { useKibanaUiSetting: () => [false] };
|
||||
});
|
||||
|
||||
jest.mock('../header_panel', () => {
|
||||
return {
|
||||
HeaderPanel: () => <div className="headerPanel"></div>,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../charts/barchart', () => {
|
||||
return {
|
||||
BarChart: () => <div className="barchart"></div>,
|
||||
};
|
||||
});
|
||||
|
||||
describe('Load More Events Table Component', () => {
|
||||
const mockMatrixOverTimeHistogramProps = {
|
||||
data: [],
|
||||
dataKey: 'mockDataKey',
|
||||
endDate: new Date('2019-07-18T20:00:00.000Z').valueOf(),
|
||||
id: 'mockId',
|
||||
loading: true,
|
||||
updateDateRange: () => {},
|
||||
startDate: new Date('2019-07-18T19:00: 00.000Z').valueOf(),
|
||||
subtitle: 'mockSubtitle',
|
||||
totalCount: -1,
|
||||
title: 'mockTitle',
|
||||
};
|
||||
describe('rendering', () => {
|
||||
test('it renders EuiLoadingContent on initialLoad', () => {
|
||||
const wrapper = shallow(<MatrixOverTimeHistogram {...mockMatrixOverTimeHistogramProps} />);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="initialLoadingPanelMatrixOverTime"]`)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders Loader while fetching data if visited before', () => {
|
||||
const mockProps = {
|
||||
...mockMatrixOverTimeHistogramProps,
|
||||
data: [{ x: new Date('2019-09-16T02:20:00.000Z').valueOf(), y: 3787, g: 'config_change' }],
|
||||
totalCount: 10,
|
||||
loading: true,
|
||||
};
|
||||
const wrapper = shallow(<MatrixOverTimeHistogram {...mockProps} />);
|
||||
expect(wrapper.find('.loader')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it renders BarChart if data available', () => {
|
||||
const mockProps = {
|
||||
...mockMatrixOverTimeHistogramProps,
|
||||
data: [{ x: new Date('2019-09-16T02:20:00.000Z').valueOf(), y: 3787, g: 'config_change' }],
|
||||
totalCount: 10,
|
||||
loading: false,
|
||||
};
|
||||
const wrapper = shallow(<MatrixOverTimeHistogram {...mockProps} />);
|
||||
|
||||
expect(wrapper.find(`.barchart`)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ScaleType, niceTimeFormatter, Position } from '@elastic/charts';
|
||||
|
||||
import { getOr, head, last } from 'lodash/fp';
|
||||
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
|
||||
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { EuiLoadingContent } from '@elastic/eui';
|
||||
import { BarChart } from '../charts/barchart';
|
||||
import { HeaderPanel } from '../header_panel';
|
||||
import { ChartSeriesData, UpdateDateRange } from '../charts/common';
|
||||
import { MatrixOverTimeHistogramData } from '../../graphql/types';
|
||||
import { DEFAULT_DARK_MODE } from '../../../common/constants';
|
||||
import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting';
|
||||
import { Loader } from '../loader';
|
||||
import { Panel } from '../panel';
|
||||
|
||||
export interface MatrixOverTimeBasicProps {
|
||||
id: string;
|
||||
data: MatrixOverTimeHistogramData[];
|
||||
loading: boolean;
|
||||
startDate: number;
|
||||
endDate: number;
|
||||
updateDateRange: UpdateDateRange;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface MatrixOverTimeProps extends MatrixOverTimeBasicProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
dataKey: string;
|
||||
}
|
||||
|
||||
const getBarchartConfigs = (from: number, to: number, onBrushEnd: UpdateDateRange) => ({
|
||||
series: {
|
||||
xScaleType: ScaleType.Time,
|
||||
yScaleType: ScaleType.Linear,
|
||||
stackAccessors: ['g'],
|
||||
},
|
||||
axis: {
|
||||
xTickFormatter: niceTimeFormatter([from, to]),
|
||||
yTickFormatter: (value: string | number): string => value.toLocaleString(),
|
||||
tickSize: 8,
|
||||
},
|
||||
settings: {
|
||||
legendPosition: Position.Bottom,
|
||||
onBrushEnd,
|
||||
showLegend: true,
|
||||
theme: {
|
||||
scales: {
|
||||
barsPadding: 0.05,
|
||||
},
|
||||
chartMargins: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
chartPaddings: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
customHeight: 324,
|
||||
});
|
||||
|
||||
export const MatrixOverTimeHistogram = ({
|
||||
id,
|
||||
loading,
|
||||
data,
|
||||
dataKey,
|
||||
endDate,
|
||||
updateDateRange,
|
||||
startDate,
|
||||
title,
|
||||
subtitle,
|
||||
totalCount,
|
||||
}: MatrixOverTimeProps) => {
|
||||
const bucketStartDate = getOr(startDate, 'x', head(data));
|
||||
const bucketEndDate = getOr(endDate, 'x', last(data));
|
||||
const barchartConfigs = getBarchartConfigs(bucketStartDate!, bucketEndDate!, updateDateRange);
|
||||
const [showInspect, setShowInspect] = useState(false);
|
||||
const [darkMode] = useKibanaUiSetting(DEFAULT_DARK_MODE);
|
||||
const [loadingInitial, setLoadingInitial] = useState(false);
|
||||
|
||||
const barChartData: ChartSeriesData[] = [
|
||||
{
|
||||
key: dataKey,
|
||||
value: data,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (totalCount >= 0 && loadingInitial) {
|
||||
setLoadingInitial(false);
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
return (
|
||||
<Panel
|
||||
data-test-subj={`${dataKey}Panel`}
|
||||
loading={loading}
|
||||
onMouseEnter={() => setShowInspect(true)}
|
||||
onMouseLeave={() => setShowInspect(false)}
|
||||
>
|
||||
<HeaderPanel
|
||||
id={id}
|
||||
title={title}
|
||||
showInspect={!loadingInitial && showInspect}
|
||||
subtitle={!loadingInitial && subtitle}
|
||||
/>
|
||||
|
||||
{loadingInitial ? (
|
||||
<EuiLoadingContent data-test-subj="initialLoadingPanelMatrixOverTime" lines={10} />
|
||||
) : (
|
||||
<>
|
||||
<BarChart barChart={barChartData} configs={barchartConfigs} />
|
||||
|
||||
{loading && (
|
||||
<Loader
|
||||
overlay
|
||||
overlayBackground={
|
||||
darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor
|
||||
}
|
||||
size="xl"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as i18n from './translation';
|
||||
import { MatrixOverTimeHistogram, MatrixOverTimeBasicProps } from '../../../matrix_over_time';
|
||||
|
||||
export const EventsOverTimeHistogram = (props: MatrixOverTimeBasicProps) => {
|
||||
const dataKey = 'eventsOverTime';
|
||||
const { totalCount } = props;
|
||||
const subtitle = `${i18n.SHOWING}: ${totalCount.toLocaleString()} ${i18n.UNIT(totalCount)}`;
|
||||
const { ...matrixOverTimeProps } = props;
|
||||
|
||||
return (
|
||||
<MatrixOverTimeHistogram
|
||||
title={i18n.EVENT_COUNT_FREQUENCY_BY_ACTION}
|
||||
subtitle={subtitle}
|
||||
dataKey={dataKey}
|
||||
{...matrixOverTimeProps}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const EVENT_COUNT_FREQUENCY_BY_ACTION = i18n.translate(
|
||||
'xpack.siem.eventsOverTime.eventCountFrequencyByActionTitle',
|
||||
{
|
||||
defaultMessage: 'Event count frequency by action',
|
||||
}
|
||||
);
|
||||
|
||||
export const LOADING_EVENTS_OVER_TIME = i18n.translate(
|
||||
'xpack.siem.eventsOverTime.loadingEventsOverTimeTitle',
|
||||
{
|
||||
defaultMessage: 'Loading events histogram',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOWING = i18n.translate('xpack.siem.eventsOverTime.showing', {
|
||||
defaultMessage: 'Showing',
|
||||
});
|
||||
|
||||
export const UNIT = (totalCount: number) =>
|
||||
i18n.translate('xpack.siem.eventsOverTime.unit', {
|
||||
values: { totalCount },
|
||||
defaultMessage: `{totalCount, plural, =1 {event} other {events}}`,
|
||||
});
|
|
@ -38,11 +38,11 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] =
|
|||
data-test-subj="stat-item"
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
data-test-subj="stat-item"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
data-test-subj="stat-item"
|
||||
>
|
||||
<EuiPanel
|
||||
|
@ -118,7 +118,7 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] =
|
|||
showInspect={false}
|
||||
>
|
||||
<div
|
||||
className="sc-EHOje wlqEL"
|
||||
className="sc-bxivhb fEneUz"
|
||||
data-test-subj="transparent-inspect-container"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
|
@ -188,10 +188,10 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] =
|
|||
key="stat-items-field-hosts"
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
|
@ -203,22 +203,22 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] =
|
|||
>
|
||||
<FlexItem>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<StatValue>
|
||||
<EuiTitle
|
||||
className="sc-gzVnrw dJnFxB"
|
||||
className="sc-EHOje koezUx"
|
||||
>
|
||||
<p
|
||||
className="euiTitle euiTitle--medium sc-gzVnrw dJnFxB"
|
||||
className="euiTitle euiTitle--medium sc-EHOje koezUx"
|
||||
data-test-subj="stat-title"
|
||||
>
|
||||
<EmptyWrapper>
|
||||
<span
|
||||
className="sc-htpNat bijuWJ"
|
||||
className="sc-bdVaJa cSuzWb"
|
||||
>
|
||||
—
|
||||
</span>
|
||||
|
@ -292,11 +292,11 @@ exports[`Stat Items Component disable charts it renders the default widget 2`] =
|
|||
data-test-subj="stat-item"
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
data-test-subj="stat-item"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
data-test-subj="stat-item"
|
||||
>
|
||||
<EuiPanel
|
||||
|
@ -372,7 +372,7 @@ exports[`Stat Items Component disable charts it renders the default widget 2`] =
|
|||
showInspect={false}
|
||||
>
|
||||
<div
|
||||
className="sc-EHOje wlqEL"
|
||||
className="sc-bxivhb fEneUz"
|
||||
data-test-subj="transparent-inspect-container"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
|
@ -442,10 +442,10 @@ exports[`Stat Items Component disable charts it renders the default widget 2`] =
|
|||
key="stat-items-field-hosts"
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
|
@ -458,22 +458,22 @@ exports[`Stat Items Component disable charts it renders the default widget 2`] =
|
|||
0
|
||||
<FlexItem>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<StatValue>
|
||||
<EuiTitle
|
||||
className="sc-gzVnrw dJnFxB"
|
||||
className="sc-EHOje koezUx"
|
||||
>
|
||||
<p
|
||||
className="euiTitle euiTitle--medium sc-gzVnrw dJnFxB"
|
||||
className="euiTitle euiTitle--medium sc-EHOje koezUx"
|
||||
data-test-subj="stat-title"
|
||||
>
|
||||
<EmptyWrapper>
|
||||
<span
|
||||
className="sc-htpNat bijuWJ"
|
||||
className="sc-bdVaJa cSuzWb"
|
||||
>
|
||||
—
|
||||
</span>
|
||||
|
@ -616,11 +616,11 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
data-test-subj="stat-item"
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
data-test-subj="stat-item"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
data-test-subj="stat-item"
|
||||
>
|
||||
<EuiPanel
|
||||
|
@ -696,7 +696,7 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
showInspect={false}
|
||||
>
|
||||
<div
|
||||
className="sc-EHOje dUUqHB"
|
||||
className="sc-bxivhb fGGQew"
|
||||
data-test-subj="transparent-inspect-container"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
|
@ -766,10 +766,10 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
key="stat-items-field-uniqueSourceIps"
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
|
@ -783,11 +783,11 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
grow={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<EuiIcon
|
||||
color="#DB1374"
|
||||
|
@ -826,17 +826,17 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
</FlexItem>
|
||||
<FlexItem>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<StatValue>
|
||||
<EuiTitle
|
||||
className="sc-gzVnrw dJnFxB"
|
||||
className="sc-EHOje koezUx"
|
||||
>
|
||||
<p
|
||||
className="euiTitle euiTitle--medium sc-gzVnrw dJnFxB"
|
||||
className="euiTitle euiTitle--medium sc-EHOje koezUx"
|
||||
data-test-subj="stat-title"
|
||||
>
|
||||
1,714
|
||||
|
@ -857,10 +857,10 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
key="stat-items-field-uniqueDestinationIps"
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
|
@ -874,11 +874,11 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
grow={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<EuiIcon
|
||||
color="#490092"
|
||||
|
@ -917,17 +917,17 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
</FlexItem>
|
||||
<FlexItem>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<StatValue>
|
||||
<EuiTitle
|
||||
className="sc-gzVnrw dJnFxB"
|
||||
className="sc-EHOje koezUx"
|
||||
>
|
||||
<p
|
||||
className="euiTitle euiTitle--medium sc-gzVnrw dJnFxB"
|
||||
className="euiTitle euiTitle--medium sc-EHOje koezUx"
|
||||
data-test-subj="stat-title"
|
||||
>
|
||||
2,359
|
||||
|
@ -957,10 +957,10 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
>
|
||||
<FlexItem>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<BarChart
|
||||
barChart={
|
||||
|
@ -992,6 +992,7 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
"axis": Object {
|
||||
"xTickFormatter": [Function],
|
||||
},
|
||||
"customHeight": 74,
|
||||
"series": Object {
|
||||
"xScaleType": "ordinal",
|
||||
"yScaleType": "linear",
|
||||
|
@ -1003,112 +1004,19 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
}
|
||||
}
|
||||
>
|
||||
<AutoSizer
|
||||
content={true}
|
||||
detectAnyWindowResize={false}
|
||||
>
|
||||
<WrappedByAutoSizer
|
||||
innerRef={[Function]}
|
||||
>
|
||||
<div
|
||||
className="sc-bwzfXH ffMqh"
|
||||
>
|
||||
<BarChartWithCustomPrompt
|
||||
configs={
|
||||
Object {
|
||||
"axis": Object {
|
||||
"xTickFormatter": [Function],
|
||||
},
|
||||
"series": Object {
|
||||
"xScaleType": "ordinal",
|
||||
"yScaleType": "linear",
|
||||
},
|
||||
"settings": Object {
|
||||
"onElementClick": [Function],
|
||||
"rotation": 90,
|
||||
},
|
||||
}
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#DB1374",
|
||||
"key": "uniqueSourceIps",
|
||||
"value": Array [
|
||||
Object {
|
||||
"x": "uniqueSourceIps",
|
||||
"y": "1714",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"color": "#490092",
|
||||
"key": "uniqueDestinationIps",
|
||||
"value": Array [
|
||||
Object {
|
||||
"x": "uniqueDestinationIps",
|
||||
"y": 2354,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<BarChartBaseComponent
|
||||
configs={
|
||||
Object {
|
||||
"axis": Object {
|
||||
"xTickFormatter": [Function],
|
||||
},
|
||||
"series": Object {
|
||||
"xScaleType": "ordinal",
|
||||
"yScaleType": "linear",
|
||||
},
|
||||
"settings": Object {
|
||||
"onElementClick": [Function],
|
||||
"rotation": 90,
|
||||
},
|
||||
}
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#DB1374",
|
||||
"key": "uniqueSourceIps",
|
||||
"value": Array [
|
||||
Object {
|
||||
"x": "uniqueSourceIps",
|
||||
"y": "1714",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"color": "#490092",
|
||||
"key": "uniqueDestinationIps",
|
||||
"value": Array [
|
||||
Object {
|
||||
"x": "uniqueDestinationIps",
|
||||
"y": 2354,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</BarChartWithCustomPrompt>
|
||||
</div>
|
||||
</WrappedByAutoSizer>
|
||||
</AutoSizer>
|
||||
<div
|
||||
className="barchart"
|
||||
/>
|
||||
</BarChart>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
<EuiFlexItem
|
||||
className="sc-bZQynM kpuYFd"
|
||||
className="sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem sc-bZQynM kpuYFd"
|
||||
className="euiFlexItem sc-ifAKCX cFdETZ"
|
||||
>
|
||||
<AreaChart
|
||||
areaChart={
|
||||
|
@ -1157,6 +1065,7 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
"xTickFormatter": [Function],
|
||||
"yTickFormatter": [Function],
|
||||
},
|
||||
"customHeight": 74,
|
||||
"series": Object {
|
||||
"xScaleType": "time",
|
||||
"yScaleType": "linear",
|
||||
|
@ -1167,134 +1076,9 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
|
|||
}
|
||||
}
|
||||
>
|
||||
<AutoSizer
|
||||
content={true}
|
||||
detectAnyWindowResize={false}
|
||||
>
|
||||
<WrappedByAutoSizer
|
||||
innerRef={[Function]}
|
||||
>
|
||||
<div
|
||||
className="sc-bwzfXH ffMqh"
|
||||
>
|
||||
<AreaChartWithCustomPrompt
|
||||
configs={
|
||||
Object {
|
||||
"axis": Object {
|
||||
"xTickFormatter": [Function],
|
||||
"yTickFormatter": [Function],
|
||||
},
|
||||
"series": Object {
|
||||
"xScaleType": "time",
|
||||
"yScaleType": "linear",
|
||||
},
|
||||
"settings": Object {
|
||||
"onBrushEnd": [MockFunction],
|
||||
},
|
||||
}
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#DB1374",
|
||||
"key": "uniqueSourceIpsHistogram",
|
||||
"value": Array [
|
||||
Object {
|
||||
"x": 1556888400000,
|
||||
"y": 565975,
|
||||
},
|
||||
Object {
|
||||
"x": 1556931600000,
|
||||
"y": 1084366,
|
||||
},
|
||||
Object {
|
||||
"x": 1556974800000,
|
||||
"y": 12280,
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"color": "#490092",
|
||||
"key": "uniqueDestinationIpsHistogram",
|
||||
"value": Array [
|
||||
Object {
|
||||
"x": 1556888400000,
|
||||
"y": 565975,
|
||||
},
|
||||
Object {
|
||||
"x": 1556931600000,
|
||||
"y": 1084366,
|
||||
},
|
||||
Object {
|
||||
"x": 1556974800000,
|
||||
"y": 12280,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<AreaChartBaseComponent
|
||||
configs={
|
||||
Object {
|
||||
"axis": Object {
|
||||
"xTickFormatter": [Function],
|
||||
"yTickFormatter": [Function],
|
||||
},
|
||||
"series": Object {
|
||||
"xScaleType": "time",
|
||||
"yScaleType": "linear",
|
||||
},
|
||||
"settings": Object {
|
||||
"onBrushEnd": [MockFunction],
|
||||
},
|
||||
}
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#DB1374",
|
||||
"key": "uniqueSourceIpsHistogram",
|
||||
"value": Array [
|
||||
Object {
|
||||
"x": 1556888400000,
|
||||
"y": 565975,
|
||||
},
|
||||
Object {
|
||||
"x": 1556931600000,
|
||||
"y": 1084366,
|
||||
},
|
||||
Object {
|
||||
"x": 1556974800000,
|
||||
"y": 12280,
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"color": "#490092",
|
||||
"key": "uniqueDestinationIpsHistogram",
|
||||
"value": Array [
|
||||
Object {
|
||||
"x": 1556888400000,
|
||||
"y": 565975,
|
||||
},
|
||||
Object {
|
||||
"x": 1556931600000,
|
||||
"y": 1084366,
|
||||
},
|
||||
Object {
|
||||
"x": 1556974800000,
|
||||
"y": 12280,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</AreaChartWithCustomPrompt>
|
||||
</div>
|
||||
</WrappedByAutoSizer>
|
||||
</AutoSizer>
|
||||
<div
|
||||
className="areachart"
|
||||
/>
|
||||
</AreaChart>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -37,6 +37,14 @@ import { KpiNetworkData, KpiHostsData } from '../../graphql/types';
|
|||
const from = new Date('2019-06-15T06:00:00.000Z').valueOf();
|
||||
const to = new Date('2019-06-18T06:00:00.000Z').valueOf();
|
||||
|
||||
jest.mock('../charts/areachart', () => {
|
||||
return { AreaChart: () => <div className="areachart"></div> };
|
||||
});
|
||||
|
||||
jest.mock('../charts/barchart', () => {
|
||||
return { BarChart: () => <div className="barchart"></div> };
|
||||
});
|
||||
|
||||
describe('Stat Items Component', () => {
|
||||
const theme = () => ({ eui: euiDarkVars, darkMode: true });
|
||||
const state: State = mockGlobalState;
|
||||
|
|
|
@ -27,7 +27,7 @@ import styled from 'styled-components';
|
|||
import { KpiHostsData, KpiNetworkData } from '../../graphql/types';
|
||||
import { AreaChart } from '../charts/areachart';
|
||||
import { BarChart } from '../charts/barchart';
|
||||
import { ChartConfigsData, ChartData, ChartSeriesConfigs, UpdateDateRange } from '../charts/common';
|
||||
import { ChartSeriesData, ChartData, ChartSeriesConfigs, UpdateDateRange } from '../charts/common';
|
||||
import { getEmptyTagValue } from '../empty_value';
|
||||
|
||||
import { InspectButton } from '../inspect';
|
||||
|
@ -69,8 +69,8 @@ export interface StatItems {
|
|||
}
|
||||
|
||||
export interface StatItemsProps extends StatItems {
|
||||
areaChart?: ChartConfigsData[];
|
||||
barChart?: ChartConfigsData[];
|
||||
areaChart?: ChartSeriesData[];
|
||||
barChart?: ChartSeriesData[];
|
||||
from: number;
|
||||
id: string;
|
||||
narrowDateRange: UpdateDateRange;
|
||||
|
@ -79,6 +79,7 @@ export interface StatItemsProps extends StatItems {
|
|||
|
||||
export const numberFormatter = (value: string | number): string => value.toLocaleString();
|
||||
const statItemBarchartRotation: Rotation = 90;
|
||||
const statItemChartCustomHeight = 74;
|
||||
|
||||
export const areachartConfigs = (config?: {
|
||||
xTickFormatter: (value: number) => string;
|
||||
|
@ -95,6 +96,7 @@ export const areachartConfigs = (config?: {
|
|||
settings: {
|
||||
onBrushEnd: getOr(() => {}, 'onBrushEnd', config),
|
||||
},
|
||||
customHeight: statItemChartCustomHeight,
|
||||
});
|
||||
|
||||
export const barchartConfigs = (config?: { onElementClick?: ElementClickListener }) => ({
|
||||
|
@ -109,6 +111,7 @@ export const barchartConfigs = (config?: { onElementClick?: ElementClickListener
|
|||
onElementClick: getOr(() => {}, 'onElementClick', config),
|
||||
rotation: statItemBarchartRotation,
|
||||
},
|
||||
customHeight: statItemChartCustomHeight,
|
||||
});
|
||||
|
||||
export const addValueToFields = (
|
||||
|
@ -119,7 +122,7 @@ export const addValueToFields = (
|
|||
export const addValueToAreaChart = (
|
||||
fields: StatItem[],
|
||||
data: KpiHostsData | KpiNetworkData
|
||||
): ChartConfigsData[] =>
|
||||
): ChartSeriesData[] =>
|
||||
fields
|
||||
.filter(field => get(`${field.key}Histogram`, data) != null)
|
||||
.map(field => ({
|
||||
|
@ -131,9 +134,9 @@ export const addValueToAreaChart = (
|
|||
export const addValueToBarChart = (
|
||||
fields: StatItem[],
|
||||
data: KpiHostsData | KpiNetworkData
|
||||
): ChartConfigsData[] => {
|
||||
): ChartSeriesData[] => {
|
||||
if (fields.length === 0) return [];
|
||||
return fields.reduce((acc: ChartConfigsData[], field: StatItem, idx: number) => {
|
||||
return fields.reduce((acc: ChartSeriesData[], field: StatItem, idx: number) => {
|
||||
const { key, color } = field;
|
||||
const y: number | null = getOr(null, key, data);
|
||||
const x: string = get(`${idx}.name`, fields) || getOr('', `${idx}.description`, fields);
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
|
||||
export const EventsOverTimeGqlQuery = gql`
|
||||
query GetEventsOverTimeQuery(
|
||||
$sourceId: ID!
|
||||
$timerange: TimerangeInput!
|
||||
$defaultIndex: [String!]!
|
||||
$filterQuery: String
|
||||
$inspect: Boolean!
|
||||
) {
|
||||
source(id: $sourceId) {
|
||||
id
|
||||
EventsOverTime(
|
||||
timerange: $timerange
|
||||
filterQuery: $filterQuery
|
||||
defaultIndex: $defaultIndex
|
||||
) {
|
||||
eventsOverTime {
|
||||
x
|
||||
y
|
||||
g
|
||||
}
|
||||
totalCount
|
||||
inspect @include(if: $inspect) {
|
||||
dsl
|
||||
response
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { getOr } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { Query } from 'react-apollo';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
|
||||
import { inputsModel, State, inputsSelectors, hostsModel } from '../../../store';
|
||||
import { createFilter, getDefaultFetchPolicy } from '../../helpers';
|
||||
import { QueryTemplate, QueryTemplateProps } from '../../query_template';
|
||||
|
||||
import { EventsOverTimeGqlQuery } from './events_over_time.gql_query';
|
||||
import { GetEventsOverTimeQuery, MatrixOverTimeHistogramData } from '../../../graphql/types';
|
||||
|
||||
const ID = 'eventsOverTimeQuery';
|
||||
|
||||
export interface EventsArgs {
|
||||
endDate: number;
|
||||
eventsOverTime: MatrixOverTimeHistogramData[];
|
||||
id: string;
|
||||
inspect: inputsModel.InspectQuery;
|
||||
loading: boolean;
|
||||
refetch: inputsModel.Refetch;
|
||||
startDate: number;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface OwnProps extends QueryTemplateProps {
|
||||
children?: (args: EventsArgs) => React.ReactNode;
|
||||
type: hostsModel.HostsType;
|
||||
}
|
||||
|
||||
export interface EventsOverTimeComponentReduxProps {
|
||||
isInspected: boolean;
|
||||
}
|
||||
|
||||
type EventsOverTimeProps = OwnProps & EventsOverTimeComponentReduxProps;
|
||||
|
||||
class EventsOverTimeComponentQuery extends QueryTemplate<
|
||||
EventsOverTimeProps,
|
||||
GetEventsOverTimeQuery.Query,
|
||||
GetEventsOverTimeQuery.Variables
|
||||
> {
|
||||
public render() {
|
||||
const {
|
||||
children,
|
||||
filterQuery,
|
||||
id = ID,
|
||||
isInspected,
|
||||
sourceId,
|
||||
startDate,
|
||||
endDate,
|
||||
} = this.props;
|
||||
return (
|
||||
<Query<GetEventsOverTimeQuery.Query, GetEventsOverTimeQuery.Variables>
|
||||
query={EventsOverTimeGqlQuery}
|
||||
fetchPolicy={getDefaultFetchPolicy()}
|
||||
notifyOnNetworkStatusChange
|
||||
variables={{
|
||||
filterQuery: createFilter(filterQuery),
|
||||
sourceId,
|
||||
timerange: {
|
||||
interval: '12h',
|
||||
from: startDate!,
|
||||
to: endDate!,
|
||||
},
|
||||
defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY),
|
||||
inspect: isInspected,
|
||||
}}
|
||||
>
|
||||
{({ data, loading, refetch }) => {
|
||||
const source = getOr({}, `source.EventsOverTime`, data);
|
||||
const eventsOverTime = getOr([], `eventsOverTime`, source);
|
||||
const totalCount = getOr(-1, 'totalCount', source);
|
||||
return children!({
|
||||
endDate: endDate!,
|
||||
eventsOverTime,
|
||||
id,
|
||||
inspect: getOr(null, 'inspect', source),
|
||||
loading,
|
||||
refetch,
|
||||
startDate: startDate!,
|
||||
totalCount,
|
||||
});
|
||||
}}
|
||||
</Query>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getQuery = inputsSelectors.globalQueryByIdSelector();
|
||||
const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => {
|
||||
const { isInspected } = getQuery(state, id);
|
||||
return {
|
||||
isInspected,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export const EventsOverTimeQuery = connect(makeMapStateToProps)(EventsOverTimeComponentQuery);
|
|
@ -916,6 +916,53 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "EventsOverTime",
|
||||
"description": "",
|
||||
"args": [
|
||||
{
|
||||
"name": "timerange",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null }
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "filterQuery",
|
||||
"description": "",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "defaultIndex",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "OBJECT", "name": "EventsOverTimeData", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "Hosts",
|
||||
"description": "Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified",
|
||||
|
@ -5451,6 +5498,108 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "EventsOverTimeData",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "inspect",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "OBJECT", "name": "Inspect", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "eventsOverTime",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "MatrixOverTimeHistogramData",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "totalCount",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "MatrixOverTimeHistogramData",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "x",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "y",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "g",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "HostsSortField",
|
||||
|
|
|
@ -117,6 +117,8 @@ export interface Source {
|
|||
TimelineDetails: TimelineDetailsData;
|
||||
|
||||
LastEventTime: LastEventTimeData;
|
||||
|
||||
EventsOverTime: EventsOverTimeData;
|
||||
/** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */
|
||||
Hosts: HostsData;
|
||||
|
||||
|
@ -847,6 +849,22 @@ export interface LastEventTimeData {
|
|||
inspect?: Inspect | null;
|
||||
}
|
||||
|
||||
export interface EventsOverTimeData {
|
||||
inspect?: Inspect | null;
|
||||
|
||||
eventsOverTime: MatrixOverTimeHistogramData[];
|
||||
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface MatrixOverTimeHistogramData {
|
||||
x: number;
|
||||
|
||||
y: number;
|
||||
|
||||
g: string;
|
||||
}
|
||||
|
||||
export interface HostsData {
|
||||
edges: HostsEdges[];
|
||||
|
||||
|
@ -1835,6 +1853,13 @@ export interface LastEventTimeSourceArgs {
|
|||
|
||||
defaultIndex: string[];
|
||||
}
|
||||
export interface EventsOverTimeSourceArgs {
|
||||
timerange: TimerangeInput;
|
||||
|
||||
filterQuery?: string | null;
|
||||
|
||||
defaultIndex: string[];
|
||||
}
|
||||
export interface HostsSourceArgs {
|
||||
id?: string | null;
|
||||
|
||||
|
@ -2416,6 +2441,58 @@ export namespace GetDomainsQuery {
|
|||
};
|
||||
}
|
||||
|
||||
export namespace GetEventsOverTimeQuery {
|
||||
export type Variables = {
|
||||
sourceId: string;
|
||||
timerange: TimerangeInput;
|
||||
defaultIndex: string[];
|
||||
filterQuery?: string | null;
|
||||
inspect: boolean;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
|
||||
source: Source;
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
__typename?: 'Source';
|
||||
|
||||
id: string;
|
||||
|
||||
EventsOverTime: EventsOverTime;
|
||||
};
|
||||
|
||||
export type EventsOverTime = {
|
||||
__typename?: 'EventsOverTimeData';
|
||||
|
||||
eventsOverTime: _EventsOverTime[];
|
||||
|
||||
totalCount: number;
|
||||
|
||||
inspect?: Inspect | null;
|
||||
};
|
||||
|
||||
export type _EventsOverTime = {
|
||||
__typename?: 'MatrixOverTimeHistogramData';
|
||||
|
||||
x: number;
|
||||
|
||||
y: number;
|
||||
|
||||
g: string;
|
||||
};
|
||||
|
||||
export type Inspect = {
|
||||
__typename?: 'Inspect';
|
||||
|
||||
dsl: string[];
|
||||
|
||||
response: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export namespace GetLastEventTimeQuery {
|
||||
export type Variables = {
|
||||
sourceId: string;
|
||||
|
|
|
@ -55,6 +55,9 @@ const HostDetailsBodyComponent = React.memo<HostDetailsBodyComponentProps>(
|
|||
to: fromTo.to,
|
||||
});
|
||||
},
|
||||
updateDateRange: (min: number, max: number) => {
|
||||
setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
|
||||
},
|
||||
})}
|
||||
</>
|
||||
) : null
|
||||
|
|
|
@ -136,7 +136,6 @@ const HostDetailsComponent = React.memo<HostDetailsComponentProps>(
|
|||
)}
|
||||
</KpiHostDetailsQuery>
|
||||
|
||||
<EuiHorizontalRule />
|
||||
<SiemNavigation
|
||||
navTabs={navTabsHostDetails(detailName, hasMlUserPermissions(capabilities))}
|
||||
display="default"
|
||||
|
|
|
@ -57,6 +57,9 @@ const HostsBodyComponent = memo<HostsBodyComponentProps>(
|
|||
to: fromTo.to,
|
||||
});
|
||||
},
|
||||
updateDateRange: (min: number, max: number) => {
|
||||
setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
|
||||
},
|
||||
})}
|
||||
</>
|
||||
) : null
|
||||
|
|
|
@ -0,0 +1,387 @@
|
|||
/*
|
||||
* 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 { StaticIndexPattern } from 'ui/index_patterns';
|
||||
import { getOr, omit } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { HostsTable, UncommonProcessTable } from '../../components/page/hosts';
|
||||
|
||||
import { HostsQuery } from '../../containers/hosts';
|
||||
import { AuthenticationTable } from '../../components/page/hosts/authentications_table';
|
||||
import { AnomaliesHostTable } from '../../components/ml/tables/anomalies_host_table';
|
||||
import { UncommonProcessesQuery } from '../../containers/uncommon_processes';
|
||||
import { InspectQuery, Refetch } from '../../store/inputs/model';
|
||||
import { NarrowDateRange } from '../../components/ml/types';
|
||||
import { hostsModel } from '../../store';
|
||||
import { manageQuery } from '../../components/page/manage_query';
|
||||
import { AuthenticationsQuery } from '../../containers/authentications';
|
||||
import { ESTermQuery } from '../../../common/typed_json';
|
||||
import { HostsTableType } from '../../store/hosts/model';
|
||||
import { StatefulEventsViewer } from '../../components/events_viewer';
|
||||
import { NavTab } from '../../components/navigation/types';
|
||||
import { EventsOverTimeQuery } from '../../containers/events/events_over_time';
|
||||
import { EventsOverTimeHistogram } from '../../components/page/hosts/events_over_time';
|
||||
import { UpdateDateRange } from '../../components/charts/common';
|
||||
|
||||
const getTabsOnHostsUrl = (tabName: HostsTableType) => `#/hosts/${tabName}`;
|
||||
const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => {
|
||||
return `#/hosts/${hostName}/${tabName}`;
|
||||
};
|
||||
|
||||
type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts &
|
||||
HostsTableType.authentications &
|
||||
HostsTableType.uncommonProcesses &
|
||||
HostsTableType.events;
|
||||
|
||||
type KeyHostsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission & HostsTableType.anomalies;
|
||||
|
||||
export type KeyHostsNavTab = KeyHostsNavTabWithoutMlPermission | KeyHostsNavTabWithMlPermission;
|
||||
|
||||
type KeyHostDetailsNavTabWithoutMlPermission = HostsTableType.authentications &
|
||||
HostsTableType.uncommonProcesses &
|
||||
HostsTableType.events;
|
||||
|
||||
type KeyHostDetailsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission &
|
||||
HostsTableType.anomalies;
|
||||
|
||||
export type KeyHostDetailsNavTab =
|
||||
| KeyHostDetailsNavTabWithoutMlPermission
|
||||
| KeyHostDetailsNavTabWithMlPermission;
|
||||
|
||||
export type HostsNavTab = Record<KeyHostsNavTab, NavTab>;
|
||||
|
||||
export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => {
|
||||
const hostsNavTabs = {
|
||||
[HostsTableType.hosts]: {
|
||||
id: HostsTableType.hosts,
|
||||
name: i18n.NAVIGATION_ALL_HOSTS_TITLE,
|
||||
href: getTabsOnHostsUrl(HostsTableType.hosts),
|
||||
disabled: false,
|
||||
urlKey: 'host',
|
||||
},
|
||||
[HostsTableType.authentications]: {
|
||||
id: HostsTableType.authentications,
|
||||
name: i18n.NAVIGATION_AUTHENTICATIONS_TITLE,
|
||||
href: getTabsOnHostsUrl(HostsTableType.authentications),
|
||||
disabled: false,
|
||||
urlKey: 'host',
|
||||
},
|
||||
[HostsTableType.uncommonProcesses]: {
|
||||
id: HostsTableType.uncommonProcesses,
|
||||
name: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE,
|
||||
href: getTabsOnHostsUrl(HostsTableType.uncommonProcesses),
|
||||
disabled: false,
|
||||
urlKey: 'host',
|
||||
},
|
||||
[HostsTableType.anomalies]: {
|
||||
id: HostsTableType.anomalies,
|
||||
name: i18n.NAVIGATION_ANOMALIES_TITLE,
|
||||
href: getTabsOnHostsUrl(HostsTableType.anomalies),
|
||||
disabled: false,
|
||||
urlKey: 'host',
|
||||
},
|
||||
[HostsTableType.events]: {
|
||||
id: HostsTableType.events,
|
||||
name: i18n.NAVIGATION_EVENTS_TITLE,
|
||||
href: getTabsOnHostsUrl(HostsTableType.events),
|
||||
disabled: false,
|
||||
urlKey: 'host',
|
||||
},
|
||||
};
|
||||
|
||||
return hasMlUserPermissions ? hostsNavTabs : omit([HostsTableType.anomalies], hostsNavTabs);
|
||||
};
|
||||
|
||||
export const navTabsHostDetails = (
|
||||
hostName: string,
|
||||
hasMlUserPermissions: boolean
|
||||
): Record<KeyHostDetailsNavTab, NavTab> => {
|
||||
const hostDetailsNavTabs = {
|
||||
[HostsTableType.authentications]: {
|
||||
id: HostsTableType.authentications,
|
||||
name: i18n.NAVIGATION_AUTHENTICATIONS_TITLE,
|
||||
href: getTabsOnHostDetailsUrl(hostName, HostsTableType.authentications),
|
||||
disabled: false,
|
||||
urlKey: 'host',
|
||||
isDetailPage: true,
|
||||
},
|
||||
[HostsTableType.uncommonProcesses]: {
|
||||
id: HostsTableType.uncommonProcesses,
|
||||
name: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE,
|
||||
href: getTabsOnHostDetailsUrl(hostName, HostsTableType.uncommonProcesses),
|
||||
disabled: false,
|
||||
urlKey: 'host',
|
||||
isDetailPage: true,
|
||||
},
|
||||
[HostsTableType.anomalies]: {
|
||||
id: HostsTableType.anomalies,
|
||||
name: i18n.NAVIGATION_ANOMALIES_TITLE,
|
||||
href: getTabsOnHostDetailsUrl(hostName, HostsTableType.anomalies),
|
||||
disabled: false,
|
||||
urlKey: 'host',
|
||||
isDetailPage: true,
|
||||
},
|
||||
[HostsTableType.events]: {
|
||||
id: HostsTableType.events,
|
||||
name: i18n.NAVIGATION_EVENTS_TITLE,
|
||||
href: getTabsOnHostDetailsUrl(hostName, HostsTableType.events),
|
||||
disabled: false,
|
||||
urlKey: 'host',
|
||||
isDetailPage: true,
|
||||
},
|
||||
};
|
||||
|
||||
return hasMlUserPermissions
|
||||
? hostDetailsNavTabs
|
||||
: omit(HostsTableType.anomalies, hostDetailsNavTabs);
|
||||
};
|
||||
|
||||
interface OwnProps {
|
||||
type: hostsModel.HostsType;
|
||||
startDate: number;
|
||||
endDate: number;
|
||||
filterQuery?: string | ESTermQuery;
|
||||
kqlQueryExpression: string;
|
||||
}
|
||||
export type HostsComponentsQueryProps = OwnProps & {
|
||||
deleteQuery?: ({ id }: { id: string }) => void;
|
||||
indexPattern: StaticIndexPattern;
|
||||
skip: boolean;
|
||||
setQuery: ({
|
||||
id,
|
||||
inspect,
|
||||
loading,
|
||||
refetch,
|
||||
}: {
|
||||
id: string;
|
||||
inspect: InspectQuery | null;
|
||||
loading: boolean;
|
||||
refetch: Refetch;
|
||||
}) => void;
|
||||
updateDateRange?: UpdateDateRange;
|
||||
filterQueryExpression?: string;
|
||||
hostName?: string;
|
||||
};
|
||||
|
||||
export type AnomaliesQueryTabBodyProps = OwnProps & {
|
||||
skip: boolean;
|
||||
narrowDateRange: NarrowDateRange;
|
||||
hostName?: string;
|
||||
};
|
||||
|
||||
const AuthenticationTableManage = manageQuery(AuthenticationTable);
|
||||
const HostsTableManage = manageQuery(HostsTable);
|
||||
const UncommonProcessTableManage = manageQuery(UncommonProcessTable);
|
||||
|
||||
export const HostsQueryTabBody = ({
|
||||
deleteQuery,
|
||||
endDate,
|
||||
filterQuery,
|
||||
indexPattern,
|
||||
skip,
|
||||
setQuery,
|
||||
startDate,
|
||||
type,
|
||||
}: HostsComponentsQueryProps) => {
|
||||
return (
|
||||
<HostsQuery
|
||||
endDate={endDate}
|
||||
filterQuery={filterQuery}
|
||||
skip={skip}
|
||||
sourceId="default"
|
||||
startDate={startDate}
|
||||
type={type}
|
||||
>
|
||||
{({ hosts, totalCount, loading, pageInfo, loadPage, id, inspect, isInspected, refetch }) => (
|
||||
<HostsTableManage
|
||||
deleteQuery={deleteQuery}
|
||||
data={hosts}
|
||||
fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
|
||||
id={id}
|
||||
indexPattern={indexPattern}
|
||||
inspect={inspect}
|
||||
isInspect={isInspected}
|
||||
loading={loading}
|
||||
loadPage={loadPage}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)}
|
||||
totalCount={totalCount}
|
||||
type={type}
|
||||
/>
|
||||
)}
|
||||
</HostsQuery>
|
||||
);
|
||||
};
|
||||
|
||||
export const AuthenticationsQueryTabBody = ({
|
||||
deleteQuery,
|
||||
endDate,
|
||||
filterQuery,
|
||||
skip,
|
||||
setQuery,
|
||||
startDate,
|
||||
type,
|
||||
}: HostsComponentsQueryProps) => {
|
||||
return (
|
||||
<AuthenticationsQuery
|
||||
endDate={endDate}
|
||||
filterQuery={filterQuery}
|
||||
skip={skip}
|
||||
sourceId="default"
|
||||
startDate={startDate}
|
||||
type={type}
|
||||
>
|
||||
{({
|
||||
authentications,
|
||||
totalCount,
|
||||
loading,
|
||||
pageInfo,
|
||||
loadPage,
|
||||
id,
|
||||
inspect,
|
||||
isInspected,
|
||||
refetch,
|
||||
}) => {
|
||||
return (
|
||||
<AuthenticationTableManage
|
||||
data={authentications}
|
||||
deleteQuery={deleteQuery}
|
||||
fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
|
||||
id={id}
|
||||
inspect={inspect}
|
||||
isInspect={isInspected}
|
||||
loading={loading}
|
||||
loadPage={loadPage}
|
||||
refetch={refetch}
|
||||
showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)}
|
||||
setQuery={setQuery}
|
||||
totalCount={totalCount}
|
||||
type={type}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AuthenticationsQuery>
|
||||
);
|
||||
};
|
||||
|
||||
export const UncommonProcessTabBody = ({
|
||||
deleteQuery,
|
||||
endDate,
|
||||
filterQuery,
|
||||
skip,
|
||||
setQuery,
|
||||
startDate,
|
||||
type,
|
||||
}: HostsComponentsQueryProps) => {
|
||||
return (
|
||||
<UncommonProcessesQuery
|
||||
endDate={endDate}
|
||||
filterQuery={filterQuery}
|
||||
skip={skip}
|
||||
sourceId="default"
|
||||
startDate={startDate}
|
||||
type={type}
|
||||
>
|
||||
{({
|
||||
uncommonProcesses,
|
||||
totalCount,
|
||||
loading,
|
||||
pageInfo,
|
||||
loadPage,
|
||||
id,
|
||||
inspect,
|
||||
isInspected,
|
||||
refetch,
|
||||
}) => (
|
||||
<UncommonProcessTableManage
|
||||
deleteQuery={deleteQuery}
|
||||
data={uncommonProcesses}
|
||||
fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
|
||||
id={id}
|
||||
inspect={inspect}
|
||||
isInspect={isInspected}
|
||||
loading={loading}
|
||||
loadPage={loadPage}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)}
|
||||
totalCount={totalCount}
|
||||
type={type}
|
||||
/>
|
||||
)}
|
||||
</UncommonProcessesQuery>
|
||||
);
|
||||
};
|
||||
|
||||
export const AnomaliesTabBody = ({
|
||||
endDate,
|
||||
skip,
|
||||
startDate,
|
||||
type,
|
||||
narrowDateRange,
|
||||
hostName,
|
||||
}: AnomaliesQueryTabBodyProps) => {
|
||||
return (
|
||||
<AnomaliesHostTable
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
skip={skip}
|
||||
type={type}
|
||||
hostName={hostName}
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const EventsOverTimeManage = manageQuery(EventsOverTimeHistogram);
|
||||
|
||||
export const EventsTabBody = ({
|
||||
endDate,
|
||||
kqlQueryExpression,
|
||||
startDate,
|
||||
setQuery,
|
||||
filterQuery,
|
||||
updateDateRange = () => {},
|
||||
}: HostsComponentsQueryProps) => {
|
||||
const HOSTS_PAGE_TIMELINE_ID = 'hosts-page';
|
||||
|
||||
return (
|
||||
<>
|
||||
<EventsOverTimeQuery
|
||||
endDate={endDate}
|
||||
filterQuery={filterQuery}
|
||||
sourceId="default"
|
||||
startDate={startDate}
|
||||
type={hostsModel.HostsType.page}
|
||||
>
|
||||
{({ eventsOverTime, loading, id, inspect, refetch, totalCount }) => (
|
||||
<EventsOverTimeManage
|
||||
data={eventsOverTime!}
|
||||
endDate={endDate}
|
||||
id={id}
|
||||
inspect={inspect}
|
||||
loading={loading}
|
||||
updateDateRange={updateDateRange}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
startDate={startDate}
|
||||
totalCount={totalCount}
|
||||
/>
|
||||
)}
|
||||
</EventsOverTimeQuery>
|
||||
<EuiSpacer size="l" />
|
||||
<StatefulEventsViewer
|
||||
end={endDate}
|
||||
id={HOSTS_PAGE_TIMELINE_ID}
|
||||
kqlQueryExpression={kqlQueryExpression}
|
||||
start={startDate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -5,22 +5,58 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { StatefulEventsViewer } from '../../../components/events_viewer';
|
||||
import { HostsComponentsQueryProps } from './types';
|
||||
import { manageQuery } from '../../../components/page/manage_query';
|
||||
import { EventsOverTimeHistogram } from '../../../components/page/hosts/events_over_time';
|
||||
import { EventsOverTimeQuery } from '../../../containers/events/events_over_time';
|
||||
import { hostsModel } from '../../../store/hosts';
|
||||
|
||||
const HOSTS_PAGE_TIMELINE_ID = 'hosts-page';
|
||||
const EventsOverTimeManage = manageQuery(EventsOverTimeHistogram);
|
||||
|
||||
export const EventsQueryTabBody = ({
|
||||
endDate,
|
||||
kqlQueryExpression,
|
||||
startDate,
|
||||
}: HostsComponentsQueryProps) => (
|
||||
<StatefulEventsViewer
|
||||
end={endDate}
|
||||
id={HOSTS_PAGE_TIMELINE_ID}
|
||||
kqlQueryExpression={kqlQueryExpression}
|
||||
start={startDate}
|
||||
/>
|
||||
);
|
||||
setQuery,
|
||||
filterQuery,
|
||||
updateDateRange = () => {},
|
||||
}: HostsComponentsQueryProps) => {
|
||||
return (
|
||||
<>
|
||||
<EventsOverTimeQuery
|
||||
endDate={endDate}
|
||||
filterQuery={filterQuery}
|
||||
sourceId="default"
|
||||
startDate={startDate}
|
||||
type={hostsModel.HostsType.page}
|
||||
>
|
||||
{({ eventsOverTime, loading, id, inspect, refetch, totalCount }) => (
|
||||
<EventsOverTimeManage
|
||||
data={eventsOverTime!}
|
||||
endDate={endDate}
|
||||
id={id}
|
||||
inspect={inspect}
|
||||
loading={loading}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
startDate={startDate}
|
||||
totalCount={totalCount}
|
||||
updateDateRange={updateDateRange}
|
||||
/>
|
||||
)}
|
||||
</EventsOverTimeQuery>
|
||||
<EuiSpacer size="l" />
|
||||
<StatefulEventsViewer
|
||||
end={endDate}
|
||||
id={HOSTS_PAGE_TIMELINE_ID}
|
||||
kqlQueryExpression={kqlQueryExpression}
|
||||
start={startDate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
EventsQueryTabBody.displayName = 'EventsQueryTabBody';
|
||||
|
|
|
@ -12,6 +12,7 @@ import { InspectQuery, Refetch } from '../../../store/inputs/model';
|
|||
|
||||
import { HostsTableType } from '../../../store/hosts/model';
|
||||
import { NavTab } from '../../../components/navigation/types';
|
||||
import { UpdateDateRange } from '../../../components/charts/common';
|
||||
|
||||
export type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts &
|
||||
HostsTableType.authentications &
|
||||
|
@ -53,6 +54,7 @@ export type HostsComponentsQueryProps = QueryTabBodyProps & {
|
|||
loading: boolean;
|
||||
refetch: Refetch;
|
||||
}) => void;
|
||||
updateDateRange?: UpdateDateRange;
|
||||
narrowDateRange?: NarrowDateRange;
|
||||
};
|
||||
|
||||
|
|
|
@ -32,6 +32,11 @@ export interface EventsResolversDeps {
|
|||
events: Events;
|
||||
}
|
||||
|
||||
type QueryEventsOverTimeResolver = ChildResolverOf<
|
||||
AppResolverOf<SourceResolvers.EventsOverTimeResolver>,
|
||||
QuerySourceResolver
|
||||
>;
|
||||
|
||||
export const createEventsResolvers = (
|
||||
libs: EventsResolversDeps
|
||||
): {
|
||||
|
@ -39,6 +44,7 @@ export const createEventsResolvers = (
|
|||
Timeline: QueryTimelineResolver;
|
||||
TimelineDetails: QueryTimelineDetailsResolver;
|
||||
LastEventTime: QueryLastEventTimeResolver;
|
||||
EventsOverTime: QueryEventsOverTimeResolver;
|
||||
};
|
||||
} => ({
|
||||
Source: {
|
||||
|
@ -65,6 +71,13 @@ export const createEventsResolvers = (
|
|||
};
|
||||
return libs.events.getLastEventTimeData(req, options);
|
||||
},
|
||||
async EventsOverTime(source, args, { req }, info) {
|
||||
const options = {
|
||||
...createOptions(source, args, info),
|
||||
defaultIndex: args.defaultIndex,
|
||||
};
|
||||
return libs.events.getEventsOverTime(req, options);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -68,6 +68,18 @@ export const eventsSchema = gql`
|
|||
network
|
||||
}
|
||||
|
||||
type MatrixOverTimeHistogramData {
|
||||
x: Float!
|
||||
y: Float!
|
||||
g: String!
|
||||
}
|
||||
|
||||
type EventsOverTimeData {
|
||||
inspect: Inspect
|
||||
eventsOverTime: [MatrixOverTimeHistogramData!]!
|
||||
totalCount: Float!
|
||||
}
|
||||
|
||||
extend type Source {
|
||||
Timeline(
|
||||
pagination: PaginationInput!
|
||||
|
@ -88,5 +100,10 @@ export const eventsSchema = gql`
|
|||
details: LastTimeDetails!
|
||||
defaultIndex: [String!]!
|
||||
): LastEventTimeData!
|
||||
EventsOverTime(
|
||||
timerange: TimerangeInput!
|
||||
filterQuery: String
|
||||
defaultIndex: [String!]!
|
||||
): EventsOverTimeData!
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -146,6 +146,8 @@ export interface Source {
|
|||
TimelineDetails: TimelineDetailsData;
|
||||
|
||||
LastEventTime: LastEventTimeData;
|
||||
|
||||
EventsOverTime: EventsOverTimeData;
|
||||
/** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */
|
||||
Hosts: HostsData;
|
||||
|
||||
|
@ -876,6 +878,22 @@ export interface LastEventTimeData {
|
|||
inspect?: Inspect | null;
|
||||
}
|
||||
|
||||
export interface EventsOverTimeData {
|
||||
inspect?: Inspect | null;
|
||||
|
||||
eventsOverTime: MatrixOverTimeHistogramData[];
|
||||
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface MatrixOverTimeHistogramData {
|
||||
x: number;
|
||||
|
||||
y: number;
|
||||
|
||||
g: string;
|
||||
}
|
||||
|
||||
export interface HostsData {
|
||||
edges: HostsEdges[];
|
||||
|
||||
|
@ -1864,6 +1882,13 @@ export interface LastEventTimeSourceArgs {
|
|||
|
||||
defaultIndex: string[];
|
||||
}
|
||||
export interface EventsOverTimeSourceArgs {
|
||||
timerange: TimerangeInput;
|
||||
|
||||
filterQuery?: string | null;
|
||||
|
||||
defaultIndex: string[];
|
||||
}
|
||||
export interface HostsSourceArgs {
|
||||
id?: string | null;
|
||||
|
||||
|
@ -2497,6 +2522,8 @@ export namespace SourceResolvers {
|
|||
TimelineDetails?: TimelineDetailsResolver<TimelineDetailsData, TypeParent, Context>;
|
||||
|
||||
LastEventTime?: LastEventTimeResolver<LastEventTimeData, TypeParent, Context>;
|
||||
|
||||
EventsOverTime?: EventsOverTimeResolver<EventsOverTimeData, TypeParent, Context>;
|
||||
/** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */
|
||||
Hosts?: HostsResolver<HostsData, TypeParent, Context>;
|
||||
|
||||
|
@ -2609,6 +2636,19 @@ export namespace SourceResolvers {
|
|||
defaultIndex: string[];
|
||||
}
|
||||
|
||||
export type EventsOverTimeResolver<
|
||||
R = EventsOverTimeData,
|
||||
Parent = Source,
|
||||
Context = SiemContext
|
||||
> = Resolver<R, Parent, Context, EventsOverTimeArgs>;
|
||||
export interface EventsOverTimeArgs {
|
||||
timerange: TimerangeInput;
|
||||
|
||||
filterQuery?: string | null;
|
||||
|
||||
defaultIndex: string[];
|
||||
}
|
||||
|
||||
export type HostsResolver<R = HostsData, Parent = Source, Context = SiemContext> = Resolver<
|
||||
R,
|
||||
Parent,
|
||||
|
@ -5204,6 +5244,58 @@ export namespace LastEventTimeDataResolvers {
|
|||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
||||
export namespace EventsOverTimeDataResolvers {
|
||||
export interface Resolvers<Context = SiemContext, TypeParent = EventsOverTimeData> {
|
||||
inspect?: InspectResolver<Inspect | null, TypeParent, Context>;
|
||||
|
||||
eventsOverTime?: EventsOverTimeResolver<MatrixOverTimeHistogramData[], TypeParent, Context>;
|
||||
|
||||
totalCount?: TotalCountResolver<number, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type InspectResolver<
|
||||
R = Inspect | null,
|
||||
Parent = EventsOverTimeData,
|
||||
Context = SiemContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type EventsOverTimeResolver<
|
||||
R = MatrixOverTimeHistogramData[],
|
||||
Parent = EventsOverTimeData,
|
||||
Context = SiemContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type TotalCountResolver<
|
||||
R = number,
|
||||
Parent = EventsOverTimeData,
|
||||
Context = SiemContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
||||
export namespace MatrixOverTimeHistogramDataResolvers {
|
||||
export interface Resolvers<Context = SiemContext, TypeParent = MatrixOverTimeHistogramData> {
|
||||
x?: XResolver<number, TypeParent, Context>;
|
||||
|
||||
y?: YResolver<number, TypeParent, Context>;
|
||||
|
||||
g?: GResolver<string, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type XResolver<
|
||||
R = number,
|
||||
Parent = MatrixOverTimeHistogramData,
|
||||
Context = SiemContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type YResolver<
|
||||
R = number,
|
||||
Parent = MatrixOverTimeHistogramData,
|
||||
Context = SiemContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type GResolver<
|
||||
R = string,
|
||||
Parent = MatrixOverTimeHistogramData,
|
||||
Context = SiemContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
||||
export namespace HostsDataResolvers {
|
||||
export interface Resolvers<Context = SiemContext, TypeParent = HostsData> {
|
||||
edges?: EdgesResolver<HostsEdges[], TypeParent, Context>;
|
||||
|
|
|
@ -25,12 +25,13 @@ import {
|
|||
TimelineData,
|
||||
TimelineDetailsData,
|
||||
TimelineEdges,
|
||||
EventsOverTimeData,
|
||||
} from '../../graphql/types';
|
||||
import { baseCategoryFields } from '../../utils/beat_schema/8.0.0';
|
||||
import { reduceFields } from '../../utils/build_query/reduce_fields';
|
||||
import { mergeFieldsWithHit, inspectStringifyObject } from '../../utils/build_query';
|
||||
import { eventFieldsMap } from '../ecs_fields';
|
||||
import { FrameworkAdapter, FrameworkRequest } from '../framework';
|
||||
import { FrameworkAdapter, FrameworkRequest, RequestBasicOptions } from '../framework';
|
||||
import { TermAggregation } from '../types';
|
||||
|
||||
import { buildDetailsQuery, buildTimelineQuery } from './query.dsl';
|
||||
|
@ -42,7 +43,10 @@ import {
|
|||
LastEventTimeRequestOptions,
|
||||
RequestDetailsOptions,
|
||||
TimelineRequestOptions,
|
||||
EventsActionGroupData,
|
||||
} from './types';
|
||||
import { buildEventsOverTimeQuery } from './query.events_over_time.dsl';
|
||||
import { MatrixOverTimeHistogramData } from '../../../public/graphql/types';
|
||||
|
||||
export class ElasticsearchEventsAdapter implements EventsAdapter {
|
||||
constructor(private readonly framework: FrameworkAdapter) {}
|
||||
|
@ -125,8 +129,65 @@ export class ElasticsearchEventsAdapter implements EventsAdapter {
|
|||
lastSeen: getOr(null, 'aggregations.last_seen_event.value_as_string', response),
|
||||
};
|
||||
}
|
||||
|
||||
public async getEventsOverTime(
|
||||
request: FrameworkRequest,
|
||||
options: RequestBasicOptions
|
||||
): Promise<EventsOverTimeData> {
|
||||
const dsl = buildEventsOverTimeQuery(options);
|
||||
const response = await this.framework.callWithRequest<EventHit, TermAggregation>(
|
||||
request,
|
||||
'search',
|
||||
dsl
|
||||
);
|
||||
const totalCount = getOr(0, 'hits.total.value', response);
|
||||
const eventsOverTimeBucket = getOr([], 'aggregations.eventActionGroup.buckets', response);
|
||||
const inspect = {
|
||||
dsl: [inspectStringifyObject(dsl)],
|
||||
response: [inspectStringifyObject(response)],
|
||||
};
|
||||
return {
|
||||
inspect,
|
||||
eventsOverTime: getEventsOverTimeByActionName(eventsOverTimeBucket),
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not in use at the moment,
|
||||
* reserved this parser for next feature of switchign between total events and grouped events
|
||||
*/
|
||||
export const getTotalEventsOverTime = (
|
||||
data: EventsActionGroupData[]
|
||||
): MatrixOverTimeHistogramData[] => {
|
||||
return data && data.length > 0
|
||||
? data.map<MatrixOverTimeHistogramData>(({ key, doc_count }) => ({
|
||||
x: key,
|
||||
y: doc_count,
|
||||
g: 'total events',
|
||||
}))
|
||||
: [];
|
||||
};
|
||||
|
||||
const getEventsOverTimeByActionName = (
|
||||
data: EventsActionGroupData[]
|
||||
): MatrixOverTimeHistogramData[] => {
|
||||
let result: MatrixOverTimeHistogramData[] = [];
|
||||
data.forEach(({ key: group, events }) => {
|
||||
const eventsData = getOr([], 'buckets', events).map(
|
||||
({ key, doc_count }: { key: number; doc_count: number }) => ({
|
||||
x: key,
|
||||
y: doc_count,
|
||||
g: group,
|
||||
})
|
||||
);
|
||||
result = [...result, ...eventsData];
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const formatEventsData = (
|
||||
fields: readonly string[],
|
||||
hit: EventHit,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { LastEventTimeData, TimelineData, TimelineDetailsData } from '../../graphql/types';
|
||||
import { FrameworkRequest } from '../framework';
|
||||
import { FrameworkRequest, RequestBasicOptions } from '../framework';
|
||||
export * from './elasticsearch_adapter';
|
||||
import {
|
||||
EventsAdapter,
|
||||
|
@ -13,6 +13,7 @@ import {
|
|||
LastEventTimeRequestOptions,
|
||||
RequestDetailsOptions,
|
||||
} from './types';
|
||||
import { EventsOverTimeData } from '../../../public/graphql/types';
|
||||
|
||||
export class Events {
|
||||
constructor(private readonly adapter: EventsAdapter) {}
|
||||
|
@ -37,4 +38,11 @@ export class Events {
|
|||
): Promise<LastEventTimeData> {
|
||||
return this.adapter.getLastEventTimeData(req, options);
|
||||
}
|
||||
|
||||
public async getEventsOverTime(
|
||||
req: FrameworkRequest,
|
||||
options: RequestBasicOptions
|
||||
): Promise<EventsOverTimeData> {
|
||||
return this.adapter.getEventsOverTime(req, options);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { createQueryFilterClauses, calculateTimeseriesInterval } from '../../utils/build_query';
|
||||
import { RequestBasicOptions } from '../framework';
|
||||
|
||||
export const buildEventsOverTimeQuery = ({
|
||||
filterQuery,
|
||||
timerange: { from, to },
|
||||
defaultIndex,
|
||||
sourceConfiguration: {
|
||||
fields: { timestamp },
|
||||
},
|
||||
}: RequestBasicOptions) => {
|
||||
const filter = [
|
||||
...createQueryFilterClauses(filterQuery),
|
||||
{
|
||||
range: {
|
||||
[timestamp]: {
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getHistogramAggregation = () => {
|
||||
const minIntervalSeconds = 10;
|
||||
const interval = calculateTimeseriesInterval(from, to, minIntervalSeconds);
|
||||
const histogramTimestampField = '@timestamp';
|
||||
const dateHistogram = {
|
||||
date_histogram: {
|
||||
field: histogramTimestampField,
|
||||
fixed_interval: `${interval}s`,
|
||||
},
|
||||
};
|
||||
const autoDateHistogram = {
|
||||
auto_date_histogram: {
|
||||
field: histogramTimestampField,
|
||||
buckets: 36,
|
||||
},
|
||||
};
|
||||
return {
|
||||
eventActionGroup: {
|
||||
terms: {
|
||||
field: 'event.action',
|
||||
missing: 'All others',
|
||||
order: {
|
||||
_count: 'desc',
|
||||
},
|
||||
size: 10,
|
||||
},
|
||||
aggs: {
|
||||
events: interval ? dateHistogram : autoDateHistogram,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const dslQuery = {
|
||||
index: defaultIndex,
|
||||
allowNoIndices: true,
|
||||
ignoreUnavailable: true,
|
||||
body: {
|
||||
aggregations: getHistogramAggregation(),
|
||||
query: {
|
||||
bool: {
|
||||
filter,
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
track_total_hits: true,
|
||||
},
|
||||
};
|
||||
|
||||
return dslQuery;
|
||||
};
|
|
@ -11,8 +11,14 @@ import {
|
|||
SourceConfiguration,
|
||||
TimelineData,
|
||||
TimelineDetailsData,
|
||||
EventsOverTimeData,
|
||||
} from '../../graphql/types';
|
||||
import { FrameworkRequest, RequestOptions, RequestOptionsPaginated } from '../framework';
|
||||
import {
|
||||
FrameworkRequest,
|
||||
RequestOptions,
|
||||
RequestOptionsPaginated,
|
||||
RequestBasicOptions,
|
||||
} from '../framework';
|
||||
import { SearchHit } from '../types';
|
||||
|
||||
export interface EventsAdapter {
|
||||
|
@ -25,6 +31,10 @@ export interface EventsAdapter {
|
|||
req: FrameworkRequest,
|
||||
options: LastEventTimeRequestOptions
|
||||
): Promise<LastEventTimeData>;
|
||||
getEventsOverTime(
|
||||
req: FrameworkRequest,
|
||||
options: RequestBasicOptions
|
||||
): Promise<EventsOverTimeData>;
|
||||
}
|
||||
|
||||
export interface TimelineRequestOptions extends RequestOptions {
|
||||
|
@ -77,3 +87,17 @@ export interface RequestDetailsOptions {
|
|||
eventId: string;
|
||||
defaultIndex: string[];
|
||||
}
|
||||
|
||||
interface EventsOverTimeHistogramData {
|
||||
key_as_string: string;
|
||||
key: number;
|
||||
doc_count: number;
|
||||
}
|
||||
|
||||
export interface EventsActionGroupData {
|
||||
key: number;
|
||||
events: {
|
||||
bucket: EventsOverTimeHistogramData[];
|
||||
};
|
||||
doc_count: number;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
** Applying the same logic as:
|
||||
** x-pack/legacy/plugins/apm/server/lib/helpers/get_bucket_size/calculate_auto.js
|
||||
*/
|
||||
import moment from 'moment';
|
||||
import { get } from 'lodash/fp';
|
||||
const d = moment.duration;
|
||||
|
||||
const roundingRules = [
|
||||
[d(500, 'ms'), d(100, 'ms')],
|
||||
[d(5, 'second'), d(1, 'second')],
|
||||
[d(7.5, 'second'), d(5, 'second')],
|
||||
[d(15, 'second'), d(10, 'second')],
|
||||
[d(45, 'second'), d(30, 'second')],
|
||||
[d(3, 'minute'), d(1, 'minute')],
|
||||
[d(9, 'minute'), d(5, 'minute')],
|
||||
[d(20, 'minute'), d(10, 'minute')],
|
||||
[d(45, 'minute'), d(30, 'minute')],
|
||||
[d(2, 'hour'), d(1, 'hour')],
|
||||
[d(6, 'hour'), d(3, 'hour')],
|
||||
[d(24, 'hour'), d(12, 'hour')],
|
||||
[d(1, 'week'), d(1, 'd')],
|
||||
[d(3, 'week'), d(1, 'week')],
|
||||
[d(1, 'year'), d(1, 'month')],
|
||||
[Infinity, d(1, 'year')],
|
||||
];
|
||||
|
||||
const revRoundingRules = roundingRules.slice(0).reverse();
|
||||
|
||||
const find = (
|
||||
rules: Array<Array<number | moment.Duration>>,
|
||||
check: (
|
||||
bound: number | moment.Duration,
|
||||
interval: number | moment.Duration,
|
||||
target: number
|
||||
) => number | moment.Duration | undefined,
|
||||
last?: boolean
|
||||
): ((buckets: number, duration: number | moment.Duration) => moment.Duration | undefined) => {
|
||||
const pick = (buckets: number, duration: number | moment.Duration): number | moment.Duration => {
|
||||
const target =
|
||||
typeof duration === 'number' ? duration / buckets : duration.asMilliseconds() / buckets;
|
||||
let lastResp = null;
|
||||
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i];
|
||||
const resp = check(rule[0], rule[1], target);
|
||||
|
||||
if (resp == null) {
|
||||
if (last) {
|
||||
if (lastResp) return lastResp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!last && resp) return resp;
|
||||
lastResp = resp;
|
||||
}
|
||||
|
||||
// fallback to just a number of milliseconds, ensure ms is >= 1
|
||||
const ms = Math.max(Math.floor(target), 1);
|
||||
return moment.duration(ms, 'ms');
|
||||
};
|
||||
|
||||
return (buckets, duration) => {
|
||||
const interval = pick(buckets, duration);
|
||||
const intervalData = get('_data', interval);
|
||||
if (intervalData) return moment.duration(intervalData);
|
||||
};
|
||||
};
|
||||
|
||||
export const calculateAuto = {
|
||||
near: find(
|
||||
revRoundingRules,
|
||||
(bound, interval, target) => {
|
||||
if (bound > target) return interval;
|
||||
},
|
||||
true
|
||||
),
|
||||
lessThan: find(revRoundingRules, (_bound, interval, target) => {
|
||||
if (interval < target) return interval;
|
||||
}),
|
||||
atLeast: find(revRoundingRules, (_bound, interval, target) => {
|
||||
if (interval <= target) return interval;
|
||||
}),
|
||||
};
|
||||
|
||||
export const calculateTimeseriesInterval = (
|
||||
lowerBoundInMsSinceEpoch: number,
|
||||
upperBoundInMsSinceEpoch: number,
|
||||
minIntervalSeconds: number
|
||||
) => {
|
||||
const duration = moment.duration(upperBoundInMsSinceEpoch - lowerBoundInMsSinceEpoch, 'ms');
|
||||
|
||||
const matchedInterval = calculateAuto.near(50, duration);
|
||||
|
||||
return matchedInterval ? Math.max(matchedInterval.asSeconds(), 1) : null;
|
||||
};
|
|
@ -7,6 +7,7 @@
|
|||
export * from './fields';
|
||||
export * from './filters';
|
||||
export * from './merge_fields_with_hits';
|
||||
export * from './calculate_timeseries_interval';
|
||||
|
||||
export const assertUnreachable = (
|
||||
x: never,
|
||||
|
|
98
x-pack/test/api_integration/apis/siem/events_over_time.ts
Normal file
98
x-pack/test/api_integration/apis/siem/events_over_time.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { EventsOverTimeGqlQuery } from '../../../../legacy/plugins/siem/public/containers/events/events_over_time/events_over_time.gql_query';
|
||||
import { GetEventsOverTimeQuery } from '../../../../legacy/plugins/siem/public/graphql/types';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const client = getService('siemGraphQLClient');
|
||||
const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf();
|
||||
const TO = new Date('3000-01-01T00:00:00.000Z').valueOf();
|
||||
describe('Events over time', () => {
|
||||
describe('With filebeat', () => {
|
||||
before(() => esArchiver.load('filebeat/default'));
|
||||
after(() => esArchiver.unload('filebeat/default'));
|
||||
|
||||
it('Make sure that we get events over time data', () => {
|
||||
return client
|
||||
.query<GetEventsOverTimeQuery.Query>({
|
||||
query: EventsOverTimeGqlQuery,
|
||||
variables: {
|
||||
sourceId: 'default',
|
||||
timerange: {
|
||||
interval: '12h',
|
||||
to: TO,
|
||||
from: FROM,
|
||||
},
|
||||
defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
inspect: false,
|
||||
},
|
||||
})
|
||||
.then(resp => {
|
||||
const expectedData = [
|
||||
{
|
||||
x: new Date('2018-12-20T00:00:00.000Z').valueOf(),
|
||||
y: 4884,
|
||||
g: 'All others',
|
||||
__typename: 'MatrixOverTimeHistogramData',
|
||||
},
|
||||
{
|
||||
x: new Date('2018-12-20T00:00:00.000Z').valueOf(),
|
||||
y: 1273,
|
||||
g: 'netflow_flow',
|
||||
__typename: 'MatrixOverTimeHistogramData',
|
||||
},
|
||||
];
|
||||
const eventsOverTime = resp.data.source.EventsOverTime;
|
||||
expect(eventsOverTime.eventsOverTime).to.eql(expectedData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('With packetbeat', () => {
|
||||
before(() => esArchiver.load('packetbeat/default'));
|
||||
after(() => esArchiver.unload('packetbeat/default'));
|
||||
|
||||
it('Make sure that we get events over time data', () => {
|
||||
return client
|
||||
.query<GetEventsOverTimeQuery.Query>({
|
||||
query: EventsOverTimeGqlQuery,
|
||||
variables: {
|
||||
sourceId: 'default',
|
||||
timerange: {
|
||||
interval: '12h',
|
||||
to: TO,
|
||||
from: FROM,
|
||||
},
|
||||
defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
inspect: false,
|
||||
},
|
||||
})
|
||||
.then(resp => {
|
||||
const expectedData = [
|
||||
{
|
||||
x: new Date('2018-12-20T00:00:00.000Z').valueOf(),
|
||||
y: 4884,
|
||||
g: 'All others',
|
||||
__typename: 'MatrixOverTimeHistogramData',
|
||||
},
|
||||
{
|
||||
x: new Date('2018-12-20T00:00:00.000Z').valueOf(),
|
||||
y: 1273,
|
||||
g: 'netflow_flow',
|
||||
__typename: 'MatrixOverTimeHistogramData',
|
||||
},
|
||||
];
|
||||
const eventsOverTime = resp.data.source.EventsOverTime;
|
||||
expect(eventsOverTime.eventsOverTime).to.eql(expectedData);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -8,6 +8,7 @@ export default function ({ loadTestFile }) {
|
|||
describe('Siem GraphQL Endpoints', () => {
|
||||
loadTestFile(require.resolve('./authentications'));
|
||||
loadTestFile(require.resolve('./domains'));
|
||||
loadTestFile(require.resolve('./events_over_time'));
|
||||
loadTestFile(require.resolve('./hosts'));
|
||||
loadTestFile(require.resolve('./kpi_network'));
|
||||
loadTestFile(require.resolve('./kpi_hosts'));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue