Skip to content

Commit 0ae85e5

Browse files
committed
[Infra UI] Replace EUI Charts with Elastic Charts on node detail page (elastic#41262)
* Remove EUICharts from node detail replace with Elastic Charts * Moving stream check inside onChangeTimerange check * Fixing typo * Adding error message back in * Removing exception from getMaxMinTimestamp() * Checking for valid data * Adjusting i18n names
1 parent c321764 commit 0ae85e5

File tree

7 files changed

+328
-230
lines changed

7 files changed

+328
-230
lines changed

x-pack/legacy/plugins/infra/public/components/metrics/sections/chart_section.tsx

Lines changed: 98 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -3,251 +3,122 @@
33
* or more contributor license agreements. Licensed under the Elastic License;
44
* you may not use this file except in compliance with the Elastic License.
55
*/
6-
import { EuiIcon, EuiPageContentBody, EuiTitle } from '@elastic/eui';
7-
import {
8-
EuiAreaSeries,
9-
EuiBarSeries,
10-
EuiCrosshairX,
11-
EuiDataPoint,
12-
EuiLineSeries,
13-
EuiSeriesChart,
14-
EuiSeriesChartProps,
15-
EuiSeriesProps,
16-
EuiXAxis,
17-
EuiYAxis,
18-
} from '@elastic/eui/lib/experimental';
6+
import React, { useCallback } from 'react';
197
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
20-
import Color from 'color';
218
import { get } from 'lodash';
22-
import moment from 'moment';
23-
import React, { ReactText } from 'react';
24-
import { InfraDataSeries, InfraMetricData, InfraTimerangeInput } from '../../../graphql/types';
25-
import { InfraFormatter, InfraFormatterType } from '../../../lib/lib';
9+
import { Axis, Chart, getAxisId, niceTimeFormatter, Position, Settings } from '@elastic/charts';
10+
import { EuiPageContentBody, EuiTitle } from '@elastic/eui';
11+
import { InfraMetricLayoutSection } from '../../../pages/metrics/layouts/types';
12+
import { InfraMetricData, InfraTimerangeInput } from '../../../graphql/types';
13+
import { getChartTheme } from '../../metrics_explorer/helpers/get_chart_theme';
14+
import { InfraFormatterType } from '../../../lib/lib';
15+
import { SeriesChart } from './series_chart';
2616
import {
27-
InfraMetricLayoutSection,
28-
InfraMetricLayoutVisualizationType,
29-
} from '../../../pages/metrics/layouts/types';
30-
import { createFormatter } from '../../../utils/formatters';
31-
32-
const MARGIN_LEFT = 60;
33-
34-
const chartComponentsByType = {
35-
[InfraMetricLayoutVisualizationType.line]: EuiLineSeries,
36-
[InfraMetricLayoutVisualizationType.area]: EuiAreaSeries,
37-
[InfraMetricLayoutVisualizationType.bar]: EuiBarSeries,
38-
};
17+
getFormatter,
18+
getMaxMinTimestamp,
19+
getChartName,
20+
getChartColor,
21+
getChartType,
22+
seriesHasLessThen2DataPoints,
23+
} from './helpers';
24+
import { ErrorMessage } from './error_message';
3925

4026
interface Props {
4127
section: InfraMetricLayoutSection;
4228
metric: InfraMetricData;
4329
onChangeRangeTime?: (time: InfraTimerangeInput) => void;
44-
crosshairValue?: number;
45-
onCrosshairUpdate?: (crosshairValue: number) => void;
4630
isLiveStreaming?: boolean;
4731
stopLiveStreaming?: () => void;
4832
intl: InjectedIntl;
4933
}
5034

