[SIEM] Replace Eui chart with elastic charts for siem kpis (#36660)

* move charts to separate components

* replace areachart

* apply custom styles

* customize barchart color

* customize color for areachart

* move reusable functions into common

* exchange x & y value in barchart dataset

* replace pure component with react memo and upgrade enzyme adapter
This commit is contained in:
Angela Chuang 2019-05-29 14:57:38 +08:00 committed by GitHub
parent 90fdb149e0
commit ac3eb8566b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1066 additions and 1231 deletions

View file

@ -349,7 +349,7 @@
"dedent": "^0.7.0",
"delete-empty": "^2.0.0",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.9.0",
"enzyme-adapter-react-16": "^1.10.0",
"enzyme-adapter-utils": "^1.10.0",
"enzyme-to-json": "^3.3.4",
"eslint": "^5.16.0",

View file

@ -115,7 +115,7 @@
"del": "^3.0.0",
"dotenv": "2.0.0",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.9.0",
"enzyme-adapter-react-16": "^1.10.0",
"enzyme-adapter-utils": "^1.10.0",
"enzyme-to-json": "^3.3.4",
"execa": "^1.0.0",

View file

@ -8,7 +8,7 @@ import { mount, ReactWrapper } from 'enzyme';
import * as React from 'react';
import { AreaChartBaseComponent, AreaChartWithCustomPrompt } from './areachart';
import { AreaChartData } from '.';
import { AreaChartData } from './common';
describe('AreaChartBaseComponent', () => {
let wrapper: ReactWrapper;
@ -38,16 +38,8 @@ describe('AreaChartBaseComponent', () => {
wrapper = mount(<AreaChartBaseComponent height={100} width={120} data={mockAreaChartData} />);
});
it('should render two area series', () => {
expect(wrapper.find('EuiAreaSeries')).toHaveLength(2);
});
it('should render a customized x-asix', () => {
expect(wrapper.find('EuiXAxis')).toHaveLength(1);
});
it('should render a customized y-asix', () => {
expect(wrapper.find('EuiYAxis')).toHaveLength(1);
it('should render Chart', () => {
expect(wrapper.find('Chart')).toHaveLength(1);
});
});
@ -59,7 +51,7 @@ describe('AreaChartBaseComponent', () => {
});
it('should not render without height and width', () => {
expect(wrapper.find('SeriesChart')).toHaveLength(0);
expect(wrapper.find('Chart')).toHaveLength(0);
});
});
});
@ -113,7 +105,7 @@ describe('AreaChartWithCustomPrompt', () => {
});
it('render AreaChartBaseComponent', () => {
expect(wrapper.find('[data-test-subj="stat-area-chart"]').first()).toHaveLength(1);
expect(wrapper.find('Chart')).toHaveLength(1);
expect(wrapper.find('ChartHolder')).toHaveLength(0);
});
});
@ -195,7 +187,7 @@ describe('AreaChartWithCustomPrompt', () => {
});
it('render Chart Holder', () => {
expect(wrapper.find('[data-test-subj="stat-area-chart"]')).toHaveLength(0);
expect(wrapper.find('Chart')).toHaveLength(0);
expect(wrapper.find('ChartHolder')).toHaveLength(1);
});
});

View file

