From ba8e8fb1bea7c958eebae200362b97865b226f5b Mon Sep 17 00:00:00 2001
From: Angela Chuang <6295984+angorayc@users.noreply.github.com>
Date: Fri, 4 Oct 2019 11:00:26 +0100
Subject: [PATCH] [SIEM] Chart enhancement (#47130)
* chart styling
* rename variable
* styling for bar chart
* add unit test
* clean up
* fix for code review
---
.../components/charts/areachart.test.tsx | 293 +++++++++---------
.../public/components/charts/areachart.tsx | 61 ++--
.../components/charts/barchart.test.tsx | 262 +++++++---------
.../public/components/charts/barchart.tsx | 63 ++--
.../charts/chart_place_holder.test.tsx | 92 ++++++
.../components/charts/chart_place_holder.tsx | 40 +++
.../public/components/charts/common.test.tsx | 83 +++--
.../siem/public/components/charts/common.tsx | 51 +--
.../public/components/charts/translation.ts | 15 +
.../components/matrix_over_time/index.tsx | 2 +-
.../page/network/kpi_network/mock.ts | 11 +-
.../__snapshots__/index.test.tsx.snap | 3 +
.../public/components/stat_items/index.tsx | 2 +
13 files changed, 545 insertions(+), 433 deletions(-)
create mode 100644 x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.test.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.tsx
create mode 100644 x-pack/legacy/plugins/siem/public/components/charts/translation.ts
diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx
index 7338a959495f8..910e576e6e1e7 100644
--- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx
@@ -7,13 +7,135 @@
import { ShallowWrapper, shallow } from 'enzyme';
import * as React from 'react';
-import { AreaChartBaseComponent, AreaChartWithCustomPrompt, AreaChart } from './areachart';
-import { ChartHolder, ChartSeriesData } from './common';
+import { AreaChartBaseComponent, AreaChart } from './areachart';
+import { ChartSeriesData } from './common';
import { ScaleType, AreaSeries, Axis } from '@elastic/charts';
-jest.mock('@elastic/charts');
const customHeight = '100px';
const customWidth = '120px';
+const chartDataSets = [
+ [
+ [
+ {
+ key: 'uniqueSourceIpsHistogram',
+ value: [
+ { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 580213 },
+ { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: null },
+ { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12382 },
+ ],
+ color: '#DB1374',
+ },
+ {
+ key: 'uniqueDestinationIpsHistogram',
+ value: [
+ { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 },
+ { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 },
+ { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 },
+ ],
+ color: '#490092',
+ },
+ ],
+ ],
+ [
+ [
+ {
+ key: 'uniqueSourceIpsHistogram',
+ value: [
+ { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 580213 },
+ { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1096175 },
+ { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12382 },
+ ],
+ color: '#DB1374',
+ },
+ {
+ key: 'uniqueDestinationIpsHistogram',
+ value: [
+ { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 },
+ { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 },
+ { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 },
+ ],
+ color: '#490092',
+ },
+ ],
+ ],
+ [
+ [
+ {
+ key: 'uniqueSourceIpsHistogram',
+ value: [
+ { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 580213 },
+ { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: {} },
+ { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12382 },
+ ],
+ color: '#DB1374',
+ },
+ {
+ key: 'uniqueDestinationIpsHistogram',
+ value: [
+ { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 },
+ { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 },
+ { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 },
+ ],
+ color: '#490092',
+ },
+ ],
+ ],
+ [
+ [
+ {
+ key: 'uniqueSourceIpsHistogram',
+ value: [],
+ color: '#DB1374',
+ },
+ {
+ key: 'uniqueDestinationIpsHistogram',
+ value: [
+ { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 },
+ { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 },
+ { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 },
+ ],
+ color: '#490092',
+ },
+ ],
+ ],
+];
+
+const chartHolderDataSets = [
+ [null],
+ [[]],
+ [
+ {
+ key: 'uniqueSourceIpsHistogram',
+ value: null,
+ color: '#DB1374',
+ },
+ {
+ key: 'uniqueDestinationIpsHistogram',
+ value: null,
+ color: '#490092',
+ },
+ ],
+ [
+ {
+ key: 'uniqueSourceIpsHistogram',
+ value: [
+ { x: new Date('2019-05-03T13:00:00.000Z').valueOf() },
+ { x: new Date('2019-05-04T01:00:00.000Z').valueOf() },
+ { x: new Date('2019-05-04T13:00:00.000Z').valueOf() },
+ ],
+ color: '#DB1374',
+ },
+ {
+ key: 'uniqueDestinationIpsHistogram',
+ value: [
+ { x: new Date('2019-05-03T13:00:00.000Z').valueOf() },
+ { x: new Date('2019-05-04T01:00:00.000Z').valueOf() },
+ { x: new Date('2019-05-04T13:00:00.000Z').valueOf() },
+ ],
+ color: '#490092',
+ },
+ ],
+];
describe('AreaChartBaseComponent', () => {
let shallowWrapper: ShallowWrapper;
const mockAreaChartData: ChartSeriesData[] = [
@@ -186,137 +308,6 @@ describe('AreaChartBaseComponent', () => {
});
});
-describe('AreaChartWithCustomPrompt', () => {
- let shallowWrapper: ShallowWrapper;
- describe.each([
- [
- [
- {
- key: 'uniqueSourceIpsHistogram',
- value: [
- { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 580213 },
- { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1096175 },
- { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12382 },
- ],
- color: '#DB1374',
- },
- {
- key: 'uniqueDestinationIpsHistogram',
- value: [
- { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 },
- { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 },
- { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 },
- ],
- color: '#490092',
- },
- ],
- ],
- ] as Array<[ChartSeriesData[]]>)('renders areachart', data => {
- beforeAll(() => {
- shallowWrapper = shallow(
-
- );
- });
-
- it('render AreaChartBaseComponent', () => {
- expect(shallowWrapper.find(AreaChartBaseComponent)).toHaveLength(1);
- expect(shallowWrapper.find(ChartHolder)).toHaveLength(0);
- });
- });
-
- describe.each([
- null,
- [],
- [
- {
- key: 'uniqueSourceIpsHistogram',
- value: null,
- color: '#DB1374',
- },
- {
- key: 'uniqueDestinationIpsHistogram',
- value: null,
- color: '#490092',
- },
- ],
- [
- {
- key: 'uniqueSourceIpsHistogram',
- value: [
- { x: new Date('2019-05-03T13:00:00.000Z').valueOf() },
- { x: new Date('2019-05-04T01:00:00.000Z').valueOf() },
- { x: new Date('2019-05-04T13:00:00.000Z').valueOf() },
- ],
- color: '#DB1374',
- },
- {
- key: 'uniqueDestinationIpsHistogram',
- value: [
- { x: new Date('2019-05-03T13:00:00.000Z').valueOf() },
- { x: new Date('2019-05-04T01:00:00.000Z').valueOf() },
- { x: new Date('2019-05-04T13:00:00.000Z').valueOf() },
- ],
- color: '#490092',
- },
- ],
- [
- [
- {
- key: 'uniqueSourceIpsHistogram',
- value: [
- { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 580213 },
- { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: null },
- { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12382 },
- ],
- color: '#DB1374',
- },
- {
- key: 'uniqueDestinationIpsHistogram',
- value: [
- { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 },
- { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 },
- { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 },
- ],
- color: '#490092',
- },
- ],
- ],
- [
- [
- {
- key: 'uniqueSourceIpsHistogram',
- value: [
- { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 580213 },
- { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: {} },
- { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12382 },
- ],
- color: '#DB1374',
- },
- {
- key: 'uniqueDestinationIpsHistogram',
- value: [
- { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 },
- { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 },
- { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 },
- ],
- color: '#490092',
- },
- ],
- ],
- ] as Array<[ChartSeriesData[] | null | undefined]>)('renders prompt', data => {
- beforeAll(() => {
- shallowWrapper = shallow(
-
- );
- });
-
- it('render Chart Holder', () => {
- expect(shallowWrapper.find(AreaChartBaseComponent)).toHaveLength(0);
- expect(shallowWrapper.find(ChartHolder)).toHaveLength(1);
- });
- });
-});
-
describe('AreaChart', () => {
let shallowWrapper: ShallowWrapper;
const mockConfig = {
@@ -332,20 +323,28 @@ describe('AreaChart', () => {
},
customHeight: 324,
};
+ describe.each(chartDataSets as Array<[ChartSeriesData[]]>)('with valid data [%o]', data => {
+ beforeAll(() => {
+ shallowWrapper = shallow();
+ });
- it('should render if data exist', () => {
- const mockData = [
- { key: 'uniqueSourceIps', value: [{ y: 100, x: 100, g: 'group' }], color: '#DB1374' },
- ];
- shallowWrapper = shallow();
- expect(shallowWrapper.find('AutoSizer')).toHaveLength(1);
- expect(shallowWrapper.find('ChartHolder')).toHaveLength(0);
+ it(`should render area chart`, () => {
+ expect(shallowWrapper.find('AutoSizer')).toHaveLength(1);
+ expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(0);
+ });
});
- it('should render a chartHolder if no data given', () => {
- const mockData = [{ key: 'uniqueSourceIps', value: [], color: '#DB1374' }];
- shallowWrapper = shallow();
- expect(shallowWrapper.find('AutoSizer')).toHaveLength(0);
- expect(shallowWrapper.find('ChartHolder')).toHaveLength(1);
- });
+ describe.each(chartHolderDataSets as Array<[ChartSeriesData[] | null | undefined]>)(
+ 'with invalid data [%o]',
+ data => {
+ beforeAll(() => {
+ shallowWrapper = shallow();
+ });
+
+ it(`should render a chart place holder`, () => {
+ expect(shallowWrapper.find('AutoSizer')).toHaveLength(0);
+ expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(1);
+ });
+ }
+ );
});
diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx
index c4bb01a66753b..6347b93772b4e 100644
--- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx
@@ -17,19 +17,19 @@ import {
AreaSeriesStyle,
RecursivePartial,
} from '@elastic/charts';
-import { getOr, get } from 'lodash/fp';
+import { getOr, get, isNull, isNumber } from 'lodash/fp';
+import { AutoSizer } from '../auto_sizer';
+import { ChartPlaceHolder } from './chart_place_holder';
import {
- ChartSeriesData,
- ChartHolder,
- getSeriesStyle,
- WrappedByAutoSizer,
- ChartSeriesConfigs,
browserTimezone,
chartDefaultSettings,
+ ChartSeriesConfigs,
+ ChartSeriesData,
getChartHeight,
getChartWidth,
+ getSeriesStyle,
+ WrappedByAutoSizer,
} from './common';
-import { AutoSizer } from '../auto_sizer';
// custom series styles: https://ela.st/areachart-styling
const getSeriesLineStyle = (): RecursivePartial => {
@@ -51,6 +51,17 @@ const getSeriesLineStyle = (): RecursivePartial => {
};
};
+const checkIfAllTheDataInTheSeriesAreValid = (series: unknown): series is ChartSeriesData =>
+ !!get('value.length', series) &&
+ get('value', series).every(
+ ({ x, y }: { x: unknown; y: unknown }) => !isNull(x) && isNumber(y) && y > 0
+ );
+
+const checkIfAnyValidSeriesExist = (
+ data: ChartSeriesData[] | null | undefined
+): data is ChartSeriesData[] =>
+ Array.isArray(data) && data.some(checkIfAllTheDataInTheSeriesAreValid);
+
// https://ela.st/multi-areaseries
export const AreaChartBaseComponent = React.memo<{
data: ChartSeriesData[];
@@ -73,12 +84,12 @@ export const AreaChartBaseComponent = React.memo<{
{data.map(series => {
const seriesKey = series.key;
const seriesSpecId = getSpecId(seriesKey);
- return series.value != null ? (
+ return checkIfAllTheDataInTheSeriesAreValid(series) ? (
(({ data, height, width, configs }) => {
- return data != null &&
- data.length &&
- data.every(
- ({ value }) =>
- value != null &&
- value.length > 0 &&
- value.every(chart => chart.x != null && chart.y != null)
- ) ? (
-
- ) : (
-
- );
-});
-
-AreaChartWithCustomPrompt.displayName = 'AreaChartWithCustomPrompt';
-
export const AreaChart = React.memo<{
areaChart: ChartSeriesData[] | null | undefined;
configs?: ChartSeriesConfigs | undefined;
@@ -135,11 +124,11 @@ export const AreaChart = React.memo<{
const customHeight = get('customHeight', configs);
const customWidth = get('customWidth', configs);
- return get(`0.value.length`, areaChart) ? (
+ return checkIfAnyValidSeriesExist(areaChart) ? (
{({ measureRef, content: { height, width } }) => (
-
) : (
-
+
);
});
diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx
index 527556842126c..4b3ec577e6488 100644
--- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx
@@ -7,13 +7,113 @@
import { shallow, ShallowWrapper } from 'enzyme';
import * as React from 'react';
-import { BarChartBaseComponent, BarChartWithCustomPrompt, BarChart } from './barchart';
-import { ChartSeriesData, ChartHolder } from './common';
+import { BarChartBaseComponent, BarChart } from './barchart';
+import { ChartSeriesData } from './common';
import { BarSeries, ScaleType, Axis } from '@elastic/charts';
-jest.mock('@elastic/charts');
const customHeight = '100px';
const customWidth = '120px';
+const chartDataSets = [
+ [
+ [
+ { key: 'uniqueSourceIps', value: [{ y: 1714, x: 'uniqueSourceIps' }], color: '#DB1374' },
+ {
+ key: 'uniqueDestinationIps',
+ value: [{ y: 2354, x: 'uniqueDestinationIps' }],
+ color: '#490092',
+ },
+ ],
+ ],
+ [
+ [
+ { key: 'uniqueSourceIps', value: [{ y: 1714, x: '' }], color: '#DB1374' },
+ {
+ key: 'uniqueDestinationIps',
+ value: [{ y: 2354, x: '' }],
+ color: '#490092',
+ },
+ ],
+ ],
+ [
+ [
+ { key: 'uniqueSourceIps', value: [{ y: 1714, x: 'uniqueSourceIps' }], color: '#DB1374' },
+ {
+ key: 'uniqueDestinationIps',
+ value: [{ y: 0, x: 'uniqueDestinationIps' }],
+ color: '#490092',
+ },
+ ],
+ ],
+ [
+ [
+ { key: 'uniqueSourceIps', value: [{ y: null, x: 'uniqueSourceIps' }], color: '#DB1374' },
+ {
+ key: 'uniqueDestinationIps',
+ value: [{ y: 2354, x: 'uniqueDestinationIps' }],
+ color: '#490092',
+ },
+ ],
+ ],
+];
+
+const chartHolderDataSets: Array<[ChartSeriesData[] | undefined | null]> = [
+ [[]],
+ [null],
+ [
+ [
+ { key: 'uniqueSourceIps', color: '#DB1374' },
+ {
+ key: 'uniqueDestinationIps',
+ color: '#490092',
+ },
+ ],
+ ],
+ [
+ [
+ { key: 'uniqueSourceIps', value: [], color: '#DB1374' },
+ {
+ key: 'uniqueDestinationIps',
+ value: [],
+ color: '#490092',
+ },
+ ],
+ ],
+ [
+ [
+ { key: 'uniqueSourceIps', value: [{}], color: '#DB1374' },
+ {
+ key: 'uniqueDestinationIps',
+ value: [{}],
+ color: '#490092',
+ },
+ ],
+ ],
+ [
+ [
+ { key: 'uniqueSourceIps', value: [{ y: 0, x: 'uniqueSourceIps' }], color: '#DB1374' },
+ {
+ key: 'uniqueDestinationIps',
+ value: [{ y: 0, x: 'uniqueDestinationIps' }],
+ color: '#490092',
+ },
+ ],
+ ],
+] as any; // eslint-disable-line @typescript-eslint/no-explicit-any
+
+const mockConfig = {
+ series: {
+ xScaleType: ScaleType.Time,
+ yScaleType: ScaleType.Linear,
+ stackAccessors: ['g'],
+ },
+ axis: {
+ xTickFormatter: jest.fn(),
+ yTickFormatter: jest.fn(),
+ tickSize: 8,
+ },
+ customHeight: 324,
+};
+
describe('BarChartBaseComponent', () => {
let shallowWrapper: ShallowWrapper;
const mockBarChartData: ChartSeriesData[] = [
@@ -168,164 +268,28 @@ describe('BarChartBaseComponent', () => {
});
});
-describe.each([
- [
- [
- { key: 'uniqueSourceIps', value: [{ y: 1714, x: 'uniqueSourceIps' }], color: '#DB1374' },
- {
- key: 'uniqueDestinationIps',
- value: [{ y: 2354, x: 'uniqueDestinationIps' }],
- color: '#490092',
- },
- ],
- ],
- [
- [
- { key: 'uniqueSourceIps', value: [{ y: 1714, x: '' }], color: '#DB1374' },
- {
- key: 'uniqueDestinationIps',
- value: [{ y: 2354, x: '' }],
- color: '#490092',
- },
- ],
- ],
- [
- [
- { key: 'uniqueSourceIps', value: [{ y: 1714, x: 'uniqueSourceIps' }], color: '#DB1374' },
- {
- key: 'uniqueDestinationIps',
- value: [{ y: 0, x: 'uniqueDestinationIps' }],
- color: '#490092',
- },
- ],
- ],
-])('BarChartWithCustomPrompt', mockBarChartData => {
+describe.each(chartDataSets)('BarChart with valid data [%o]', data => {
let shallowWrapper: ShallowWrapper;
- describe('renders barchart', () => {
- beforeAll(() => {
- shallowWrapper = shallow(
-
- );
- });
-
- it('render BarChartBaseComponent', () => {
- expect(shallowWrapper.find(BarChartBaseComponent)).toHaveLength(1);
- expect(shallowWrapper.find(ChartHolder)).toHaveLength(0);
- });
- });
-});
-const table: Array<[ChartSeriesData[] | undefined | null]> = [
- [],
- null,
- [
- [
- { key: 'uniqueSourceIps', color: '#DB1374' },
- {
- key: 'uniqueDestinationIps',
- color: '#490092',
- },
- ],
- ],
- [
- [
- { key: 'uniqueSourceIps', value: [], color: '#DB1374' },
- {
- key: 'uniqueDestinationIps',
- value: [],
- color: '#490092',
- },
- ],
- ],
- [
- [
- { key: 'uniqueSourceIps', value: [{}], color: '#DB1374' },
- {
- key: 'uniqueDestinationIps',
- value: [{}],
- color: '#490092',
- },
- ],
- ],
- [
- [
- { key: 'uniqueSourceIps', value: [{ y: 0, x: 'uniqueSourceIps' }], color: '#DB1374' },
- {
- key: 'uniqueDestinationIps',
- value: [{ y: 0, x: 'uniqueDestinationIps' }],
- color: '#490092',
- },
- ],
- ],
- [
- [
- { key: 'uniqueSourceIps', value: [{ y: null, x: 'uniqueSourceIps' }], color: '#DB1374' },
- {
- key: 'uniqueDestinationIps',
- value: [{ y: 2354, x: 'uniqueDestinationIps' }],
- color: '#490092',
- },
- ],
- ],
- [
- [
- { key: 'uniqueSourceIps', value: [{ y: null, x: 'uniqueSourceIps' }], color: '#DB1374' },
- {
- key: 'uniqueDestinationIps',
- value: [{ y: null, x: 'uniqueDestinationIps' }],
- color: '#490092',
- },
- ],
- ],
-] as any; // eslint-disable-line @typescript-eslint/no-explicit-any
-
-describe.each(table)('renders prompt', data => {
- let shallowWrapper: ShallowWrapper;
beforeAll(() => {
- shallowWrapper = shallow(
-
- );
+ shallowWrapper = shallow();
});
- it('render Chart Holder', () => {
- expect(shallowWrapper.find(BarChartBaseComponent)).toHaveLength(0);
- expect(shallowWrapper.find(ChartHolder)).toHaveLength(1);
+ it(`should render chart`, () => {
+ expect(shallowWrapper.find('AutoSizer')).toHaveLength(1);
+ expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(0);
});
});
-describe('BarChart', () => {
+describe.each(chartHolderDataSets)('BarChart with invalid data [%o]', data => {
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();
- expect(shallowWrapper.find('AutoSizer')).toHaveLength(1);
- expect(shallowWrapper.find('ChartHolder')).toHaveLength(0);
+ beforeAll(() => {
+ shallowWrapper = shallow();
});
- it('should render a chartHolder if no data given', () => {
- const mockData = [{ key: 'uniqueSourceIps', value: [], color: '#DB1374' }];
- shallowWrapper = shallow();
+ it(`should render chart holder`, () => {
expect(shallowWrapper.find('AutoSizer')).toHaveLength(0);
- expect(shallowWrapper.find('ChartHolder')).toHaveLength(1);
+ expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(1);
});
});
diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx
index 02345fc149c2a..9ef26c690c56b 100644
--- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx
@@ -16,20 +16,33 @@ import {
ScaleType,
Settings,
} from '@elastic/charts';
-import { getOr, get } from 'lodash/fp';
+import { getOr, get, isNumber } from 'lodash/fp';
+import { AutoSizer } from '../auto_sizer';
+import { ChartPlaceHolder } from './chart_place_holder';
import {
- ChartSeriesData,
- WrappedByAutoSizer,
- ChartHolder,
- SeriesType,
- getSeriesStyle,
- ChartSeriesConfigs,
browserTimezone,
chartDefaultSettings,
+ ChartSeriesConfigs,
+ ChartSeriesData,
+ checkIfAllValuesAreZero,
+ getSeriesStyle,
getChartHeight,
getChartWidth,
+ SeriesType,
+ WrappedByAutoSizer,
} from './common';
-import { AutoSizer } from '../auto_sizer';
+
+const checkIfAllTheDataInTheSeriesAreValid = (series: ChartSeriesData): series is ChartSeriesData =>
+ series != null &&
+ !!get('value.length', series) &&
+ (series.value || []).every(({ x, y }) => isNumber(y) && y >= 0);
+
+const checkIfAnyValidSeriesExist = (
+ data: ChartSeriesData[] | null | undefined
+): data is ChartSeriesData[] =>
+ Array.isArray(data) &&
+ !checkIfAllValuesAreZero(data) &&
+ data.some(checkIfAllTheDataInTheSeriesAreValid);
// Bar chart rotation: https://ela.st/chart-rotations
export const BarChartBaseComponent = React.memo<{
@@ -54,7 +67,7 @@ export const BarChartBaseComponent = React.memo<{
const barSeriesKey = series.key;
const barSeriesSpecId = getSpecId(barSeriesKey);
const seriesType = SeriesType.BAR;
- return (
+ return checkIfAllTheDataInTheSeriesAreValid ? (
- );
+ ) : null;
})}
(({ 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)
- ) ? (
-
- ) : (
-
- );
-});
-
-BarChartWithCustomPrompt.displayName = 'BarChartWithCustomPrompt';
-
export const BarChart = React.memo<{
barChart: ChartSeriesData[] | null | undefined;
configs?: ChartSeriesConfigs | undefined;
}>(({ barChart, configs }) => {
const customHeight = get('customHeight', configs);
const customWidth = get('customWidth', configs);
- return get(`0.value.length`, barChart) ? (
+ return checkIfAnyValidSeriesExist(barChart) ? (
{({ measureRef, content: { height, width } }) => (
-
) : (
-
+
);
});
diff --git a/x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.test.tsx
new file mode 100644
index 0000000000000..7674fd09739f5
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.test.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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 React from 'react';
+import { ChartPlaceHolder } from './chart_place_holder';
+import { ChartSeriesData } from './common';
+
+describe('ChartPlaceHolder', () => {
+ let shallowWrapper: ShallowWrapper;
+ const mockDataAllZeros = [
+ {
+ key: 'mockKeyA',
+ color: 'mockColor',
+ value: [{ x: 'a', y: 0 }, { x: 'b', y: 0 }],
+ },
+ {
+ key: 'mockKeyB',
+ color: 'mockColor',
+ value: [{ x: 'a', y: 0 }, { x: 'b', y: 0 }],
+ },
+ ];
+ const mockDataUnexpectedValue = [
+ {
+ key: 'mockKeyA',
+ color: 'mockColor',
+ value: [{ x: 'a', y: '' }, { x: 'b', y: 0 }],
+ },
+ {
+ key: 'mockKeyB',
+ color: 'mockColor',
+ value: [{ x: 'a', y: {} }, { x: 'b', y: 0 }],
+ },
+ ];
+
+ it('should render with default props', () => {
+ const height = `100%`;
+ const width = `100%`;
+ shallowWrapper = shallow();
+ expect(shallowWrapper.props()).toMatchObject({
+ height,
+ width,
+ });
+ });
+
+ it('should render with given props', () => {
+ const height = `100px`;
+ const width = `100px`;
+ shallowWrapper = shallow(
+
+ );
+ expect(shallowWrapper.props()).toMatchObject({
+ height,
+ width,
+ });
+ });
+
+ it('should render correct wording when all values returned zero', () => {
+ const height = `100px`;
+ const width = `100px`;
+ shallowWrapper = shallow(
+
+ );
+ expect(
+ shallowWrapper
+ .find(`[data-test-subj="chartHolderText"]`)
+ .childAt(0)
+ .text()
+ ).toEqual('All values returned zero');
+ });
+
+ it('should render correct wording when unexpected value exists', () => {
+ const height = `100px`;
+ const width = `100px`;
+ shallowWrapper = shallow(
+
+ );
+ expect(
+ shallowWrapper
+ .find(`[data-test-subj="chartHolderText"]`)
+ .childAt(0)
+ .text()
+ ).toEqual('Chart Data Not Available');
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.tsx b/x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.tsx
new file mode 100644
index 0000000000000..22122b5fad74a
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { EuiFlexItem, EuiText, EuiFlexGroup } from '@elastic/eui';
+import styled from 'styled-components';
+import { ChartSeriesData, checkIfAllValuesAreZero } from './common';
+import * as i18n from './translation';
+
+const FlexGroup = styled(EuiFlexGroup)<{ height?: string | null; width?: string | null }>`
+ height: ${({ height }) => (height ? height : '100%')};
+ width: ${({ width }) => (width ? width : '100%')};
+ position: relative;
+ margin: 0;
+`;
+
+FlexGroup.displayName = 'FlexGroup';
+
+export const ChartPlaceHolder = ({
+ height = '100%',
+ width = '100%',
+ data,
+}: {
+ height?: string | null;
+ width?: string | null;
+ data: ChartSeriesData[] | null | undefined;
+}) => (
+
+
+
+ {checkIfAllValuesAreZero(data)
+ ? i18n.ALL_VALUES_ZEROS_TITLE
+ : i18n.DATA_NOT_AVAILABLE_TITLE}
+
+
+
+);
diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/common.test.tsx
index f23b97d8cd5ff..0fc7bc6afc216 100644
--- a/x-pack/legacy/plugins/siem/public/components/charts/common.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/charts/common.test.tsx
@@ -3,18 +3,18 @@
* 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 { shallow } from 'enzyme';
+import React from 'react';
import {
- ChartHolder,
+ checkIfAllValuesAreZero,
+ defaultChartHeight,
getChartHeight,
getChartWidth,
- WrappedByAutoSizer,
- defaultChartHeight,
getSeriesStyle,
- SeriesType,
getTheme,
+ SeriesType,
+ WrappedByAutoSizer,
+ ChartSeriesData,
} from './common';
import 'jest-styled-components';
import { mergeWithDefaultTheme, LIGHT_THEME } from '@elastic/charts';
@@ -26,30 +26,6 @@ jest.mock('@elastic/charts', () => {
};
});
-describe('ChartHolder', () => {
- let shallowWrapper: ShallowWrapper;
-
- it('should render with default props', () => {
- const height = `100%`;
- const width = `100%`;
- shallowWrapper = shallow();
- expect(shallowWrapper.props()).toMatchObject({
- height,
- width,
- });
- });
-
- it('should render with given props', () => {
- const height = `100px`;
- const width = `100px`;
- shallowWrapper = shallow();
- expect(shallowWrapper.props()).toMatchObject({
- height,
- width,
- });
- });
-});
-
describe('WrappedByAutoSizer', () => {
it('should render correct default height', () => {
const wrapper = shallow();
@@ -88,7 +64,7 @@ describe('getTheme', () => {
chartMargins: { bottom: 0, left: 0, right: 0, top: 4 },
chartPaddings: { bottom: 0, left: 0, right: 0, top: 0 },
scales: {
- barsPadding: 0.5,
+ barsPadding: 0.05,
},
};
getTheme();
@@ -130,3 +106,46 @@ describe('getChartWidth', () => {
expect(height).toEqual(defaultChartHeight);
});
});
+
+describe('checkIfAllValuesAreZero', () => {
+ const mockInvalidDataSets: Array<[ChartSeriesData[]]> = [
+ [[{ key: 'mockKey', color: 'mockColor', value: [{ x: 1, y: 0 }, { x: 1, y: 1 }] }]],
+ [
+ [
+ { key: 'mockKeyA', color: 'mockColor', value: [{ x: 1, y: 0 }, { x: 1, y: 1 }] },
+ { key: 'mockKeyB', color: 'mockColor', value: [{ x: 1, y: 0 }, { x: 1, y: 0 }] },
+ ],
+ ],
+ ];
+ const mockValidDataSets: Array<[ChartSeriesData[]]> = [
+ [[{ key: 'mockKey', color: 'mockColor', value: [{ x: 0, y: 0 }, { x: 1, y: 0 }] }]],
+ [
+ [
+ { key: 'mockKeyA', color: 'mockColor', value: [{ x: 1, y: 0 }, { x: 3, y: 0 }] },
+ { key: 'mockKeyB', color: 'mockColor', value: [{ x: 2, y: 0 }, { x: 4, y: 0 }] },
+ ],
+ ],
+ ];
+
+ describe.each(mockInvalidDataSets)('with data [%o]', data => {
+ let result: boolean;
+ beforeAll(() => {
+ result = checkIfAllValuesAreZero(data);
+ });
+
+ it(`should return false`, () => {
+ expect(result).toBeFalsy();
+ });
+ });
+
+ describe.each(mockValidDataSets)('with data [%o]', data => {
+ let result: boolean;
+ beforeAll(() => {
+ result = checkIfAllValuesAreZero(data);
+ });
+
+ it(`should return true`, () => {
+ expect(result).toBeTruthy();
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx
index 59873b2cd6a31..7ac91437920e5 100644
--- a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx
@@ -3,58 +3,33 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiText, EuiFlexItem } from '@elastic/eui';
-import React from 'react';
-import styled from 'styled-components';
+
+import chrome from 'ui/chrome';
import {
CustomSeriesColorsMap,
+ DARK_THEME,
DataSeriesColorsValues,
getSpecId,
+ LIGHT_THEME,
mergeWithDefaultTheme,
PartialTheme,
- LIGHT_THEME,
- DARK_THEME,
+ Rendering,
+ Rotation,
ScaleType,
- TickFormatter,
SettingSpecProps,
- Rotation,
- Rendering,
+ TickFormatter,
} from '@elastic/charts';
-import { i18n } from '@kbn/i18n';
-import chrome from 'ui/chrome';
import moment from 'moment-timezone';
+import styled from 'styled-components';
import { DEFAULT_DATE_FORMAT_TZ, DEFAULT_DARK_MODE } from '../../../common/constants';
+
export const defaultChartHeight = '100%';
export const defaultChartWidth = '100%';
const chartDefaultRotation: Rotation = 0;
const chartDefaultRendering: Rendering = 'canvas';
-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 = ({
- height = '100%',
- width = '100%',
-}: {
- height?: string | null;
- width?: string | null;
-}) => (
-
-
-
- {i18n.translate('xpack.siem.chart.dataNotAvailableTitle', {
- defaultMessage: 'Chart Data Not Available',
- })}
-
-
-
-);
-
export interface ChartData {
x: number | string | null;
y: number | string | null;
@@ -136,7 +111,7 @@ export const getTheme = () => {
bottom: 0,
},
scales: {
- barsPadding: 0.5,
+ barsPadding: 0.05,
},
};
const isDarkMode: boolean = chrome.getUiSettingsClient().get(DEFAULT_DARK_MODE);
@@ -166,3 +141,9 @@ export const getChartWidth = (customWidth?: number, autoSizerWidth?: number): st
const height = customWidth || autoSizerWidth;
return height ? `${height}px` : defaultChartWidth;
};
+
+export const checkIfAllValuesAreZero = (data: ChartSeriesData[] | null | undefined): boolean =>
+ Array.isArray(data) &&
+ data.every(series => {
+ return Array.isArray(series.value) && (series.value as ChartData[]).every(({ y }) => y === 0);
+ });
diff --git a/x-pack/legacy/plugins/siem/public/components/charts/translation.ts b/x-pack/legacy/plugins/siem/public/components/charts/translation.ts
new file mode 100644
index 0000000000000..341cb7782f87c
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/charts/translation.ts
@@ -0,0 +1,15 @@
+/*
+ * 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 ALL_VALUES_ZEROS_TITLE = i18n.translate('xpack.siem.chart.dataAllValuesZerosTitle', {
+ defaultMessage: 'All values returned zero',
+});
+
+export const DATA_NOT_AVAILABLE_TITLE = i18n.translate('xpack.siem.chart.dataNotAvailableTitle', {
+ defaultMessage: 'Chart Data Not Available',
+});
diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.tsx
index f95b9e6b3ecf5..2898541a4a3d1 100644
--- a/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.tsx
@@ -53,7 +53,7 @@ const getBarchartConfigs = (from: number, to: number, onBrushEnd: UpdateDateRang
showLegend: true,
theme: {
scales: {
- barsPadding: 0.05,
+ barsPadding: 0.08,
},
chartMargins: {
left: 0,
diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/mock.ts b/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/mock.ts
index bb06926ec08f4..e06bb1477bc7f 100644
--- a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/mock.ts
+++ b/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/mock.ts
@@ -184,12 +184,19 @@ export const mockEnableChartsData = {
{
key: 'uniqueSourcePrivateIps',
color: '#DB1374',
- value: [{ x: 'Src.', y: 383, g: 'uniqueSourcePrivateIps' }],
+ value: [
+ {
+ x: 'Src.',
+ y: 383,
+ g: 'uniqueSourcePrivateIps',
+ y0: 0,
+ },
+ ],
},
{
key: 'uniqueDestinationPrivateIps',
color: '#490092',
- value: [{ x: 'Dest.', y: 18, g: 'uniqueDestinationPrivateIps' }],
+ value: [{ x: 'Dest.', y: 18, g: 'uniqueDestinationPrivateIps', y0: 0 }],
},
],
description: 'Unique private IPs',
diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap
index 9541ad4de043b..7475220b56e77 100644
--- a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap
+++ b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap
@@ -994,6 +994,9 @@ exports[`Stat Items Component rendering kpis with charts it renders the default
},
"customHeight": 74,
"series": Object {
+ "stackAccessors": Array [
+ "y0",
+ ],
"xScaleType": "ordinal",
"yScaleType": "linear",
},
diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/index.tsx b/x-pack/legacy/plugins/siem/public/components/stat_items/index.tsx
index 110d146381709..c206a4d33270b 100644
--- a/x-pack/legacy/plugins/siem/public/components/stat_items/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/stat_items/index.tsx
@@ -103,6 +103,7 @@ export const barchartConfigs = (config?: { onElementClick?: ElementClickListener
series: {
xScaleType: ScaleType.Ordinal,
yScaleType: ScaleType.Linear,
+ stackAccessors: ['y0'],
},
axis: {
xTickFormatter: numberFormatter,
@@ -145,6 +146,7 @@ export const addValueToBarChart = (
x,
y,
g: key,
+ y0: 0,
},
];