mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 13:12:14 +08:00

* feat(grafana-ui): introduce development exports to prevent importing from grafana/ui/src * refactor(theme-generation): move theme templates into scripts so themes continue to build * refactor(frontend): replace grafana/ui paths that use nested src with /internal or /unstable * chore(betterer): update better results file * feat(grafana-ui): support enterprise, remove Text component from internal * docs(packages): update readme with exporting code conventions
268 lines
8.5 KiB
TypeScript
268 lines
8.5 KiB
TypeScript
import { Component } from 'react';
|
|
import * as React from 'react';
|
|
import uPlot, { AlignedData } from 'uplot';
|
|
|
|
import {
|
|
DataFrame,
|
|
DataLinkPostProcessor,
|
|
Field,
|
|
FieldMatcherID,
|
|
fieldMatchers,
|
|
FieldType,
|
|
getLinksSupplier,
|
|
InterpolateFunction,
|
|
TimeRange,
|
|
TimeZone,
|
|
} from '@grafana/data';
|
|
import { DashboardCursorSync, VizLegendOptions } from '@grafana/schema';
|
|
import { Themeable2, VizLayout } from '@grafana/ui';
|
|
import { UPlotChart, AxisProps, Renderers, UPlotConfigBuilder, ScaleProps, pluginLog } from '@grafana/ui/internal';
|
|
|
|
import { GraphNGLegendEvent, XYFieldMatchers } from './types';
|
|
import { preparePlotFrame as defaultPreparePlotFrame } from './utils';
|
|
|
|
/**
|
|
* @internal -- not a public API
|
|
*/
|
|
export type PropDiffFn<T extends Record<string, unknown> = {}> = (prev: T, next: T) => boolean;
|
|
|
|
export interface GraphNGProps extends Themeable2 {
|
|
frames: DataFrame[];
|
|
structureRev?: number; // a number that will change when the frames[] structure changes
|
|
width: number;
|
|
height: number;
|
|
timeRange: TimeRange;
|
|
timeZone: TimeZone[] | TimeZone;
|
|
legend: VizLegendOptions;
|
|
fields?: XYFieldMatchers; // default will assume timeseries data
|
|
renderers?: Renderers;
|
|
tweakScale?: (opts: ScaleProps, forField: Field) => ScaleProps;
|
|
tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps;
|
|
onLegendClick?: (event: GraphNGLegendEvent) => void;
|
|
children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode;
|
|
prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder;
|
|
propsToDiff?: Array<string | PropDiffFn>;
|
|
preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null;
|
|
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null;
|
|
replaceVariables: InterpolateFunction;
|
|
dataLinkPostProcessor?: DataLinkPostProcessor;
|
|
cursorSync?: DashboardCursorSync;
|
|
|
|
// Remove fields that are hidden from the visualization before rendering
|
|
// The fields will still be available for other things like data links
|
|
// this is a temporary hack that only works when:
|
|
// 1. renderLegend (above) does not render <PlotLegend>
|
|
// 2. does not have legend series toggle
|
|
// 3. passes through all fields required for link/action gen (including those with hideFrom.viz)
|
|
omitHideFromViz?: boolean;
|
|
|
|
/**
|
|
* needed for propsToDiff to re-init the plot & config
|
|
* this is a generic approach to plot re-init, without having to specify which panel-level options
|
|
* should cause invalidation. we can drop this in favor of something like panelOptionsRev that gets passed in
|
|
* similar to structureRev. then we can drop propsToDiff entirely.
|
|
*/
|
|
options?: Record<string, any>;
|
|
}
|
|
|
|
function sameProps<T extends Record<string, unknown>>(
|
|
prevProps: T,
|
|
nextProps: T,
|
|
propsToDiff: Array<string | PropDiffFn> = []
|
|
) {
|
|
for (const propName of propsToDiff) {
|
|
if (typeof propName === 'function') {
|
|
if (!propName(prevProps, nextProps)) {
|
|
return false;
|
|
}
|
|
} else if (nextProps[propName] !== prevProps[propName]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @internal -- not a public API
|
|
*/
|
|
export interface GraphNGState {
|
|
alignedFrame: DataFrame;
|
|
alignedData?: AlignedData;
|
|
config?: UPlotConfigBuilder;
|
|
}
|
|
|
|
const defaultMatchers = {
|
|
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
|
y: fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])),
|
|
};
|
|
|
|
/**
|
|
* "Time as X" core component, expects ascending x
|
|
*/
|
|
export class GraphNG extends Component<GraphNGProps, GraphNGState> {
|
|
private plotInstance: React.RefObject<uPlot>;
|
|
|
|
constructor(props: GraphNGProps) {
|
|
super(props);
|
|
let state = this.prepState(props);
|
|
state.alignedData = state.config!.prepData!([state.alignedFrame]) as AlignedData;
|
|
this.state = state;
|
|
this.plotInstance = React.createRef();
|
|
}
|
|
|
|
getTimeRange = () => this.props.timeRange;
|
|
|
|
prepState(props: GraphNGProps, withConfig = true) {
|
|
let state: GraphNGState = null as any;
|
|
|
|
const { frames, fields = defaultMatchers, preparePlotFrame, replaceVariables, dataLinkPostProcessor } = props;
|
|
|
|
const preparePlotFrameFn = preparePlotFrame ?? defaultPreparePlotFrame;
|
|
|
|
const withLinks = frames.some((frame) => frame.fields.some((field) => (field.config.links?.length ?? 0) > 0));
|
|
|
|
const alignedFrame = preparePlotFrameFn(
|
|
frames,
|
|
{
|
|
...fields,
|
|
// if there are data links, keep all fields during join so they're index-matched
|
|
y: withLinks ? () => true : fields.y,
|
|
},
|
|
props.timeRange
|
|
);
|
|
|
|
pluginLog('GraphNG', false, 'data aligned', alignedFrame);
|
|
|
|
if (alignedFrame) {
|
|
let alignedFrameFinal = alignedFrame;
|
|
|
|
if (withLinks) {
|
|
const timeZone = Array.isArray(this.props.timeZone) ? this.props.timeZone[0] : this.props.timeZone;
|
|
|
|
// for links gen we need to use original frames but with the aligned/joined data values
|
|
let linkFrames = frames.map((frame, frameIdx) => ({
|
|
...frame,
|
|
fields: alignedFrame.fields.filter(
|
|
(field, fieldIdx) => fieldIdx === 0 || field.state?.origin?.frameIndex === frameIdx
|
|
),
|
|
length: alignedFrame.length,
|
|
}));
|
|
|
|
linkFrames.forEach((linkFrame, frameIndex) => {
|
|
linkFrame.fields.forEach((field) => {
|
|
field.getLinks = getLinksSupplier(
|
|
linkFrame,
|
|
field,
|
|
{
|
|
...field.state?.scopedVars,
|
|
__dataContext: {
|
|
value: {
|
|
data: linkFrames,
|
|
field: field,
|
|
frame: linkFrame,
|
|
frameIndex,
|
|
},
|
|
},
|
|
},
|
|
replaceVariables,
|
|
timeZone,
|
|
dataLinkPostProcessor
|
|
);
|
|
});
|
|
});
|
|
|
|
// filter join field and fields.y
|
|
alignedFrameFinal = {
|
|
...alignedFrame,
|
|
fields: alignedFrame.fields.filter((field, i) => i === 0 || fields.y(field, alignedFrame, [alignedFrame])),
|
|
};
|
|
}
|
|
|
|
if (props.omitHideFromViz) {
|
|
const nonHiddenFields = alignedFrameFinal.fields.filter((field) => field.config.custom?.hideFrom?.viz !== true);
|
|
alignedFrameFinal = {
|
|
...alignedFrameFinal,
|
|
fields: nonHiddenFields,
|
|
length: nonHiddenFields.length,
|
|
};
|
|
}
|
|
|
|
let config = this.state?.config;
|
|
|
|
if (withConfig) {
|
|
config = props.prepConfig(alignedFrameFinal, this.props.frames, this.getTimeRange);
|
|
pluginLog('GraphNG', false, 'config prepared', config);
|
|
}
|
|
|
|
state = {
|
|
alignedFrame: alignedFrameFinal,
|
|
config,
|
|
};
|
|
|
|
pluginLog('GraphNG', false, 'data prepared', state.alignedData);
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
componentDidUpdate(prevProps: GraphNGProps) {
|
|
const { frames, structureRev, timeZone, cursorSync, propsToDiff } = this.props;
|
|
|
|
const propsChanged = !sameProps(prevProps, this.props, propsToDiff);
|
|
|
|
if (
|
|
frames !== prevProps.frames ||
|
|
propsChanged ||
|
|
timeZone !== prevProps.timeZone ||
|
|
cursorSync !== prevProps.cursorSync
|
|
) {
|
|
let newState = this.prepState(this.props, false);
|
|
|
|
if (newState) {
|
|
const shouldReconfig =
|
|
this.state.config === undefined ||
|
|
timeZone !== prevProps.timeZone ||
|
|
cursorSync !== prevProps.cursorSync ||
|
|
structureRev !== prevProps.structureRev ||
|
|
!structureRev ||
|
|
propsChanged;
|
|
|
|
if (shouldReconfig) {
|
|
newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange);
|
|
pluginLog('GraphNG', false, 'config recreated', newState.config);
|
|
}
|
|
|
|
newState.alignedData = newState.config!.prepData!([newState.alignedFrame]) as AlignedData;
|
|
|
|
this.setState(newState);
|
|
}
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const { width, height, children, renderLegend } = this.props;
|
|
const { config, alignedFrame, alignedData } = this.state;
|
|
|
|
if (!config) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<VizLayout width={width} height={height} legend={renderLegend(config)}>
|
|
{(vizWidth: number, vizHeight: number) => (
|
|
<UPlotChart
|
|
config={config}
|
|
data={alignedData!}
|
|
width={vizWidth}
|
|
height={vizHeight}
|
|
plotRef={(u) => ((this.plotInstance as React.MutableRefObject<uPlot>).current = u)}
|
|
>
|
|
{children ? children(config, alignedFrame) : null}
|
|
</UPlotChart>
|
|
)}
|
|
</VizLayout>
|
|
);
|
|
}
|
|
}
|