@ -0,0 +1,134 @@
/*
* 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 {
Axis,
AreaSeries,
Chart,
getAxisId,
getSpecId,
Position,
ScaleType,
} from '@elastic/charts';
import '@elastic/charts/dist/style.css';
import {
AreaChartData,
ChartHolder,
getSeriesStyle,
numberFormatter,
WrappedByAutoSizer,
} from './common';
import { AutoSizer } from '../auto_sizer';
const dateFormatter = (d: string) => {
return d.toLocaleString().split('T')[0];
};
const getSeriesLineStyle = (color: string | undefined) => {
return color
? {
area: {
fill: color,
opacity: 0.04,
visible: true,
},
line: {
stroke: color,
strokeWidth: 1,
visible: true,
},
border: {
visible: false,
strokeWidth: 1,
stroke: color,
},
point: {
visible: false,
radius: 0.2,
stroke: color,
strokeWidth: 1,
opacity: 1,
},
}
: undefined;
};
export const AreaChartBaseComponent = React.memo<{
data: AreaChartData[];
width: number | null | undefined;
height: number | null | undefined;
}>(({ data, ...chartConfigs }) => {
return chartConfigs.width && chartConfigs.height ? (
<div style={{ height: chartConfigs.height, width: chartConfigs.width, position: 'relative' }}>
<Chart>
{data.map(series => {
const seriesKey = series.key;
const seriesSpecId = getSpecId(seriesKey);
return series.value != null ? (
<AreaSeries
id={seriesSpecId}
key={seriesKey}
name={series.key.replace('Histogram', '')}
data={series.value}
xScaleType={ScaleType.Ordinal}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
areaSeriesStyle={getSeriesLineStyle(series.color)}
customSeriesColors={getSeriesStyle(seriesKey, series.color)}
/>
) : null;
})}
<Axis
id={getAxisId(`group-${data[0].key}-x`)}
position={Position.Bottom}
showOverlappingTicks={false}
tickFormat={dateFormatter}
tickSize={0}
/>
<Axis
id={getAxisId(`group-${data[0].key}-y`)}
position={Position.Left}
tickSize={0}
tickFormat={numberFormatter}
/>
</Chart>
</div>
) : null;
});
export const AreaChartWithCustomPrompt = React.memo<{
data: AreaChartData[] | null | undefined;
height: number | null | undefined;
width: number | null | undefined;
}>(({ data, height, width }) => {
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} />
) : (
<ChartHolder />
);
});
export const AreaChart = React.memo<{ areaChart: AreaChartData[] | null | undefined }>(
({ areaChart }) => (
<AutoSizer detectAnyWindowResize={false} content>
{({ measureRef, content: { height, width } }) => (
<WrappedByAutoSizer innerRef={measureRef}>
<AreaChartWithCustomPrompt data={areaChart} height={height} width={width} />
</WrappedByAutoSizer>
)}
</AutoSizer>
)
);

View file

@ -8,15 +8,19 @@ import { mount, ReactWrapper } from 'enzyme';
import * as React from 'react';
import { BarChartBaseComponent, BarChartWithCustomPrompt } from './barchart';
import { BarChartData } from '.';
import { BarChartData } from './common';
describe('BarChartBaseComponent', () => {
let wrapper: ReactWrapper;
const mockBarChartData: BarChartData[] = [
{ key: 'uniqueSourceIps', value: [{ x: 1714, y: 'uniqueSourceIps' }], color: '#DB1374' },
{
key: 'uniqueSourceIps',
value: [{ y: 1714, x: 'uniqueSourceIps', g: 'uniqueSourceIps' }],
color: '#DB1374',
},
{
key: 'uniqueDestinationIps',
value: [{ x: 2354, y: 'uniqueDestinationIps' }],
value: [{ y: 2354, x: 'uniqueDestinationIps', g: 'uniqueDestinationIps' }],
color: '#490092',
},
];
@ -26,16 +30,8 @@ describe('BarChartBaseComponent', () => {
wrapper = mount(<BarChartBaseComponent height={100} width={120} data={mockBarChartData} />);
});
it('should render two area series', () => {
expect(wrapper.find('EuiBarSeries')).toHaveLength(2);
});
it('should render a customized x-asix', () => {
expect(wrapper.find('EuiXAxis')).toHaveLength(1);
});
it('should render a customized y-asix', () => {
expect(wrapper.find('EuiYAxis')).toHaveLength(1);
it('should render two bar series', () => {
expect(wrapper.find('Chart')).toHaveLength(1);
});
});
@ -45,7 +41,7 @@ describe('BarChartBaseComponent', () => {
});
it('should not render without height and width', () => {
expect(wrapper.find('SeriesChart')).toHaveLength(0);
expect(wrapper.find('Chart')).toHaveLength(0);
});
});
});
@ -53,30 +49,30 @@ describe('BarChartBaseComponent', () => {
describe.each([
[
[
{ key: 'uniqueSourceIps', value: [{ x: 1714, y: 'uniqueSourceIps' }], color: '#DB1374' },
{ key: 'uniqueSourceIps', value: [{ y: 1714, x: 'uniqueSourceIps' }], color: '#DB1374' },
{
key: 'uniqueDestinationIps',
value: [{ x: 2354, y: 'uniqueDestinationIps' }],
value: [{ y: 2354, x: 'uniqueDestinationIps' }],
color: '#490092',
},
],
],
[
[
{ key: 'uniqueSourceIps', value: [{ x: 1714, y: '' }], color: '#DB1374' },
{ key: 'uniqueSourceIps', value: [{ y: 1714, x: '' }], color: '#DB1374' },
{
key: 'uniqueDestinationIps',
value: [{ x: 2354, y: '' }],
value: [{ y: 2354, x: '' }],
color: '#490092',
},
],
],
[
[
{ key: 'uniqueSourceIps', value: [{ x: 1714, y: 'uniqueSourceIps' }], color: '#DB1374' },
{ key: 'uniqueSourceIps', value: [{ y: 1714, x: 'uniqueSourceIps' }], color: '#DB1374' },
{
key: 'uniqueDestinationIps',
value: [{ x: 0, y: 'uniqueDestinationIps' }],
value: [{ y: 0, x: 'uniqueDestinationIps' }],
color: '#490092',
},
],
@ -91,7 +87,7 @@ describe.each([
});
it('render BarChartBaseComponent', () => {
expect(wrapper.find('[data-test-subj="stat-bar-chart"]').first()).toHaveLength(1);
expect(wrapper.find('Chart')).toHaveLength(1);
expect(wrapper.find('ChartHolder')).toHaveLength(0);
});
});
@ -131,30 +127,30 @@ describe.each([
],
[
[
{ key: 'uniqueSourceIps', value: [{ x: 0, y: 'uniqueSourceIps' }], color: '#DB1374' },
{ key: 'uniqueSourceIps', value: [{ y: 0, x: 'uniqueSourceIps' }], color: '#DB1374' },
{
key: 'uniqueDestinationIps',
value: [{ x: 0, y: 'uniqueDestinationIps' }],
value: [{ y: 0, x: 'uniqueDestinationIps' }],
color: '#490092',
},
],
],
[
[
{ key: 'uniqueSourceIps', value: [{ x: null, y: 'uniqueSourceIps' }], color: '#DB1374' },
{ key: 'uniqueSourceIps', value: [{ y: null, x: 'uniqueSourceIps' }], color: '#DB1374' },
{
key: 'uniqueDestinationIps',
value: [{ x: 2354, y: 'uniqueDestinationIps' }],
value: [{ y: 2354, x: 'uniqueDestinationIps' }],
color: '#490092',
},
],
],
[
[
{ key: 'uniqueSourceIps', value: [{ x: null, y: 'uniqueSourceIps' }], color: '#DB1374' },
{ key: 'uniqueSourceIps', value: [{ y: null, x: 'uniqueSourceIps' }], color: '#DB1374' },
{
key: 'uniqueDestinationIps',
value: [{ x: null, y: 'uniqueDestinationIps' }],
value: [{ y: null, x: 'uniqueDestinationIps' }],
color: '#490092',
},
],
@ -166,7 +162,7 @@ describe.each([
});
it('render Chart Holder', () => {
expect(wrapper.find('[data-test-subj="stat-bar-chart"]')).toHaveLength(0);
expect(wrapper.find('Chart')).toHaveLength(0);
expect(wrapper.find('ChartHolder')).toHaveLength(1);
});
});

View file

@ -0,0 +1,111 @@
/*
* 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 {
Chart,
BarSeries,
Axis,
Position,
getSpecId,
ScaleType,
Settings,
mergeWithDefaultTheme,
PartialTheme,
} from '@elastic/charts';
import { getAxisId } from '@elastic/charts';
import {
BarChartData,
WrappedByAutoSizer,
ChartHolder,
numberFormatter,
SeriesType,
getSeriesStyle,
} from './common';
import { AutoSizer } from '../auto_sizer';
const getTheme = () => {
const theme: PartialTheme = {
scales: {
barsPadding: 0.5,
},
};
return mergeWithDefaultTheme(theme);
};
export const BarChartBaseComponent = React.memo<{
data: BarChartData[];
width: number | null | undefined;
height: number | null | undefined;
}>(({ data, ...chartConfigs }) => {
return chartConfigs.width && chartConfigs.height ? (
<Chart>
<Settings rotation={90} theme={getTheme()} />
{data.map(series => {
const barSeriesKey = series.key;
const barSeriesSpecId = getSpecId(barSeriesKey);
const seriesType = SeriesType.BAR;
return (
<BarSeries
id={barSeriesSpecId}
key={barSeriesKey}
name={series.key}
xScaleType={ScaleType.Ordinal}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
splitSeriesAccessors={['g']}
data={series.value!}
stackAccessors={['y']}
customSeriesColors={getSeriesStyle(barSeriesKey, series.color, seriesType)}
/>
);
})}
<Axis
id={getAxisId(`stat-items-barchart-${data[0].key}-x`)}
position={Position.Bottom}
tickSize={0}
tickFormat={numberFormatter}
/>
<Axis
id={getAxisId(`stat-items-barchart-${data[0].key}-y`)}
position={Position.Left}
tickSize={0}
/>
</Chart>
) : null;
});
export const BarChartWithCustomPrompt = React.memo<{
data: BarChartData[] | null | undefined;
height: number | null | undefined;
width: number | null | undefined;
}>(({ data, height, width }) => {
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} />
) : (
<ChartHolder />
);
});
export const BarChart = React.memo<{ barChart: BarChartData[] | null | undefined }>(
({ barChart }) => (
<AutoSizer detectAnyWindowResize={false} content>
{({ measureRef, content: { height, width } }) => (
<WrappedByAutoSizer innerRef={measureRef}>
<BarChartWithCustomPrompt height={height} width={width} data={barChart} />
</WrappedByAutoSizer>
)}
</AutoSizer>
)
);

View file

@ -0,0 +1,81 @@
/*
* 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 { EuiFlexGroup, EuiText, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { CustomSeriesColorsMap, DataSeriesColorsValues, getSpecId } from '@elastic/charts';
import { i18n } from '@kbn/i18n';
const FlexGroup = styled(EuiFlexGroup)`
height: 100%;
`;
export const ChartHolder = () => (
<FlexGroup justifyContent="center" alignItems="center">
<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 AreaChartData {
key: string;
value: ChartData[] | [] | null;
color?: string | undefined;
}
export interface ChartData {
x: number | string | null;
y: number | string | null;
y0?: number;
g?: number | string;
}
export interface BarChartData {
key: string;
value: [ChartData] | [] | null;
color?: string | undefined;
}
export const WrappedByAutoSizer = styled.div`
height: 100px;
position: relative;
&:hover {
z-index: 100;
}
`;
export const numberFormatter = (value: string | number) => {
return value.toLocaleString && value.toLocaleString();
};
export enum SeriesType {
BAR = 'bar',
AREA = 'area',
LINE = 'line',
}
export const getSeriesStyle = (
seriesKey: string,
color: string | undefined,
seriesType?: SeriesType
) => {
if (!color) return undefined;
const customSeriesColors: CustomSeriesColorsMap = new Map();
const dataSeriesColorValues: DataSeriesColorsValues = {
colorValues: seriesType === SeriesType.BAR ? [seriesKey] : [],
specId: getSpecId(seriesKey),
};
customSeriesColors.set(dataSeriesColorValues, color);
return customSeriesColors;
};

View file

@ -7,19 +7,12 @@
import { EuiFlexGroup } from '@elastic/eui';
import { get, getOr } from 'lodash/fp';
import React from 'react';
import { pure } from 'recompose';
import { EuiLoadingSpinner } from '@elastic/eui';
import { EuiFlexItem } from '@elastic/eui';
import { KpiHostsData } from '../../../../graphql/types';
import {
AreaChartData,
BarChartData,
StatItem,
StatItems,
StatItemsComponent,
StatItemsProps,
} from '../../../stat_items';
import { StatItem, StatItems, StatItemsComponent, StatItemsProps } from '../../../stat_items';
import * as i18n from './translations';
import { BarChartData, AreaChartData } from '../../../charts/common';
interface KpiHostsProps {
data: KpiHostsData;
@ -93,7 +86,7 @@ const fieldTitleMapping: StatItems[] = [
},
];
export const KpiHostsComponent = pure<KpiHostsProps>(({ data, loading }) => {
export const KpiHostsComponent = React.memo<KpiHostsProps>(({ data, loading }) => {
return loading ? (
<EuiFlexGroup justifyContent="center" alignItems="center" style={{ minHeight: 247 }}>
<EuiFlexItem grow={false}>
@ -148,8 +141,8 @@ const addValueToBarChart = (fields: StatItem[], data: KpiHostsData): BarChartDat
if (fields.length === 0) return [];
return fields.reduce((acc: BarChartData[], field: StatItem, idx: number) => {
const key: string = get('key', field);
const x: number | null = getOr(null, key, data);
const y: string = get(`${idx}.name`, fields) || getOr('', `${idx}.description`, fields);
const y: number | null = getOr(null, key, data);
const x: string = get(`${idx}.name`, fields) || getOr('', `${idx}.description`, fields);
return acc.concat([
{
@ -158,6 +151,7 @@ const addValueToBarChart = (fields: StatItem[], data: KpiHostsData): BarChartDat
{
x,
y,
g: key,
},
],
},

View file

@ -1,85 +0,0 @@
/*
* 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 { pure } from 'recompose';
import styled from 'styled-components';
import { EuiSeriesChart, EuiAreaSeries, EuiXAxis, EuiYAxis } from '@elastic/eui/lib/experimental';
import { AreaChartData, WrappedByAutoSizer, ChartHolder } from '.';
import { AutoSizer } from '../auto_sizer';
export const AreaChartBaseComponent = pure<{
data: AreaChartData[];
width: number | null | undefined;
height: number | null | undefined;
}>(({ data, ...chartConfigs }) =>
chartConfigs.width && chartConfigs.height ? (
<SeriesChart
{...chartConfigs}
showDefaultAxis={false}
xType="ordinal"
data-test-subj="stat-area-chart"
>
{data.map(series => (
/**
* Placing ts-ignore here for fillOpacity
* */
// @ts-ignore
<EuiAreaSeries
key={`stat-items-areachart-${series.key}`}
name={series.key.replace('Histogram', '')}
// @ts-ignore
data={series.value}
fillOpacity={0.04}
color={series.color}
/>
))}
{/*
// @ts-ignore */}
<EuiXAxis tickFormat={timestamp => timestamp.split('T')[0]} />
{/*
// @ts-ignore */}
<EuiYAxis />
</SeriesChart>
) : null
);
export const AreaChartWithCustomPrompt = pure<{
data: AreaChartData[] | null | undefined;
height: number | null | undefined;
width: number | null | undefined;
}>(({ data, height, width }) => {
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} />
) : (
<ChartHolder />
);
});
export const AreaChart = pure<{ areaChart: AreaChartData[] | null | undefined }>(
({ areaChart }) => (
<AutoSizer detectAnyWindowResize={false} content>
{({ measureRef, content: { height, width } }) => (
<WrappedByAutoSizer innerRef={measureRef}>
<AreaChartWithCustomPrompt data={areaChart} height={height} width={width} />
</WrappedByAutoSizer>
)}
</AutoSizer>
)
);
const SeriesChart = styled(EuiSeriesChart)`
svg .rv-xy-plot__axis__ticks .rv-xy-plot__axis__tick:not(:first-child):not(:last-child) {
display: none;
}
`;

