[SIEM] Chart enhancement (#47130) (#47318)

* chart styling

* rename variable

* styling for bar chart

* add unit test

* clean up

* fix for code review
This commit is contained in:
Angela Chuang 2019-10-04 15:01:27 +01:00 committed by GitHub
parent 20b15ef565
commit 100865a57e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 553 additions and 441 deletions

View file

@ -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(
<AreaChartWithCustomPrompt height={customHeight} width={customWidth} data={data} />
);
});
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(
<AreaChartWithCustomPrompt height={customHeight} width={customWidth} data={data} />
);
});
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(<AreaChart configs={mockConfig} areaChart={data} />);
});
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 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(<AreaChart configs={mockConfig} areaChart={mockData} />);
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(<AreaChart configs={mockConfig} areaChart={data} />);
});
it(`should render a chart place holder`, () => {
expect(shallowWrapper.find('AutoSizer')).toHaveLength(0);
expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(1);
});
}
);
});

View file

@ -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<AreaSeriesStyle> => {
@ -51,6 +51,17 @@ const getSeriesLineStyle = (): RecursivePartial<AreaSeriesStyle> => {
};
};
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) ? (
<AreaSeries
id={seriesSpecId}
key={seriesKey}
name={series.key.replace('Histogram', '')}
data={series.value}
data={series.value || undefined}
xScaleType={getOr(ScaleType.Linear, 'configs.series.xScaleType', chartConfigs)}
yScaleType={getOr(ScaleType.Linear, 'configs.series.yScaleType', chartConfigs)}
timeZone={browserTimezone}
@ -106,28 +117,6 @@ export const AreaChartBaseComponent = React.memo<{
AreaChartBaseComponent.displayName = 'AreaChartBaseComponent';
export const AreaChartWithCustomPrompt = React.memo<{
data: ChartSeriesData[] | null | undefined;
height: string | null | undefined;
width: string | null | undefined;
configs?: ChartSeriesConfigs | undefined;
}>(({ 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)
) ? (
<AreaChartBaseComponent height={height} width={width} data={data} configs={configs} />
) : (
<ChartHolder height={height} width={width} />
);
});
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) ? (
<AutoSizer detectAnyWindowResize={false} content>
{({ measureRef, content: { height, width } }) => (
<WrappedByAutoSizer innerRef={measureRef} height={getChartHeight(customHeight, height)}>
<AreaChartWithCustomPrompt
<AreaChartBaseComponent
data={areaChart}
height={getChartHeight(customHeight, height)}
width={getChartWidth(customWidth, width)}
@ -149,7 +138,11 @@ export const AreaChart = React.memo<{
)}
</AutoSizer>
) : (
<ChartHolder height={getChartHeight(customHeight)} width={getChartWidth(customWidth)} />
<ChartPlaceHolder
height={getChartHeight(customHeight)}
width={getChartWidth(customWidth)}
data={areaChart}
/>
);
});

View file

@ -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(
<BarChartWithCustomPrompt
height={customHeight}
width={customWidth}
data={mockBarChartData}
/>
);
});
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(
<BarChartWithCustomPrompt height={customHeight} width={customWidth} data={data} />
);
shallowWrapper = shallow(<BarChart configs={mockConfig} barChart={data} />);
});
it('render Chart Holder', () => {
expect(shallowWrapper.find(BarChartBaseComponent)).toHaveLength(0);
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} />);
it(`should render chart`, () => {
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);
expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(0);
});
});
describe.each(chartHolderDataSets)('BarChart with invalid data [%o]', data => {
let shallowWrapper: ShallowWrapper;
beforeAll(() => {
shallowWrapper = shallow(<BarChart configs={mockConfig} barChart={data} />);
});
it(`should render chart holder`, () => {
expect(shallowWrapper.find('AutoSizer')).toHaveLength(0);
expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(1);
});
});

View file

@ -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 ? (
<BarSeries
id={barSeriesSpecId}
key={barSeriesKey}
@ -69,7 +82,7 @@ export const BarChartBaseComponent = React.memo<{
stackAccessors={get('configs.series.stackAccessors', chartConfigs)}
customSeriesColors={getSeriesStyle(barSeriesKey, series.color, seriesType)}
/>
);
) : null;
})}
<Axis
@ -87,37 +100,17 @@ export const BarChartBaseComponent = React.memo<{
BarChartBaseComponent.displayName = 'BarChartBaseComponent';
export const BarChartWithCustomPrompt = React.memo<{
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)
) ? (
<BarChartBaseComponent height={height} width={width} data={data} configs={configs} />
) : (
<ChartHolder height={height} width={width} />
);
});
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) ? (
<AutoSizer detectAnyWindowResize={false} content>
{({ measureRef, content: { height, width } }) => (
<WrappedByAutoSizer innerRef={measureRef} height={getChartHeight(customHeight, height)}>
<BarChartWithCustomPrompt
<BarChartBaseComponent
height={getChartHeight(customHeight, height)}
width={getChartWidth(customWidth, width)}
data={barChart}
@ -127,7 +120,11 @@ export const BarChart = React.memo<{
)}
</AutoSizer>
) : (
<ChartHolder height={getChartHeight(customHeight)} width={getChartWidth(customWidth)} />
<ChartPlaceHolder
height={getChartHeight(customHeight)}
width={getChartWidth(customWidth)}
data={barChart}
/>
);
});

View file

@ -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(<ChartPlaceHolder data={mockDataAllZeros} />);
expect(shallowWrapper.props()).toMatchObject({
height,
width,
});
});
it('should render with given props', () => {
const height = `100px`;
const width = `100px`;
shallowWrapper = shallow(
<ChartPlaceHolder height={height} width={width} data={mockDataAllZeros} />
);
expect(shallowWrapper.props()).toMatchObject({
height,
width,
});
});
it('should render correct wording when all values returned zero', () => {
const height = `100px`;
const width = `100px`;
shallowWrapper = shallow(
<ChartPlaceHolder height={height} width={width} data={mockDataAllZeros} />
);
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(
<ChartPlaceHolder
height={height}
width={width}
data={mockDataUnexpectedValue as ChartSeriesData[]}
/>
);
expect(
shallowWrapper
.find(`[data-test-subj="chartHolderText"]`)
.childAt(0)
.text()
).toEqual('Chart Data Not Available');
});
});

View file

@ -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;
}) => (
<FlexGroup justifyContent="center" alignItems="center" height={height} width={width}>
<EuiFlexItem grow={false}>
<EuiText size="s" textAlign="center" color="subdued" data-test-subj="chartHolderText">
{checkIfAllValuesAreZero(data)
? i18n.ALL_VALUES_ZEROS_TITLE
: i18n.DATA_NOT_AVAILABLE_TITLE}
</EuiText>
</EuiFlexItem>
</FlexGroup>
);

View file

@ -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(<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 />);
@ -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();
});
});
});

View file

@ -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,
ScaleType,
TickFormatter,
SettingSpecProps,
Rotation,
Rendering,
Rotation,
ScaleType,
SettingSpecProps,
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;
}) => (
<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', {
defaultMessage: 'Chart Data Not Available',
})}
</EuiText>
</EuiFlexItem>
</FlexGroup>
);
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);
});

View file

@ -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',
});

View file

@ -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,

View file

@ -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',

View file

@ -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",
},

View file

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