From aa6c98f7ff5afcd91e050af679f74dc8421a4611 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 6 Oct 2020 11:39:06 +0200 Subject: [PATCH] Graph NG: annotations display (#27972) * Annotations support POC * Fix markers memoization * dev dashboard update * Update public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx --- .../dev-dashboards/panel-graph/graph-ng.json | 366 ++++++++++++++---- .../src/components/Chart/TooltipContainer.tsx | 17 +- .../grafana-ui/src/components/Tags/Tag.tsx | 13 +- packages/grafana-ui/src/components/index.ts | 2 + .../grafana-ui/src/components/uPlot/Plot.tsx | 2 +- .../grafana-ui/src/components/uPlot/hooks.ts | 12 +- .../uPlot/plugins/ContextMenuPlugin.tsx | 2 +- .../components/uPlot/plugins/LegendPlugin.tsx | 2 +- .../grafana-ui/src/components/uPlot/utils.ts | 3 + .../app/plugins/panel/graph3/GraphPanel.tsx | 2 + .../panel/graph3/plugins/AnnotationMarker.tsx | 145 +++++++ .../graph3/plugins/AnnotationsPlugin.tsx | 126 ++++++ 12 files changed, 609 insertions(+), 83 deletions(-) create mode 100644 public/app/plugins/panel/graph3/plugins/AnnotationMarker.tsx create mode 100644 public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx diff --git a/devenv/dev-dashboards/panel-graph/graph-ng.json b/devenv/dev-dashboards/panel-graph/graph-ng.json index f2da6fb9d14..3e447c834f1 100644 --- a/devenv/dev-dashboards/panel-graph/graph-ng.json +++ b/devenv/dev-dashboards/panel-graph/graph-ng.json @@ -22,6 +22,9 @@ "datasource": null, "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "axis": { "grid": true, @@ -36,11 +39,11 @@ "alpha": 0.1 }, "line": { - "show": true, - "width": 1, "color": { "mode": "fixed" - } + }, + "show": true, + "width": 1 }, "nullValues": "null", "points": { @@ -355,6 +358,9 @@ "datasource": "gdev-testdata", "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "align": null, "axis": { @@ -371,10 +377,7 @@ }, "line": { "show": true, - "width": 1, - "color": { - "mode": "fixed" - } + "width": 1 }, "nullValues": "null", "points": { @@ -560,6 +563,9 @@ "datasource": "gdev-testdata", "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "align": null, "axis": { @@ -576,10 +582,7 @@ }, "line": { "show": true, - "width": 1, - "color": { - "mode": "fixed" - } + "width": 1 }, "nullValues": "null", "points": { @@ -678,6 +681,9 @@ "datasource": "gdev-testdata", "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "align": null, "axis": { @@ -694,10 +700,7 @@ }, "line": { "show": true, - "width": 1, - "color": { - "mode": "fixed" - } + "width": 1 }, "nullValues": "null", "points": { @@ -811,6 +814,9 @@ "datasource": "gdev-testdata", "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "align": null, "axis": { @@ -827,10 +833,7 @@ }, "line": { "show": false, - "width": 1, - "color": { - "mode": "fixed" - } + "width": 1 }, "nullValues": "null", "points": { @@ -899,7 +902,7 @@ "value": false }, { - "id": "custom.line.color", + "id": "color", "value": { "fixedColor": "purple", "mode": "fixed" @@ -960,6 +963,9 @@ "datasource": "gdev-testdata", "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "align": null, "axis": { @@ -976,10 +982,7 @@ }, "line": { "show": false, - "width": 1, - "color": { - "mode": "fixed" - } + "width": 1 }, "nullValues": "null", "points": { @@ -1048,7 +1051,7 @@ "value": false }, { - "id": "custom.line.color", + "id": "color", "value": { "fixedColor": "purple", "mode": "fixed" @@ -1109,6 +1112,9 @@ "datasource": "gdev-testdata", "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "align": null, "axis": { @@ -1125,10 +1131,7 @@ }, "line": { "show": false, - "width": 1, - "color": { - "mode": "fixed" - } + "width": 1 }, "nullValues": "null", "points": { @@ -1197,7 +1200,7 @@ "value": false }, { - "id": "custome.line.color", + "id": "color", "value": { "fixedColor": "purple", "mode": "fixed" @@ -1258,6 +1261,9 @@ "datasource": "gdev-testdata", "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "align": null, "axis": { @@ -1274,10 +1280,7 @@ }, "line": { "show": false, - "width": 1, - "color": { - "mode": "fixed" - } + "width": 1 }, "nullValues": "null", "points": { @@ -1346,7 +1349,7 @@ "value": false }, { - "id": "custom.line.color", + "id": "color", "value": { "fixedColor": "purple", "mode": "fixed" @@ -1422,6 +1425,9 @@ "datasource": null, "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "align": null, "axis": { @@ -1437,11 +1443,11 @@ "alpha": 0 }, "line": { - "show": true, - "width": 1, "color": { "mode": "fixed" - } + }, + "show": true, + "width": 1 }, "nullValues": "null", "points": { @@ -1506,6 +1512,9 @@ "datasource": null, "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "align": null, "axis": { @@ -1521,11 +1530,11 @@ "alpha": 0 }, "line": { - "show": true, - "width": 1, "color": { "mode": "fixed" - } + }, + "show": true, + "width": 1 }, "nullValues": "null", "points": { @@ -1590,6 +1599,9 @@ "datasource": null, "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "axis": { "grid": true, @@ -1604,11 +1616,11 @@ "alpha": 0 }, "line": { - "show": true, - "width": 1, "color": { "mode": "fixed" - } + }, + "show": true, + "width": 1 }, "nullValues": "null", "points": { @@ -1668,6 +1680,96 @@ "timeShift": null, "title": "No tooltip", "type": "graph3" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "fixed" + }, + "custom": { + "axis": { + "grid": true, + "label": "Temperature", + "side": 3, + "width": 60 + }, + "bars": { + "show": false + }, + "fill": { + "alpha": 0 + }, + "line": { + "color": { + "mode": "fixed" + }, + "show": true, + "width": 1 + }, + "nullValues": "null", + "points": { + "radius": 8, + "show": true + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 9, + "options": { + "graph": { + "realTimeUpdates": false + }, + "legend": { + "asTable": false, + "isVisible": true, + "placement": "under" + }, + "tooltipOptions": { + "mode": "multi" + } + }, + "pluginVersion": "7.2.0-pre", + "targets": [ + { + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + }, + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "10,25,40" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Diff number of datapoints milt multi series tooltip", + "transformations": [], + "type": "graph3" } ], "title": "Tooltip", @@ -1688,6 +1790,9 @@ "datasource": "gdev-testdata", "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "axis": { "grid": true, @@ -1702,11 +1807,11 @@ "alpha": 0 }, "line": { - "show": true, - "width": 1, "color": { "mode": "fixed" - } + }, + "show": true, + "width": 1 }, "nullValues": "null", "points": { @@ -1775,6 +1880,9 @@ "datasource": "gdev-testdata", "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "axis": { "grid": true, @@ -1795,10 +1903,7 @@ "show": false }, "line": { - "show": true, - "color": { - "mode": "fixed" - } + "show": true }, "points": { "radius": 8, @@ -1809,6 +1914,9 @@ "alpha": 0 }, "line": { + "color": { + "mode": "fixed" + }, "show": true, "width": 1 }, @@ -1879,6 +1987,9 @@ "datasource": "gdev-testdata", "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "axis": { "grid": true, @@ -1893,11 +2004,11 @@ "alpha": 0 }, "line": { - "show": true, - "width": 1, "color": { "mode": "fixed" - } + }, + "show": true, + "width": 1 }, "nullValues": "connected", "points": { @@ -1981,6 +2092,9 @@ "datasource": "gdev-testdata", "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "axis": { "grid": true, @@ -1995,11 +2109,11 @@ "alpha": 0 }, "line": { - "show": true, - "width": 1, "color": { "mode": "fixed" - } + }, + "show": true, + "width": 1 }, "nullValues": "null", "points": { @@ -2031,7 +2145,7 @@ "x": 0, "y": 16 }, - "id": 9, + "id": 43, "options": { "graph": { "realTimeUpdates": false @@ -2083,6 +2197,9 @@ "datasource": "gdev-testdata", "fieldConfig": { "defaults": { + "color": { + "mode": "fixed" + }, "custom": { "align": null, "axis": { @@ -2098,11 +2215,11 @@ "alpha": 0.2 }, "line": { - "show": false, - "width": 1, "color": { "mode": "fixed" - } + }, + "show": false, + "width": 1 }, "nullValues": "null", "points": { @@ -2175,7 +2292,7 @@ "value": false }, { - "id": "custom.line.color", + "id": "color", "value": { "fixedColor": "purple", "mode": "fixed" @@ -2229,6 +2346,124 @@ ], "title": "Streaming", "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 45, + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "fixed" + }, + "custom": { + "axis": { + "grid": true, + "label": "", + "side": 3, + "width": 60 + }, + "bars": { + "show": false + }, + "fill": { + "alpha": 0.1 + }, + "line": { + "color": { + "mode": "fixed" + }, + "show": true, + "width": 1 + }, + "nullValues": "null", + "points": { + "radius": 4, + "show": false + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 46, + "options": { + "graph": { + "realTimeUpdates": false + }, + "legend": { + "asTable": false, + "isVisible": true, + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.3.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk" + }, + { + "refId": "B", + "scenarioId": "random_walk" + }, + { + "refId": "C", + "scenarioId": "random_walk" + }, + { + "refId": "D", + "scenarioId": "random_walk" + }, + { + "refId": "E", + "scenarioId": "random_walk" + }, + { + "refId": "F", + "scenarioId": "annotations", + "stringInput": "" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "React NG graph", + "type": "graph3" + } + ], + "title": "Annotations", + "type": "row" } ], "refresh": false, @@ -2239,15 +2474,14 @@ "list": [] }, "time": { - "from": "now-5m", + "from": "now-2d", "to": "now" }, "timepicker": { "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "Africa/Accra", - "title": "Panel Tests - Graph NG", + "title": "Graph NG - tests", "uid": "TkZXxlNG3", - "version": 61, - "tags": ["gdev", "panel-tests", "graph"] + "version": 65 } diff --git a/packages/grafana-ui/src/components/Chart/TooltipContainer.tsx b/packages/grafana-ui/src/components/Chart/TooltipContainer.tsx index cea5c2416d7..320181335d8 100644 --- a/packages/grafana-ui/src/components/Chart/TooltipContainer.tsx +++ b/packages/grafana-ui/src/components/Chart/TooltipContainer.tsx @@ -1,12 +1,12 @@ -import React, { useState, useLayoutEffect, useRef } from 'react'; +import React, { useState, useLayoutEffect, useRef, HTMLAttributes } from 'react'; import { stylesFactory } from '../../themes/stylesFactory'; import { selectThemeVariant } from '../../themes/selectThemeVariant'; -import { css } from 'emotion'; +import { css, cx } from 'emotion'; import { useTheme } from '../../themes/ThemeContext'; import useWindowSize from 'react-use/lib/useWindowSize'; import { GrafanaTheme } from '@grafana/data'; -interface TooltipContainerProps { +interface TooltipContainerProps extends HTMLAttributes { position: { x: number; y: number }; offset: { x: number; y: number }; children?: JSX.Element; @@ -27,7 +27,13 @@ const getTooltipContainerStyles = stylesFactory((theme: GrafanaTheme) => { }; }); -export const TooltipContainer: React.FC = ({ position, offset, children }) => { +export const TooltipContainer: React.FC = ({ + position, + offset, + children, + className, + ...otherProps +}) => { const theme = useTheme(); const tooltipRef = useRef(null); const { width, height } = useWindowSize(); @@ -70,7 +76,8 @@ export const TooltipContainer: React.FC = ({ position, of top: 0, transform: `translate3d(${placement.x}px, ${placement.y}px, 0)`, }} - className={styles.wrapper} + {...otherProps} + className={cx(styles.wrapper, className)} > {children} diff --git a/packages/grafana-ui/src/components/Tags/Tag.tsx b/packages/grafana-ui/src/components/Tags/Tag.tsx index c4a7c8e73c6..275d14d0764 100644 --- a/packages/grafana-ui/src/components/Tags/Tag.tsx +++ b/packages/grafana-ui/src/components/Tags/Tag.tsx @@ -25,7 +25,13 @@ export const Tag = forwardRef(({ name, onClick, className, c }; return ( - + {name} ); @@ -50,8 +56,9 @@ const getTagStyles = (theme: GrafanaTheme, name: string, colorIndex?: number) => text-shadow: none; padding: 3px 6px; border-radius: ${theme.border.radius.md}; - - :hover { + `, + hover: css` + &:hover { opacity: 0.85; cursor: pointer; } diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index a9de0732105..08873056a1b 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -75,6 +75,7 @@ export * from './uPlot/geometries'; export { usePlotConfigContext } from './uPlot/context'; export { Canvas } from './uPlot/Canvas'; export * from './uPlot/plugins'; +export { usePlotContext, usePlotData, usePlotPluginContext } from './uPlot/context'; export { Gauge } from './Gauge/Gauge'; export { Graph } from './Graph/Graph'; @@ -129,6 +130,7 @@ export { FadeTransition } from './transitions/FadeTransition'; export { SlideOutTransition } from './transitions/SlideOutTransition'; export { Segment, SegmentAsync, SegmentInput, SegmentSelect } from './Segment/'; export { default as Chart } from './Chart'; +export { TooltipContainer } from './Chart/TooltipContainer'; export { Drawer } from './Drawer/Drawer'; export { Slider } from './Slider/Slider'; diff --git a/packages/grafana-ui/src/components/uPlot/Plot.tsx b/packages/grafana-ui/src/components/uPlot/Plot.tsx index 8978a763aa1..6a15dba7bbd 100644 --- a/packages/grafana-ui/src/components/uPlot/Plot.tsx +++ b/packages/grafana-ui/src/components/uPlot/Plot.tsx @@ -82,7 +82,7 @@ export const UPlotChart: React.FC = props => { // Memoize plot context const plotCtx = useMemo(() => { return buildPlotContext(registerPlugin, addSeries, addAxis, addScale, canvasRef, props.data, plotInstance); - }, [registerPlugin, canvasRef, props.data, plotInstance, addSeries, addAxis, addScale]); + }, [registerPlugin, addSeries, addAxis, addScale, canvasRef, props.data, plotInstance]); return ( diff --git a/packages/grafana-ui/src/components/uPlot/hooks.ts b/packages/grafana-ui/src/components/uPlot/hooks.ts index 9485165e536..bf1cfab07d6 100644 --- a/packages/grafana-ui/src/components/uPlot/hooks.ts +++ b/packages/grafana-ui/src/components/uPlot/hooks.ts @@ -36,11 +36,11 @@ export const usePlotPlugins = () => { (plugin: PlotPlugin) => { pluginLog(plugin.id, false, 'register'); - if (plugins.hasOwnProperty(plugin.id)) { - throw new Error(`${plugin.id} that is already registered`); - } - setPlugins(plugs => { + if (plugs.hasOwnProperty(plugin.id)) { + throw new Error(`${plugin.id} that is already registered`); + } + return { ...plugs, [plugin.id]: plugin, @@ -58,7 +58,7 @@ export const usePlotPlugins = () => { }); }; }, - [setPlugins, plugins] + [setPlugins] ); // When uPlot mounts let's check if there are any plugins pending registration @@ -74,7 +74,7 @@ export const usePlotPlugins = () => { return { arePluginsReady, - plugins, + plugins: plugins || {}, registerPlugin, }; }; diff --git a/packages/grafana-ui/src/components/uPlot/plugins/ContextMenuPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/ContextMenuPlugin.tsx index 2668800c4a6..4b723f11ce9 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/ContextMenuPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/ContextMenuPlugin.tsx @@ -10,7 +10,7 @@ interface ContextMenuPluginProps { } export const ContextMenuPlugin: React.FC = ({ onClose }) => { - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); const onClick = useCallback(() => { setIsOpen(!isOpen); diff --git a/packages/grafana-ui/src/components/uPlot/plugins/LegendPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/LegendPlugin.tsx index 9299ff0f400..32cbf65385d 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/LegendPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/LegendPlugin.tsx @@ -32,7 +32,7 @@ export const LegendPlugin: React.FC = ({ placement, displayMo label: getFieldDisplayName(field, data), isVisible: true, //flot vs uPlot differences - yAxis: (field.config.custom as GraphCustomFieldConfig).axis?.side === 1 ? 3 : 1, + yAxis: (field.config.custom as GraphCustomFieldConfig)?.axis?.side === 1 ? 3 : 1, }); seriesIdx++; } diff --git a/packages/grafana-ui/src/components/uPlot/utils.ts b/packages/grafana-ui/src/components/uPlot/utils.ts index 4821e6861a5..a169f45e9b8 100644 --- a/packages/grafana-ui/src/components/uPlot/utils.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.ts @@ -99,6 +99,9 @@ export const shouldReinitialisePlot = (prevConfig?: uPlot.Options, config?: uPlo } if (!prevConfig && config) { + if (config.width === 0 || config.height === 0) { + return false; + } return true; } diff --git a/public/app/plugins/panel/graph3/GraphPanel.tsx b/public/app/plugins/panel/graph3/GraphPanel.tsx index d618d4eb29b..e1002059b05 100644 --- a/public/app/plugins/panel/graph3/GraphPanel.tsx +++ b/public/app/plugins/panel/graph3/GraphPanel.tsx @@ -33,6 +33,7 @@ import { VizLayout } from './VizLayout'; import { Axis } from '@grafana/ui/src/components/uPlot/geometries/Axis'; import { timeFormatToTemplate } from '@grafana/ui/src/components/uPlot/utils'; +import { AnnotationsPlugin } from './plugins/AnnotationsPlugin'; interface GraphPanelProps extends PanelProps {} @@ -232,6 +233,7 @@ export const GraphPanel: React.FC = ({ + {data.annotations && } {/* TODO: */} {/**/} diff --git a/public/app/plugins/panel/graph3/plugins/AnnotationMarker.tsx b/public/app/plugins/panel/graph3/plugins/AnnotationMarker.tsx new file mode 100644 index 00000000000..57e51d5981c --- /dev/null +++ b/public/app/plugins/panel/graph3/plugins/AnnotationMarker.tsx @@ -0,0 +1,145 @@ +import React, { useCallback, useRef, useState } from 'react'; +import { AnnotationEvent, GrafanaTheme } from '@grafana/data'; +import { HorizontalGroup, Portal, Tag, TooltipContainer, useStyles } from '@grafana/ui'; +import { css, cx } from 'emotion'; + +interface AnnotationMarkerProps { + formatTime: (value: number) => string; + annotationEvent: AnnotationEvent; + x: number; +} + +export const AnnotationMarker: React.FC = ({ annotationEvent, x, formatTime }) => { + const styles = useStyles(getAnnotationMarkerStyles); + const [isOpen, setIsOpen] = useState(false); + const markerRef = useRef(null); + const annotationPopoverRef = useRef(null); + const popoverRenderTimeout = useRef(); + + const onMouseEnter = useCallback(() => { + if (popoverRenderTimeout.current) { + clearTimeout(popoverRenderTimeout.current); + } + setIsOpen(true); + }, [setIsOpen]); + + const onMouseLeave = useCallback(() => { + popoverRenderTimeout.current = setTimeout(() => { + setIsOpen(false); + }, 100); + }, [setIsOpen]); + + const renderMarker = useCallback(() => { + if (!markerRef?.current) { + return null; + } + + const el = markerRef.current; + const elBBox = el.getBoundingClientRect(); + + return ( + +
+
+ {annotationEvent.title} + {annotationEvent.time && {formatTime(annotationEvent.time)}} +
+
+ {annotationEvent.text &&
} + <> + + {annotationEvent.tags?.map((t, i) => ( + + ))} + + +
+
+ + ); + }, [annotationEvent]); + + return ( + <> +
+
+
+ {isOpen && {renderMarker()}} + + ); +}; + +const getAnnotationMarkerStyles = (theme: GrafanaTheme) => { + const bg = theme.isDark ? theme.palette.dark2 : theme.palette.white; + const headerBg = theme.isDark ? theme.palette.dark9 : theme.palette.gray5; + const shadowColor = theme.isDark ? theme.palette.black : theme.palette.white; + + return { + markerWrapper: css` + padding: 0 4px 4px 4px; + position: absolute; + top: 0; + `, + marker: css` + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-bottom: 4px solid ${theme.palette.red}; + pointer-events: none; + `, + wrapper: css` + background: ${bg}; + border: 1px solid ${headerBg}; + border-radius: ${theme.border.radius.md}; + max-width: 400px; + box-shadow: 0 0 20px ${shadowColor}; + `, + tooltip: css` + background: none; + padding: 0; + `, + header: css` + background: ${headerBg}; + padding: 6px 10px; + display: flex; + `, + title: css` + font-weight: ${theme.typography.weight.semibold}; + padding-right: ${theme.spacing.md}; + overflow: hidden; + display: inline-block; + white-space: nowrap; + text-overflow: ellipsis; + flex-grow: 1; + `, + time: css` + color: ${theme.colors.textWeak}; + font-style: italic; + font-weight: normal; + display: inline-block; + position: relative; + top: 1px; + `, + body: css` + padding: ${theme.spacing.sm}; + font-weight: ${theme.typography.weight.semibold}; + `, + }; +}; diff --git a/public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx b/public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx new file mode 100644 index 00000000000..80b8c2d598a --- /dev/null +++ b/public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx @@ -0,0 +1,126 @@ +import { AnnotationEvent, DataFrame, dateTimeFormat, systemDateFormats, TimeZone } from '@grafana/data'; +import { usePlotContext, usePlotPluginContext, useTheme } from '@grafana/ui'; +import { getAnnotationsFromData } from 'app/features/annotations/standardAnnotationSupport'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { css } from 'emotion'; +import { AnnotationMarker } from './AnnotationMarker'; + +interface AnnotationsPluginProps { + annotations: DataFrame[]; + timeZone: TimeZone; +} + +export const AnnotationsPlugin: React.FC = ({ annotations, timeZone }) => { + const pluginId = 'AnnotationsPlugin'; + const pluginsApi = usePlotPluginContext(); + const plotContext = usePlotContext(); + const annotationsRef = useRef(null); + const [renderCounter, setRenderCounter] = useState(0); + const theme = useTheme(); + + const timeFormatter = useCallback( + (value: number) => { + return dateTimeFormat(value, { + format: systemDateFormats.fullDate, + timeZone, + }); + }, + [timeZone] + ); + + const annotationsData = useMemo(() => { + return getAnnotationsFromData(annotations); + }, [annotations]); + + const annotationMarkers = useMemo(() => { + if (!plotContext || !plotContext?.u) { + return null; + } + const markers = []; + + for (let i = 0; i < annotationsData.length; i++) { + const annotation = annotationsData[i]; + if (!annotation.time) { + continue; + } + const xpos = plotContext.u.valToPos(annotation.time / 1000, 'x'); + markers.push( + + ); + } + + return ( +
+ {markers} +
+ ); + }, [annotationsData, timeFormatter, plotContext, renderCounter]); + + // For uPlot plugin to have access to lates annotation data we need to update the data ref + useEffect(() => { + annotationsRef.current = annotationsData; + }, [annotationsData]); + + useEffect(() => { + const unregister = pluginsApi.registerPlugin({ + id: pluginId, + hooks: { + // Render annotation lines on the canvas + draw: u => { + /** + * We cannot rely on state value here, as it would require this effect to be dependent on the state value. + * This would make the plugin re-register making the entire plot to reinitialise. ref is the way to go :) + */ + if (!annotationsRef.current) { + return null; + } + const ctx = u.ctx; + if (!ctx) { + return; + } + for (let i = 0; i < annotationsRef.current.length; i++) { + const annotation = annotationsRef.current[i]; + if (!annotation.time) { + continue; + } + const xpos = u.valToPos(annotation.time / 1000, 'x', true); + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.strokeStyle = theme.palette.red; + ctx.setLineDash([5, 5]); + ctx.moveTo(xpos, u.bbox.top); + ctx.lineTo(xpos, u.bbox.top + u.bbox.height); + ctx.stroke(); + ctx.closePath(); + } + setRenderCounter(c => c + 1); + return; + }, + }, + }); + + return () => { + unregister(); + }; + }, []); + + if (!plotContext || !plotContext.u || !plotContext.canvas) { + return null; + } + + return
{annotationMarkers}
; +};