View file

@ -1,94 +0,0 @@
/*
* 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 {
// @ts-ignore
EuiSeriesChartUtils,
} from '@elastic/eui';
import { pure } from 'recompose';
import styled from 'styled-components';
import { EuiSeriesChart, EuiBarSeries, EuiXAxis, EuiYAxis } from '@elastic/eui/lib/experimental';
import { BarChartData, WrappedByAutoSizer, ChartHolder } from '.';
import { AutoSizer } from '../auto_sizer';
const { SCALE, ORIENTATION } = EuiSeriesChartUtils;
const getYaxis = (value: string | number) => {
const label = value.toString();
const labelLength = 4;
return label.length > labelLength ? `${label.slice(0, labelLength)}.` : label;
};
export const BarChartBaseComponent = pure<{
data: BarChartData[];
width: number | null | undefined;
height: number | null | undefined;
}>(({ data, ...chartConfigs }) => {
return chartConfigs.width && chartConfigs.height ? (
// @ts-ignore
<SeriesChart
yType={SCALE.ORDINAL}
orientation={ORIENTATION.HORIZONTAL}
showDefaultAxis={false}
data-test-subj="stat-bar-chart"
{...chartConfigs}
>
{data.map(series => {
return (
<EuiBarSeries
key={`stat-items-areachart-${series.key}`}
name={series.key}
// @ts-ignore
data={series.value!}
color={series.color}
/>
);
})}
{/*
// @ts-ignore */}
<EuiXAxis />
{/*
// @ts-ignore */}
<EuiYAxis tickFormat={getYaxis} />
</SeriesChart>
) : null;
});
export const BarChartWithCustomPrompt = pure<{
data: BarChartData[] | null | undefined;
height: number | null | undefined;
width: number | null | undefined;
}>(({ data, height, width }) => {
return data &&
data.length &&
data.some(
({ value }) =>
value != null && value.length > 0 && value.every(chart => chart.x != null && chart.x > 0)
) ? (
<BarChartBaseComponent height={height} width={width} data={data} />
) : (
<ChartHolder />
);
});
export const BarChart = pure<{ barChart: BarChartData[] | null | undefined }>(({ barChart }) => (
<AutoSizer detectAnyWindowResize={false} content>
{({ measureRef, content: { height, width } }) => (
<WrappedByAutoSizer innerRef={measureRef}>
<BarChartWithCustomPrompt height={height} width={width} data={barChart} />
</WrappedByAutoSizer>
)}
</AutoSizer>
));
const SeriesChart = styled(EuiSeriesChart)`
svg
.rv-xy-plot__axis--horizontal
.rv-xy-plot__axis__ticks
.rv-xy-plot__axis__tick:not(:first-child):not(:last-child) {
display: none;
}
`;

