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:
Torkel Ödegaard
2020-11-09 15:31:03 +01:00
committed by GitHub
parent 76f4c11430
commit 71fffcb17c
45 changed files with 492 additions and 741 deletions

View File

@@ -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} />

View File

@@ -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' },

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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' },
],

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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];
};