mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GraphNG: Using new VizLayout, moving Legend into GraphNG and some other refactorings (#28913)
* Graph refactorings * Move legend to GraphNG and use new VizLayout * Things are working * remove unused things * Update * Fixed ng test dashboard * Update * More refactoring * Removed plugin * Upgrade uplot * Auto size axis * Axis scaling * Fixed tests * updated * minor simplification * Fixed selection color * Fixed story * Minor story fix * Improve x-axis formatting * Tweaks * Update * Updated * Updates to handle timezone * Updated * Fixing types * Update * Fixed type * Updated
This commit is contained in:
@@ -3,7 +3,6 @@ import { GraphWithLegend, Chart } from '@grafana/ui';
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { Options } from './types';
|
||||
import { GraphPanelController } from './GraphPanelController';
|
||||
import { LegendDisplayMode } from '@grafana/ui/src/components/Legend/Legend';
|
||||
|
||||
interface GraphPanelProps extends PanelProps<Options> {}
|
||||
|
||||
@@ -38,7 +37,6 @@ export const GraphPanel: React.FunctionComponent<GraphPanelProps> = ({
|
||||
showPoints,
|
||||
tooltipOptions,
|
||||
};
|
||||
const { asTable, isVisible, ...legendProps } = legendOptions;
|
||||
return (
|
||||
<GraphPanelController
|
||||
data={data}
|
||||
@@ -55,14 +53,14 @@ export const GraphPanel: React.FunctionComponent<GraphPanelProps> = ({
|
||||
timeZone={timeZone}
|
||||
width={width}
|
||||
height={height}
|
||||
displayMode={asTable ? LegendDisplayMode.Table : LegendDisplayMode.List}
|
||||
isLegendVisible={isVisible}
|
||||
displayMode={legendOptions.displayMode}
|
||||
isLegendVisible={legendOptions.isVisible}
|
||||
placement={legendOptions.placement}
|
||||
sortLegendBy={legendOptions.sortBy}
|
||||
sortLegendDesc={legendOptions.sortDesc}
|
||||
onSeriesToggle={onSeriesToggle}
|
||||
onHorizontalRegionSelected={onHorizontalRegionSelected}
|
||||
{...graphProps}
|
||||
{...legendProps}
|
||||
{...controllerApi}
|
||||
>
|
||||
<Chart.Tooltip mode={tooltipOptions.mode} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LegendOptions, GraphTooltipOptions } from '@grafana/ui';
|
||||
import { LegendOptions, GraphTooltipOptions, LegendDisplayMode } from '@grafana/ui';
|
||||
import { YAxis } from '@grafana/data';
|
||||
|
||||
export interface SeriesOptions {
|
||||
@@ -28,9 +28,9 @@ export const defaults: Options = {
|
||||
showPoints: false,
|
||||
},
|
||||
legend: {
|
||||
asTable: false,
|
||||
isVisible: true,
|
||||
placement: 'under',
|
||||
displayMode: LegendDisplayMode.List,
|
||||
placement: 'bottom',
|
||||
},
|
||||
series: {},
|
||||
tooltipOptions: { mode: 'single' },
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Canvas,
|
||||
ContextMenuPlugin,
|
||||
LegendDisplayMode,
|
||||
LegendPlugin,
|
||||
TooltipPlugin,
|
||||
ZoomPlugin,
|
||||
GraphNG,
|
||||
} from '@grafana/ui';
|
||||
import { ContextMenuPlugin, TooltipPlugin, ZoomPlugin, GraphNG } from '@grafana/ui';
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { Options } from './types';
|
||||
import { VizLayout } from './VizLayout';
|
||||
import { AnnotationsPlugin } from './plugins/AnnotationsPlugin';
|
||||
import { ExemplarsPlugin } from './plugins/ExemplarsPlugin';
|
||||
|
||||
@@ -26,43 +17,19 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
|
||||
onChangeTimeRange,
|
||||
}) => {
|
||||
return (
|
||||
<VizLayout width={width} height={height}>
|
||||
{({ builder, getLayout }) => {
|
||||
const layout = getLayout();
|
||||
// when all layout slots are ready we can calculate the canvas(actual viz) size
|
||||
const canvasSize = layout.isReady
|
||||
? {
|
||||
width: width - (layout.left.width + layout.right.width),
|
||||
height: height - (layout.top.height + layout.bottom.height),
|
||||
}
|
||||
: { width: 0, height: 0 };
|
||||
|
||||
if (options.legend.isVisible) {
|
||||
builder.addSlot(
|
||||
options.legend.placement,
|
||||
<LegendPlugin
|
||||
placement={options.legend.placement}
|
||||
displayMode={options.legend.asTable ? LegendDisplayMode.Table : LegendDisplayMode.List}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
builder.clearSlot(options.legend.placement);
|
||||
}
|
||||
|
||||
return (
|
||||
<GraphNG data={data.series} timeRange={timeRange} timeZone={timeZone} {...canvasSize}>
|
||||
{builder.addSlot('canvas', <Canvas />).render()}
|
||||
<TooltipPlugin mode={options.tooltipOptions.mode as any} timeZone={timeZone} />
|
||||
<ZoomPlugin onZoom={onChangeTimeRange} />
|
||||
<ContextMenuPlugin />
|
||||
|
||||
{data.annotations && <ExemplarsPlugin exemplars={data.annotations} timeZone={timeZone} />}
|
||||
{data.annotations && <AnnotationsPlugin annotations={data.annotations} timeZone={timeZone} />}
|
||||
{/* TODO: */}
|
||||
{/*<AnnotationsEditorPlugin />*/}
|
||||
</GraphNG>
|
||||
);
|
||||
}}
|
||||
</VizLayout>
|
||||
<GraphNG
|
||||
data={data.series}
|
||||
timeRange={timeRange}
|
||||
timeZone={timeZone}
|
||||
width={width}
|
||||
height={height - 8}
|
||||
legend={options.legend}
|
||||
>
|
||||
<TooltipPlugin mode={options.tooltipOptions.mode as any} timeZone={timeZone} />
|
||||
<ZoomPlugin onZoom={onChangeTimeRange} />
|
||||
<ContextMenuPlugin />
|
||||
{data.annotations && <ExemplarsPlugin exemplars={data.annotations} timeZone={timeZone} />}
|
||||
{data.annotations && <AnnotationsPlugin annotations={data.annotations} timeZone={timeZone} />}
|
||||
</GraphNG>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { useMeasure } from './useMeasure';
|
||||
import { LayoutBuilder, LayoutRendererComponent } from './LayoutBuilder';
|
||||
import { CustomScrollbar } from '@grafana/ui';
|
||||
|
||||
type UseMeasureRect = Pick<DOMRectReadOnly, 'x' | 'y' | 'top' | 'left' | 'right' | 'bottom' | 'height' | 'width'>;
|
||||
|
||||
const RESET_DIMENSIONS: UseMeasureRect = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 0,
|
||||
width: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
};
|
||||
|
||||
const DEFAULT_VIZ_LAYOUT_STATE = {
|
||||
isReady: false,
|
||||
top: RESET_DIMENSIONS,
|
||||
bottom: RESET_DIMENSIONS,
|
||||
right: RESET_DIMENSIONS,
|
||||
left: RESET_DIMENSIONS,
|
||||
canvas: RESET_DIMENSIONS,
|
||||
};
|
||||
|
||||
export type VizLayoutSlots = 'top' | 'bottom' | 'left' | 'right' | 'canvas';
|
||||
|
||||
export interface VizLayoutState extends Record<VizLayoutSlots, UseMeasureRect> {
|
||||
isReady: boolean;
|
||||
}
|
||||
|
||||
interface VizLayoutAPI {
|
||||
builder: LayoutBuilder<VizLayoutSlots>;
|
||||
getLayout: () => VizLayoutState;
|
||||
}
|
||||
|
||||
interface VizLayoutProps {
|
||||
width: number;
|
||||
height: number;
|
||||
children: (api: VizLayoutAPI) => React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graph viz layout. Consists of 5 slots: top(T), bottom(B), left(L), right(R), canvas:
|
||||
*
|
||||
* +-----------------------------------------------+
|
||||
* | T |
|
||||
* ----|---------------------------------------|----
|
||||
* | | | |
|
||||
* | | | |
|
||||
* | L | CANVAS SLOT | R |
|
||||
* | | | |
|
||||
* | | | |
|
||||
* ----|---------------------------------------|----
|
||||
* | B |
|
||||
* +-----------------------------------------------+
|
||||
*
|
||||
*/
|
||||
const VizLayoutRenderer: LayoutRendererComponent<VizLayoutSlots> = ({ slots, refs, width, height }) => {
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
height: ${height}px;
|
||||
width: ${width}px;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
`}
|
||||
>
|
||||
{slots.top && (
|
||||
<div
|
||||
ref={refs.top}
|
||||
className={css`
|
||||
width: 100%;
|
||||
max-height: 35%;
|
||||
align-self: top;
|
||||
`}
|
||||
>
|
||||
<CustomScrollbar>{slots.top}</CustomScrollbar>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(slots.left || slots.right || slots.canvas) && (
|
||||
<div
|
||||
className={css`
|
||||
label: INNER;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
{slots.left && (
|
||||
<div
|
||||
ref={refs.left}
|
||||
className={css`
|
||||
max-height: 100%;
|
||||
`}
|
||||
>
|
||||
<CustomScrollbar>{slots.left}</CustomScrollbar>
|
||||
</div>
|
||||
)}
|
||||
{slots.canvas && <div>{slots.canvas}</div>}
|
||||
{slots.right && (
|
||||
<div
|
||||
ref={refs.right}
|
||||
className={css`
|
||||
max-height: 100%;
|
||||
`}
|
||||
>
|
||||
<CustomScrollbar>{slots.right}</CustomScrollbar>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{slots.bottom && (
|
||||
<div
|
||||
ref={refs.bottom}
|
||||
className={css`
|
||||
width: 100%;
|
||||
max-height: 35%;
|
||||
`}
|
||||
>
|
||||
<CustomScrollbar>{slots.bottom}</CustomScrollbar>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const VizLayout: React.FC<VizLayoutProps> = ({ children, width, height }) => {
|
||||
/**
|
||||
* Layout slots refs & bboxes
|
||||
* Refs are passed down to the renderer component by layout builder
|
||||
* It's up to the renderer to assign refs to correct slots(which are underlying DOM elements)
|
||||
* */
|
||||
const [bottomSlotRef, bottomSlotBBox] = useMeasure();
|
||||
const [topSlotRef, topSlotBBox] = useMeasure();
|
||||
const [leftSlotRef, leftSlotBBox] = useMeasure();
|
||||
const [rightSlotRef, rightSlotBBox] = useMeasure();
|
||||
const [canvasSlotRef, canvasSlotBBox] = useMeasure();
|
||||
|
||||
// public fluent API exposed via render prop to build the layout
|
||||
const builder = useMemo(
|
||||
() =>
|
||||
new LayoutBuilder(
|
||||
VizLayoutRenderer,
|
||||
{
|
||||
top: topSlotRef,
|
||||
bottom: bottomSlotRef,
|
||||
left: leftSlotRef,
|
||||
right: rightSlotRef,
|
||||
canvas: canvasSlotRef,
|
||||
},
|
||||
width,
|
||||
height
|
||||
),
|
||||
[bottomSlotBBox, topSlotBBox, leftSlotBBox, rightSlotBBox, width, height]
|
||||
);
|
||||
|
||||
// memoized map of layout slot bboxes, used for exposing correct bboxes when the layout is ready
|
||||
const bboxMap = useMemo(
|
||||
() => ({
|
||||
top: topSlotBBox,
|
||||
bottom: bottomSlotBBox,
|
||||
left: leftSlotBBox,
|
||||
right: rightSlotBBox,
|
||||
canvas: canvasSlotBBox,
|
||||
}),
|
||||
[bottomSlotBBox, topSlotBBox, leftSlotBBox, rightSlotBBox]
|
||||
);
|
||||
|
||||
const [dimensions, setDimensions] = useState<VizLayoutState>(DEFAULT_VIZ_LAYOUT_STATE);
|
||||
|
||||
// when DOM settles we set the layout to be ready to get measurements downstream
|
||||
useLayoutEffect(() => {
|
||||
// layout is ready by now
|
||||
const currentLayout = builder.getLayout();
|
||||
|
||||
// map active layout slots to corresponding bboxes
|
||||
let nextDimensions: Partial<Record<VizLayoutSlots, UseMeasureRect>> = {};
|
||||
for (const key of Object.keys(currentLayout)) {
|
||||
nextDimensions[key as VizLayoutSlots] = bboxMap[key as VizLayoutSlots];
|
||||
}
|
||||
|
||||
const nextState = {
|
||||
// first, reset all bboxes to defaults
|
||||
...DEFAULT_VIZ_LAYOUT_STATE,
|
||||
// set layout to ready
|
||||
isReady: true,
|
||||
// update state with active slot bboxes
|
||||
...nextDimensions,
|
||||
};
|
||||
|
||||
setDimensions(nextState);
|
||||
}, [bottomSlotBBox, topSlotBBox, leftSlotBBox, rightSlotBBox, width, height]);
|
||||
|
||||
// returns current state of the layout, bounding rects of all slots to be rendered
|
||||
const getLayout = useCallback(() => {
|
||||
return dimensions;
|
||||
}, [dimensions]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
label: PanelVizLayout;
|
||||
width: ${width}px;
|
||||
height: ${height}px;
|
||||
overflow: hidden;
|
||||
`}
|
||||
>
|
||||
{children({
|
||||
builder: builder,
|
||||
getLayout,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { GraphPanel } from './GraphPanel';
|
||||
import { Options } from './types';
|
||||
|
||||
export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPanel)
|
||||
.setNoPadding()
|
||||
.useFieldConfig({
|
||||
standardOptions: {
|
||||
[FieldConfigProperty.Color]: {
|
||||
@@ -62,7 +63,7 @@ export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPane
|
||||
.addSliderInput({
|
||||
path: 'fill.alpha',
|
||||
name: 'Fill area opacity',
|
||||
defaultValue: 0.1,
|
||||
defaultValue: 0,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
@@ -161,8 +162,6 @@ export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPane
|
||||
defaultValue: 'bottom',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: 'left', label: 'Left' },
|
||||
{ value: 'top', label: 'Top' },
|
||||
{ value: 'bottom', label: 'Bottom' },
|
||||
{ value: 'right', label: 'Right' },
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DataFrame, DataFrameView, dateTimeFormat, systemDateFormats, TimeZone } from '@grafana/data';
|
||||
import { EventsCanvas, usePlotContext, usePlotPluginContext, useTheme } from '@grafana/ui';
|
||||
import { EventsCanvas, usePlotContext, useTheme } from '@grafana/ui';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { AnnotationMarker } from './AnnotationMarker';
|
||||
|
||||
@@ -17,7 +17,6 @@ interface AnnotationsDataFrameViewDTO {
|
||||
export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotations, timeZone }) => {
|
||||
const pluginId = 'AnnotationsPlugin';
|
||||
const plotCtx = usePlotContext();
|
||||
const pluginsApi = usePlotPluginContext();
|
||||
|
||||
const theme = useTheme();
|
||||
const annotationsRef = useRef<Array<DataFrameView<AnnotationsDataFrameViewDTO>>>();
|
||||
@@ -45,7 +44,7 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
||||
}, [plotCtx.isPlotReady, annotations]);
|
||||
|
||||
useEffect(() => {
|
||||
const unregister = pluginsApi.registerPlugin({
|
||||
const unregister = plotCtx.registerPlugin({
|
||||
id: pluginId,
|
||||
hooks: {
|
||||
// Render annotation lines on the canvas
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { LegendOptions, GraphTooltipOptions } from '@grafana/ui';
|
||||
|
||||
export type LegendPlacement = 'top' | 'bottom' | 'left' | 'right';
|
||||
|
||||
export interface GraphOptions {
|
||||
// Redraw as time passes
|
||||
realTimeUpdates?: boolean;
|
||||
@@ -9,10 +7,7 @@ export interface GraphOptions {
|
||||
|
||||
export interface Options {
|
||||
graph: GraphOptions;
|
||||
legend: Omit<LegendOptions, 'placement'> &
|
||||
GraphLegendEditorLegendOptions & {
|
||||
placement: LegendPlacement;
|
||||
};
|
||||
legend: LegendOptions;
|
||||
tooltipOptions: GraphTooltipOptions;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useIsomorphicLayoutEffect } from 'react-use';
|
||||
|
||||
export type UseMeasureRect = Pick<
|
||||
DOMRectReadOnly,
|
||||
'x' | 'y' | 'top' | 'left' | 'right' | 'bottom' | 'height' | 'width'
|
||||
>;
|
||||
export type UseMeasureRef<E extends HTMLElement = HTMLElement> = (element: E) => void;
|
||||
export type UseMeasureResult<E extends HTMLElement = HTMLElement> = [UseMeasureRef<E>, UseMeasureRect];
|
||||
|
||||
const defaultState: UseMeasureRect = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
};
|
||||
|
||||
export const useMeasure = <E extends HTMLElement = HTMLElement>(): UseMeasureResult<E> => {
|
||||
const [element, ref] = useState<E | null>(null);
|
||||
const [rect, setRect] = useState<UseMeasureRect>(defaultState);
|
||||
|
||||
const observer = useMemo(
|
||||
() =>
|
||||
new (window as any).ResizeObserver((entries: any) => {
|
||||
if (entries[0]) {
|
||||
const { x, y, width, height, top, left, bottom, right } = entries[0].contentRect;
|
||||
setRect({ x, y, width, height, top, left, bottom, right });
|
||||
}
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (!element) {
|
||||
setRect(defaultState);
|
||||
return;
|
||||
}
|
||||
observer.observe(element);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [element]);
|
||||
|
||||
return [ref, rect];
|
||||
};
|
||||
Reference in New Issue
Block a user