View file

@ -9,8 +9,8 @@ import toJson from 'enzyme-to-json';
import * as React from 'react';
import { StatItemsComponent, StatItemsProps } from '.';
import { BarChart } from './barchart';
import { AreaChart } from './areachart';
import { BarChart } from '../charts/barchart';
import { AreaChart } from '../charts/areachart';
import { EuiHorizontalRule } from '@elastic/eui';
describe('Stat Items', () => {

View file

@ -11,28 +11,15 @@ import {
EuiHorizontalRule,
EuiIcon,
EuiTitle,
IconType,
} from '@elastic/eui';
import React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
import { EuiText } from '@elastic/eui';
import { BarChart } from './barchart';
import { AreaChart } from './areachart';
import { BarChart } from '../charts/barchart';
import { AreaChart } from '../charts/areachart';
import { getEmptyTagValue } from '../empty_value';
export const WrappedByAutoSizer = styled.div`
height: 100px;
position: relative;
&:hover {
z-index: 100;
}
`;
const FlexGroup = styled(EuiFlexGroup)`
height: 100%;
`;
import { AreaChartData, BarChartData } from '../charts/common';
const FlexItem = styled(EuiFlexItem)`
min-width: 0;
@ -49,28 +36,10 @@ export interface StatItem {
description?: string;
value: number | undefined | null;
color?: string;
icon?: 'storage' | 'cross' | 'check' | 'visMapCoordinate';
icon?: IconType;
name?: string;
}
export interface AreaChartData {
key: string;
value: ChartData[] | [] | null;
color?: string | undefined;
}
export interface ChartData {
x: number | string | null;
y: number | string | null;
y0?: number;
}
export interface BarChartData {
key: string;
value: [ChartData] | [] | null;
color?: string | undefined;
}
export interface StatItems {
fields: StatItem[];
description?: string;
@ -85,7 +54,7 @@ export interface StatItemsProps extends StatItems {
barChart?: BarChartData[];
}
export const StatItemsComponent = pure<StatItemsProps>(
export const StatItemsComponent = React.memo<StatItemsProps>(
({ fields, description, key, grow, barChart, areaChart, enableAreaChart, enableBarChart }) => {
const isBarChartDataAbailable =
barChart &&
@ -150,13 +119,3 @@ export const StatItemsComponent = pure<StatItemsProps>(
);
}
);
export const ChartHolder = () => (
<FlexGroup justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
<EuiText size="s" textAlign="center" color="subdued">
Chart Data Not Available
</EuiText>
</EuiFlexItem>
</FlexGroup>
);

View file

@ -17,7 +17,7 @@ import { createFilter } from '../helpers';
import { QueryTemplateProps } from '../query_template';
import { kpiHostsQuery } from './index.gql_query';
import { ChartData } from '../../components/stat_items';
import { ChartData } from '../../components/charts/common';
export interface KpiHostsArgs {
id: string;

120
yarn.lock
View file

@ -4660,6 +4660,22 @@ aggregate-error@^1.0.0:
string.prototype.padstart "^3.0.0"
symbol.prototype.description "^1.0.0"
airbnb-prop-types@^2.13.2:
version "2.13.2"
resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.13.2.tgz#43147a5062dd2a4a5600e748a47b64004cc5f7fc"
integrity sha512-2FN6DlHr6JCSxPPi25EnqGaXC4OC3/B3k1lCd6MMYrZ51/Gf/1qDfaR+JElzWa+Tl7cY2aYOlsYJGFeQyVHIeQ==
dependencies:
array.prototype.find "^2.0.4"
function.prototype.name "^1.1.0"
has "^1.0.3"
is-regex "^1.0.4"
object-is "^1.0.1"
object.assign "^4.1.0"
object.entries "^1.1.0"
prop-types "^15.7.2"
prop-types-exact "^1.2.0"
react-is "^16.8.6"
ajv-errors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59"
@ -5403,6 +5419,14 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
array.prototype.find@^2.0.4:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.0.tgz#630f2eaf70a39e608ac3573e45cf8ccd0ede9ad7"
integrity sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.13.0"
array.prototype.flat@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4"
@ -10391,18 +10415,19 @@ env-variable@0.0.x:
resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.5.tgz#913dd830bef11e96a039c038d4130604eba37f88"
integrity sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==
enzyme-adapter-react-16@^1.9.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.9.1.tgz#6d49a3a31c3a0fccf527610f31b837e0f307128a"
integrity sha512-Egzogv1y77DUxdnq/CyHxLHaNxmSSKDDSDNNB/EiAXCZVFXdFibaNy2uUuRQ1n24T2m6KH/1Rw16XDRq+1yVEg==
enzyme-adapter-react-16@^1.10.0:
version "1.13.2"
resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.13.2.tgz#8a574d7cbbef7ef0cab2022e9bfc12aeaebb7ae5"
integrity sha512-h0neTuAAFfQUgEZ+PPHVIMDFJ9+CGafI8AjojNlSVh4Fd1pLDgtl2OeVkm4yKF7RSgzrPAwugq4JW8Jjo2iRJA==
dependencies:
enzyme-adapter-utils "^1.10.0"
function.prototype.name "^1.1.0"
enzyme-adapter-utils "^1.12.0"
has "^1.0.3"
object.assign "^4.1.0"
object.values "^1.1.0"
prop-types "^15.6.2"
react-is "^16.7.0"
prop-types "^15.7.2"
react-is "^16.8.6"
react-test-renderer "^16.0.0-0"
semver "^5.7.0"
enzyme-adapter-utils@^1.10.0:
version "1.10.0"
@ -10415,6 +10440,18 @@ enzyme-adapter-utils@^1.10.0:
prop-types "^15.6.2"
semver "^5.6.0"
enzyme-adapter-utils@^1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.0.tgz#96e3730d76b872f593e54ce1c51fa3a451422d93"
integrity sha512-wkZvE0VxcFx/8ZsBw0iAbk3gR1d9hK447ebnSYBf95+r32ezBq+XDSAvRErkc4LZosgH8J7et7H7/7CtUuQfBA==
dependencies:
airbnb-prop-types "^2.13.2"
function.prototype.name "^1.1.0"
object.assign "^4.1.0"
object.fromentries "^2.0.0"
prop-types "^15.7.2"
semver "^5.6.0"
enzyme-to-json@^3.3.4:
version "3.3.5"
resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.5.tgz#f8eb82bd3d5941c9d8bc6fd9140030777d17d0af"
@ -10474,7 +10511,7 @@ error@7.0.2, error@^7.0.0, error@^7.0.2:
string-template "~0.2.1"
xtend "~4.0.0"
es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.5.0:
es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.5.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==
@ -19294,9 +19331,9 @@ object-is@^1.0.1:
integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=
object-keys@^1.0.11, object-keys@^1.0.12:
version "1.1.0"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.0.tgz#11bd22348dd2e096a045ab06f6c85bcc340fa032"
integrity sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
object-keys@^1.0.6:
version "1.0.11"
@ -19345,7 +19382,7 @@ object.defaults@^1.1.0:
for-own "^1.0.0"
isobject "^3.0.0"
object.entries@^1.0.4:
object.entries@^1.0.4, object.entries@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519"
integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==
@ -20832,6 +20869,15 @@ prompts@^2.0.1:
kleur "^3.0.2"
sisteransi "^1.0.0"
prop-types-exact@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==
dependencies:
has "^1.0.3"
object.assign "^4.1.0"
reflect.ownkeys "^0.2.0"
prop-types@15.5.8:
version "15.5.8"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394"
@ -21712,15 +21758,10 @@ react-is@^16.3.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
integrity sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ==
react-is@^16.7.0:
version "16.7.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa"
integrity sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g==
react-is@^16.8.1, react-is@^16.8.2:
version "16.8.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.2.tgz#09891d324cad1cb0c1f2d91f70a71a4bee34df0f"
integrity sha512-D+NxhSR2HUCjYky1q1DwpNUD44cDpUXzSmmFyC3ug1bClcU/iDNy0YNn1iwme28fn+NFhpA13IndOd42CrFb+Q==
react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.2, react-is@^16.8.6:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
react-is@^16.8.4:
version "16.8.5"
@ -22040,7 +22081,17 @@ react-syntax-highlighter@^8.0.1:
prismjs "^1.8.4"
refractor "^2.4.1"
react-test-renderer@^16.0.0-0, react-test-renderer@^16.8.0:
react-test-renderer@^16.0.0-0:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1"
integrity sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw==
dependencies:
object-assign "^4.1.1"
prop-types "^15.6.2"
react-is "^16.8.6"
scheduler "^0.13.6"
react-test-renderer@^16.8.0:
version "16.8.2"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.2.tgz#3ce0bf12aa211116612fda01a886d6163c9c459b"
integrity sha512-gsd4NoOaYrZD2R8zi+CBV9wTGMsGhE2bRe4wvenGy0WcLJgdPscRZDDz+kmLjY+/5XpYC8yRR/v4CScgYfGyoQ==
@ -22536,6 +22587,11 @@ redux@^4.0.1:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
reflect.ownkeys@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
refractor@^2.4.1:
version "2.8.0"
resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.8.0.tgz#29d7b2254e823edd2e3e476af286af1c11472bfa"
@ -23519,15 +23575,7 @@ sax@>=0.6.0, sax@^1.2.1, sax@^1.2.4, sax@~1.2.1, sax@~1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
scheduler@^0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.2.tgz#969eaee2764a51d2e97b20a60963b2546beff8fa"
integrity sha512-qK5P8tHS7vdEMCW5IPyt8v9MJOHqTrOUgPXib7tqm9vh834ibBX5BNhwkplX/0iOzHW5sXyluehYfS9yrkz9+w==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.13.3, scheduler@^0.13.6:
scheduler@^0.13.2, scheduler@^0.13.3, scheduler@^0.13.6:
version "0.13.6"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==
@ -23670,10 +23718,10 @@ semver@^5.5.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477"
integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==
semver@^5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
semver@^5.6.0, semver@^5.7.0:
version "5.7.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
semver@~5.3.0:
version "5.3.0"