51-
const isInfraMetricLayoutVisualizationType = (
52-
subject: any
53-
): subject is InfraMetricLayoutVisualizationType => {
54-
return InfraMetricLayoutVisualizationType[subject] != null;
55-
};
56-
57-
const getChartName = (section: InfraMetricLayoutSection, seriesId: string) => {
58-
return get(section, ['visConfig', 'seriesOverrides', seriesId, 'name'], seriesId);
59-
};
60-
61-
const getChartColor = (section: InfraMetricLayoutSection, seriesId: string): string | undefined => {
62-
const color = new Color(
63-
get(section, ['visConfig', 'seriesOverrides', seriesId, 'color'], '#999')
64-
);
65-
return color.hex().toString();
66-
};
67-
68-
const getChartType = (section: InfraMetricLayoutSection, seriesId: string) => {
69-
const value = get(section, ['visConfig', 'type']);
70-
const overrideValue = get(section, ['visConfig', 'seriesOverrides', seriesId, 'type']);
71-
if (isInfraMetricLayoutVisualizationType(overrideValue)) {
72-
return overrideValue;
73-
}
74-
if (isInfraMetricLayoutVisualizationType(value)) {
75-
return value;
76-
}
77-
return InfraMetricLayoutVisualizationType.line;
78-
};
79-
80-
const getFormatter = (formatter: InfraFormatterType, formatterTemplate: string) => (
81-
val: ReactText
82-
) => {
83-
if (val == null) {
84-
return '';
85-
}
86-
return createFormatter(formatter, formatterTemplate)(val);
87-
};
88-
89-
const titleFormatter = (dataPoints: EuiDataPoint[]) => {
90-
if (dataPoints.length > 0) {
91-
const [firstDataPoint] = dataPoints;
92-
const { originalValues } = firstDataPoint;
93-
return {
94-
title: <EuiIcon type="clock" />,
95-
value: moment(originalValues.x).format('lll'),
96-
};
97-
}
98-
};
99-
100-
const createItemsFormatter = (
101-
formatter: InfraFormatter,
102-
labels: string[],
103-
seriesColors: string[]
104-
) => (dataPoints: EuiDataPoint[]) => {
105-
return dataPoints.map(d => {
106-
return {
107-
title: (
108-
<span>
109-
<EuiIcon type="dot" style={{ color: seriesColors[d.seriesIndex] }} />
110-
{labels[d.seriesIndex]}
111-
</span>
112-
),
113-
value: formatter(d.y),
114-
};
115-
});
116-
};
117-
118-
const seriesHasLessThen2DataPoints = (series: InfraDataSeries): boolean => {
119-
return series.data.length < 2;
120-
};
121-
12235
export const ChartSection = injectI18n(
123-
class extends React.PureComponent<Props> {
124-
public static displayName = 'ChartSection';
125-
public render() {
126-
const { crosshairValue, section, metric, onCrosshairUpdate, intl } = this.props;
127-
const { visConfig } = section;
128-
const crossHairProps = {
129-
crosshairValue,
130-
onCrosshairUpdate,
131-
};
132-
const chartProps: EuiSeriesChartProps = {
133-
xType: 'time',
134-
showCrosshair: false,
135-
showDefaultAxis: false,
136-
enableSelectionBrush: true,
137-
onSelectionBrushEnd: this.handleSelectionBrushEnd,
138-
};
139-
const stacked = visConfig && visConfig.stacked;
140-
if (stacked) {
141-
chartProps.stackBy = 'y';
142-
}
143-
const bounds = visConfig && visConfig.bounds;
144-
if (bounds) {
145-
chartProps.yDomain = [bounds.min, bounds.max];
146-
}
147-
if (!metric) {
148-
chartProps.statusText = intl.formatMessage({
149-
id: 'xpack.infra.chartSection.missingMetricDataText',
150-
defaultMessage: 'Missing data',
151-
});
152-
}
153-
if (metric.series.some(seriesHasLessThen2DataPoints)) {
154-
chartProps.statusText = intl.formatMessage({
155-
id: 'xpack.infra.chartSection.notEnoughDataPointsToRenderText',
156-
defaultMessage: 'Not enough data points to render chart, try increasing the time range.',
157-
});
158-
}
159-
const formatter = get(visConfig, 'formatter', InfraFormatterType.number);
160-
const formatterTemplate = get(visConfig, 'formatterTemplate', '{{value}}');
161-
const formatterFunction = getFormatter(formatter, formatterTemplate);
162-
const seriesLabels = get(metric, 'series', [] as InfraDataSeries[]).map(s =>
163-
getChartName(section, s.id)
164-
);
165-
const seriesColors = get(metric, 'series', [] as InfraDataSeries[]).map(
166-
s => getChartColor(section, s.id) || ''
36+
({ onChangeRangeTime, section, metric, intl, stopLiveStreaming, isLiveStreaming }: Props) => {
37+
const { visConfig } = section;
38+
const formatter = get(visConfig, 'formatter', InfraFormatterType.number);
39+
const formatterTemplate = get(visConfig, 'formatterTemplate', '{{value}}');
40+
const valueFormatter = useCallback(getFormatter(formatter, formatterTemplate), [
41+
formatter,
42+
formatterTemplate,
43+
]);
44+
const dateFormatter = useCallback(niceTimeFormatter(getMaxMinTimestamp(metric)), [metric]);
45+
const handleTimeChange = useCallback(
46+
(from: number, to: number) => {
47+
if (onChangeRangeTime) {
48+
if (isLiveStreaming && stopLiveStreaming) {
49+
stopLiveStreaming();
50+
}
51+
onChangeRangeTime({
52+
from,
53+
to,
54+
interval: '>=1m',
55+
});
56+
}
57+
},
58+
[onChangeRangeTime, isLiveStreaming, stopLiveStreaming]
59+
);
60+
61+
if (!metric) {
62+
return (
63+
<ErrorMessage
64+
title={intl.formatMessage({
65+
id: 'xpack.infra.chartSection.missingMetricDataText',
66+
defaultMessage: 'Missing Data',
67+
})}
68+
body={intl.formatMessage({
69+
id: 'xpack.infra.chartSection.missingMetricDataBody',
70+
defaultMessage: 'The data for this chart is missing.',
71+
})}
72+
/>
16773
);
168-
const itemsFormatter = createItemsFormatter(formatterFunction, seriesLabels, seriesColors);
74+
}
75+
76+
if (metric.series.some(seriesHasLessThen2DataPoints)) {
16977
return (
170-
<EuiPageContentBody>
171-
<EuiTitle size="xs">
172-
<h3 id={section.id}>{section.label}</h3>
173-
</EuiTitle>
174-
<div style={{ height: 200 }}>
175-
<EuiSeriesChart {...chartProps}>
176-
<EuiXAxis marginLeft={MARGIN_LEFT} />
177-
<EuiYAxis tickFormat={formatterFunction} marginLeft={MARGIN_LEFT} />
178-
<EuiCrosshairX
179-
marginLeft={MARGIN_LEFT}
180-
seriesNames={seriesLabels}
181-
itemsFormat={itemsFormatter}
182-
titleFormat={titleFormatter}
183-
{...crossHairProps}
184-
/>
185-
{metric &&
186-
metric.series.map(series => {
187-
if (!series || series.data.length < 2) {
188-
return null;
189-
}
190-
const data = series.data.map(d => {
191-
return { x: d.timestamp, y: d.value || 0, y0: 0 };
192-
});
193-
const chartType = getChartType(section, series.id);
194-
const name = getChartName(section, series.id);
195-
const seriesProps: EuiSeriesProps = {
196-
data,
197-
name,
198-
lineSize: 2,
199-
};
200-
const color = getChartColor(section, series.id);
201-
if (color) {
202-
seriesProps.color = color;
203-
}
204-
const EuiChartComponent = chartComponentsByType[chartType];
205-
return (
206-
<EuiChartComponent
207-
key={`${section.id}-${series.id}`}
208-
{...seriesProps}
209-
marginLeft={MARGIN_LEFT}
210-
/>
211-
);
212-
})}
213-
</EuiSeriesChart>
214-
</div>
215-
</EuiPageContentBody>
78+
<ErrorMessage
79+
title={intl.formatMessage({
80+
id: 'xpack.infra.chartSection.notEnoughDataPointsToRenderTitle',
81+
defaultMessage: 'Not Enough Data',
82+
})}
83+
body={intl.formatMessage({
84+
id: 'xpack.infra.chartSection.notEnoughDataPointsToRenderText',
85+
defaultMessage:
86+
'Not enough data points to render chart, try increasing the time range.',
87+
})}
88+
/>
21689
);
21790
}
21891

219-
private handleSelectionBrushEnd = (area: Area) => {
220-
const { onChangeRangeTime, isLiveStreaming, stopLiveStreaming } = this.props;
221-
const { startX, endX } = area.domainArea;
222-
if (onChangeRangeTime) {
223-
if (isLiveStreaming && stopLiveStreaming) {
224-
stopLiveStreaming();
225-
}
226-
onChangeRangeTime({
227-
to: endX.valueOf(),
228-
from: startX.valueOf(),
229-
interval: '>=1m',
230-
});
231-
}
232-
};
92+
return (
93+
<EuiPageContentBody>
94+
<EuiTitle size="xs">
95+
<h3 id={section.id}>{section.label}</h3>
96+
</EuiTitle>
97+
<div style={{ height: 200 }}>
98+
<Chart>
99+
<Axis
100+
id={getAxisId('timestamp')}
101+
position={Position.Bottom}
102+
showOverlappingTicks={true}
103+
tickFormat={dateFormatter}
104+
/>
105+
<Axis id={getAxisId('values')} position={Position.Left} tickFormat={valueFormatter} />
106+
{metric &&
107+
metric.series.map(series => (
108+
<SeriesChart
109+
key={`series-${section.id}-${series.id}`}
110+
id={`series-${section.id}-${series.id}`}
111+
series={series}
112+
name={getChartName(section, series.id)}
113+
type={getChartType(section, series.id)}
114+
color={getChartColor(section, series.id)}
115+
stack={visConfig.stacked}
116+
/>
117+
))}
118+
<Settings onBrushEnd={handleTimeChange} theme={getChartTheme()} />
119+
</Chart>
120+
</div>
121+
</EuiPageContentBody>
122+
);
233123
}
234124
);
235-
236-
interface DomainArea {
237-
startX: moment.Moment;
238-
endX: moment.Moment;
239-
startY: number;
240-
endY: number;
241-
}
242-
243-
interface DrawArea {
244-
x0: number;
245-
x1: number;
246-
y0: number;
247-
y1: number;
248-
}
249-
250-
interface Area {
251-
domainArea: DomainArea;
252-
drawArea: DrawArea;
253-
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
import React from 'react';
7+
import { EuiEmptyPrompt } from '@elastic/eui';
8+
9+
interface Props {
10+
title: string;
11+
body: string;
12+
}
13+
14+
export const ErrorMessage = ({ title, body }: Props) => (
15+
<EuiEmptyPrompt iconType="stats" title={<h3>{title}</h3>} body={<p>{body}</p>} />
16+
);

0 commit comments

Comments
 (0)