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, }, ];