Sparkline: Prevent infinite loop when rendering a sparkline with a single value
This commit is contained in:
@@ -833,11 +833,6 @@
|
||||
"count": 13
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Sparkline/Sparkline.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
|
||||
@@ -2,7 +2,14 @@ import { css, cx } from '@emotion/css';
|
||||
import { isNumber } from 'lodash';
|
||||
import { useId } from 'react';
|
||||
|
||||
import { DisplayValueAlignmentFactors, FieldDisplay, getDisplayProcessor, GrafanaTheme2 } from '@grafana/data';
|
||||
import {
|
||||
DisplayValueAlignmentFactors,
|
||||
FieldDisplay,
|
||||
FieldType,
|
||||
getDisplayProcessor,
|
||||
GrafanaTheme2,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2, useTheme2 } from '../../themes/ThemeContext';
|
||||
@@ -66,6 +73,7 @@ export interface RadialGaugeProps {
|
||||
showScaleLabels?: boolean;
|
||||
/** For data links */
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
timeRange?: TimeRange;
|
||||
}
|
||||
|
||||
export type RadialGradientMode = 'none' | 'auto';
|
||||
@@ -240,6 +248,9 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
}
|
||||
|
||||
if (displayValue.sparkline) {
|
||||
if (props.timeRange && displayValue.sparkline.x?.type === FieldType.time) {
|
||||
displayValue.sparkline.timeRange = props.timeRange;
|
||||
}
|
||||
sparklineElement = (
|
||||
<RadialSparkline
|
||||
sparkline={displayValue.sparkline}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import { PureComponent } from 'react';
|
||||
import { AlignedData, Range } from 'uplot';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import {
|
||||
compareDataFrameStructures,
|
||||
DataFrame,
|
||||
Field,
|
||||
FieldConfig,
|
||||
FieldSparkline,
|
||||
FieldType,
|
||||
getFieldColorModeForField,
|
||||
GrafanaTheme2,
|
||||
nullToValue,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
@@ -35,12 +32,6 @@ export interface SparklineProps extends Themeable2 {
|
||||
sparkline: FieldSparkline;
|
||||
}
|
||||
|
||||
interface State {
|
||||
data: AlignedData;
|
||||
alignedDataFrame: DataFrame;
|
||||
configBuilder: UPlotConfigBuilder;
|
||||
}
|
||||
|
||||
const defaultConfig: GraphFieldConfig = {
|
||||
drawStyle: GraphDrawStyle.Line,
|
||||
showPoints: VisibilityMode.Auto,
|
||||
@@ -48,156 +39,105 @@ const defaultConfig: GraphFieldConfig = {
|
||||
pointSize: 2,
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export class Sparkline extends PureComponent<SparklineProps, State> {
|
||||
constructor(props: SparklineProps) {
|
||||
super(props);
|
||||
const prepareConfig = (sparkline: FieldSparkline, dataFrame: DataFrame, theme: GrafanaTheme2): UPlotConfigBuilder => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
const alignedDataFrame = preparePlotFrame(props.sparkline, props.config);
|
||||
builder.setCursor({
|
||||
show: false,
|
||||
x: false,
|
||||
y: false,
|
||||
});
|
||||
|
||||
this.state = {
|
||||
data: preparePlotData2(alignedDataFrame, getStackingGroups(alignedDataFrame)),
|
||||
alignedDataFrame,
|
||||
configBuilder: this.prepareConfig(alignedDataFrame),
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: SparklineProps, state: State) {
|
||||
const _frame = preparePlotFrame(props.sparkline, props.config);
|
||||
const frame = nullToValue(_frame);
|
||||
if (!frame) {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
data: preparePlotData2(frame, getStackingGroups(frame)),
|
||||
alignedDataFrame: frame,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: SparklineProps, prevState: State) {
|
||||
const { alignedDataFrame } = this.state;
|
||||
|
||||
if (!alignedDataFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
let rebuildConfig = false;
|
||||
|
||||
if (prevProps.sparkline !== this.props.sparkline) {
|
||||
const isStructureChanged = !compareDataFrameStructures(this.state.alignedDataFrame, prevState.alignedDataFrame);
|
||||
const isRangeChanged = !isEqual(
|
||||
alignedDataFrame.fields[1].state?.range,
|
||||
prevState.alignedDataFrame.fields[1].state?.range
|
||||
);
|
||||
rebuildConfig = isStructureChanged || isRangeChanged;
|
||||
} else {
|
||||
rebuildConfig = !isEqual(prevProps.config, this.props.config);
|
||||
}
|
||||
|
||||
if (rebuildConfig) {
|
||||
this.setState({ configBuilder: this.prepareConfig(alignedDataFrame) });
|
||||
}
|
||||
}
|
||||
|
||||
getYRange(field: Field): Range.MinMax {
|
||||
return getYRange(field, this.state.alignedDataFrame);
|
||||
}
|
||||
|
||||
prepareConfig(data: DataFrame) {
|
||||
const { theme } = this.props;
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
builder.setCursor({
|
||||
show: false,
|
||||
x: false, // no crosshairs
|
||||
y: false,
|
||||
});
|
||||
|
||||
// X is the first field in the alligned frame
|
||||
const xField = data.fields[0];
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
isTime: false, //xField.type === FieldType.time,
|
||||
range: () => {
|
||||
const { sparkline } = this.props;
|
||||
if (sparkline.x) {
|
||||
if (sparkline.timeRange && sparkline.x.type === FieldType.time) {
|
||||
return [sparkline.timeRange.from.valueOf(), sparkline.timeRange.to.valueOf()];
|
||||
}
|
||||
const vals = sparkline.x.values;
|
||||
return [vals[0], vals[vals.length - 1]];
|
||||
// X is the first field in the aligned frame
|
||||
const xField = dataFrame.fields[0];
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
isTime: false,
|
||||
range: () => {
|
||||
if (sparkline.x) {
|
||||
if (sparkline.timeRange && sparkline.x.type === FieldType.time) {
|
||||
return [sparkline.timeRange.from.valueOf(), sparkline.timeRange.to.valueOf()];
|
||||
}
|
||||
return [0, sparkline.y.values.length - 1];
|
||||
},
|
||||
const vals = sparkline.x.values;
|
||||
return [vals[0], vals[vals.length - 1]];
|
||||
}
|
||||
return [0, sparkline.y.values.length - 1];
|
||||
},
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
scaleKey: 'x',
|
||||
theme,
|
||||
placement: AxisPlacement.Hidden,
|
||||
});
|
||||
|
||||
for (let i = 0; i < dataFrame.fields.length; i++) {
|
||||
const field = dataFrame.fields[i];
|
||||
const config: FieldConfig<GraphFieldConfig> = field.config;
|
||||
const customConfig: GraphFieldConfig = {
|
||||
...defaultConfig,
|
||||
...config.custom,
|
||||
};
|
||||
|
||||
if (field === xField || field.type !== FieldType.number) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const scaleKey = config.unit || '__fixed';
|
||||
builder.addScale({
|
||||
scaleKey,
|
||||
orientation: ScaleOrientation.Vertical,
|
||||
direction: ScaleDirection.Up,
|
||||
range: () => getYRange(field, dataFrame),
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
scaleKey: 'x',
|
||||
scaleKey,
|
||||
theme,
|
||||
placement: AxisPlacement.Hidden,
|
||||
});
|
||||
|
||||
for (let i = 0; i < data.fields.length; i++) {
|
||||
const field = data.fields[i];
|
||||
const config: FieldConfig<GraphFieldConfig> = field.config;
|
||||
const customConfig: GraphFieldConfig = {
|
||||
...defaultConfig,
|
||||
...config.custom,
|
||||
};
|
||||
const colorMode = getFieldColorModeForField(field);
|
||||
const seriesColor = colorMode.getCalculator(field, theme)(0, 0);
|
||||
const pointsMode =
|
||||
customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints;
|
||||
|
||||
if (field === xField || field.type !== FieldType.number) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const scaleKey = config.unit || '__fixed';
|
||||
builder.addScale({
|
||||
scaleKey,
|
||||
orientation: ScaleOrientation.Vertical,
|
||||
direction: ScaleDirection.Up,
|
||||
range: () => this.getYRange(field),
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
scaleKey,
|
||||
theme,
|
||||
placement: AxisPlacement.Hidden,
|
||||
});
|
||||
|
||||
const colorMode = getFieldColorModeForField(field);
|
||||
const seriesColor = colorMode.getCalculator(field, theme)(0, 0);
|
||||
const pointsMode =
|
||||
customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints;
|
||||
|
||||
builder.addSeries({
|
||||
pxAlign: false,
|
||||
scaleKey,
|
||||
theme,
|
||||
colorMode,
|
||||
thresholds: config.thresholds,
|
||||
drawStyle: customConfig.drawStyle!,
|
||||
lineColor: customConfig.lineColor ?? seriesColor,
|
||||
lineWidth: customConfig.lineWidth,
|
||||
lineInterpolation: customConfig.lineInterpolation,
|
||||
showPoints: pointsMode,
|
||||
pointSize: customConfig.pointSize,
|
||||
fillOpacity: customConfig.fillOpacity,
|
||||
fillColor: customConfig.fillColor,
|
||||
lineStyle: customConfig.lineStyle,
|
||||
gradientMode: customConfig.gradientMode,
|
||||
spanNulls: customConfig.spanNulls,
|
||||
});
|
||||
}
|
||||
|
||||
return builder;
|
||||
builder.addSeries({
|
||||
pxAlign: false,
|
||||
scaleKey,
|
||||
theme,
|
||||
colorMode,
|
||||
thresholds: config.thresholds,
|
||||
drawStyle: customConfig.drawStyle!,
|
||||
lineColor: customConfig.lineColor ?? seriesColor,
|
||||
lineWidth: customConfig.lineWidth,
|
||||
lineInterpolation: customConfig.lineInterpolation,
|
||||
showPoints: pointsMode,
|
||||
pointSize: customConfig.pointSize,
|
||||
fillOpacity: customConfig.fillOpacity,
|
||||
fillColor: customConfig.fillColor,
|
||||
lineStyle: customConfig.lineStyle,
|
||||
gradientMode: customConfig.gradientMode,
|
||||
spanNulls: customConfig.spanNulls,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, configBuilder } = this.state;
|
||||
const { width, height } = this.props;
|
||||
return <UPlotChart data={data} config={configBuilder} width={width} height={height} />;
|
||||
return builder;
|
||||
};
|
||||
|
||||
export const Sparkline: React.FC<SparklineProps> = memo((props) => {
|
||||
const { sparkline, config: fieldConfig, theme, width, height } = props;
|
||||
const alignedDataFrame = nullToValue(preparePlotFrame(sparkline, fieldConfig));
|
||||
// do not render sparklines for fields with 1 or less values - this can cause an infinite loop in uPlot
|
||||
if (alignedDataFrame.fields.some((f) => f.values.length <= 1)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const data = preparePlotData2(alignedDataFrame, getStackingGroups(alignedDataFrame));
|
||||
const configBuilder = prepareConfig(sparkline, alignedDataFrame, theme);
|
||||
|
||||
return <UPlotChart data={data} config={configBuilder} width={width} height={height} />;
|
||||
});
|
||||
Sparkline.displayName = 'Sparkline';
|
||||
|
||||
@@ -9,20 +9,20 @@ import { config } from '@grafana/runtime';
|
||||
import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin';
|
||||
|
||||
export const panelsToCheckFirst = [
|
||||
'timeseries',
|
||||
'barchart',
|
||||
// 'timeseries',
|
||||
// 'barchart',
|
||||
'gauge',
|
||||
'stat',
|
||||
'piechart',
|
||||
'bargauge',
|
||||
'table',
|
||||
'state-timeline',
|
||||
'status-history',
|
||||
'logs',
|
||||
'candlestick',
|
||||
'flamegraph',
|
||||
'traces',
|
||||
'nodeGraph',
|
||||
// 'stat',
|
||||
// 'piechart',
|
||||
// 'bargauge',
|
||||
// 'table',
|
||||
// 'state-timeline',
|
||||
// 'status-history',
|
||||
// 'logs',
|
||||
// 'candlestick',
|
||||
// 'flamegraph',
|
||||
// 'traces',
|
||||
// 'nodeGraph',
|
||||
];
|
||||
|
||||
export async function getAllSuggestions(
|
||||
|
||||
Reference in New Issue
Block a user