From d601acac3aaf2809b299e0af9ce5cef44cf92006 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 2 Apr 2024 15:32:46 -0500 Subject: [PATCH] VizTooltips: Remove old tooltips and annotations (#84420) Co-authored-by: Adela Almasan --- .betterer.results | 33 -- .../VizTooltip/VizTooltipContent.tsx | 13 +- .../uPlot/config/UPlotConfigBuilder.ts | 27 +- .../uPlot/config/addTooltipSupport.ts | 160 --------- .../uPlot/plugins/EventBusPlugin.tsx | 5 +- .../uPlot/plugins/TooltipPlugin.tsx | 333 ------------------ .../uPlot/plugins/TooltipPlugin2.tsx | 29 +- .../components/uPlot/plugins/ZoomPlugin.tsx | 127 ------- .../src/components/uPlot/plugins/index.ts | 2 - .../grafana-ui/src/components/uPlot/types.ts | 4 - .../src/components/uPlot/utils.test.ts | 16 +- .../grafana-ui/src/components/uPlot/utils.ts | 6 - .../GraphNG/__snapshots__/utils.test.ts.snap | 7 - .../src/graveyard/GraphNG/utils.test.ts | 3 - .../src/graveyard/TimeSeries/TimeSeries.tsx | 3 - .../src/graveyard/TimeSeries/utils.ts | 24 +- .../src/options/builder/tooltip.tsx | 4 +- .../app/core/components/GraphNG/GraphNG.tsx | 16 +- .../GraphNG/__snapshots__/utils.test.ts.snap | 7 - public/app/core/components/GraphNG/hooks.ts | 44 --- .../app/core/components/GraphNG/utils.test.ts | 2 - .../core/components/TimeSeries/TimeSeries.tsx | 7 - .../app/core/components/TimeSeries/utils.ts | 20 +- .../TimelineChart/TimelineChart.tsx | 11 +- .../core/components/TimelineChart/timeline.ts | 14 - .../core/components/TimelineChart/utils.ts | 110 +----- .../rules/state-history/LogTimelineViewer.tsx | 63 +--- .../rules/state-history/LokiStateHistory.tsx | 24 +- .../plugins/panel/barchart/BarChartPanel.tsx | 119 +------ public/app/plugins/panel/barchart/bars.ts | 21 +- public/app/plugins/panel/barchart/utils.ts | 4 - .../panel/candlestick/CandlestickPanel.tsx | 117 +----- .../panel/heatmap/HeatmapHoverViewOld.tsx | 222 ------------ .../plugins/panel/heatmap/HeatmapPanel.tsx | 215 ++++------- .../plugins/panel/heatmap/HeatmapTooltip.tsx | 6 +- public/app/plugins/panel/heatmap/module.tsx | 4 +- public/app/plugins/panel/heatmap/utils.ts | 106 +----- .../state-timeline/StateTimelinePanel.tsx | 300 ++++------------ .../state-timeline/StateTimelineTooltip.tsx | 129 ------- .../state-timeline/StateTimelineTooltip2.tsx | 3 +- .../status-history/StatusHistoryPanel.tsx | 284 ++++----------- .../status-history/StatusHistoryTooltip.tsx | 98 ------ .../panel/timeseries/TimeSeriesPanel.tsx | 232 ++++-------- .../panel/timeseries/TimeSeriesTooltip.tsx | 4 +- .../plugins/AnnotationEditorPlugin.tsx | 158 --------- .../timeseries/plugins/AnnotationsPlugin.tsx | 165 --------- .../timeseries/plugins/AnnotationsPlugin2.tsx | 6 +- .../timeseries/plugins/ContextMenuPlugin.tsx | 230 ------------ .../plugins/annotations/AnnotationEditor.tsx | 141 -------- .../annotations/AnnotationEditorForm.tsx | 182 ---------- .../plugins/annotations/AnnotationMarker.tsx | 167 --------- .../plugins/annotations/AnnotationTooltip.tsx | 152 -------- .../annotations2/AnnotationMarker2.tsx | 2 + .../panel/timeseries/plugins/styles.ts | 27 -- public/app/plugins/panel/timeseries/utils.ts | 4 +- public/app/plugins/panel/trend/TrendPanel.tsx | 61 ++-- .../app/plugins/panel/xychart/TooltipView.tsx | 184 ---------- .../plugins/panel/xychart/XYChartPanel.tsx | 84 +---- public/app/plugins/panel/xychart/scatter.ts | 89 +---- public/app/plugins/panel/xychart/types.ts | 20 -- .../plugins/panel/xychart/v2/XYChartPanel.tsx | 1 - 61 files changed, 408 insertions(+), 4243 deletions(-) delete mode 100644 packages/grafana-ui/src/components/uPlot/config/addTooltipSupport.ts delete mode 100644 packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx delete mode 100644 packages/grafana-ui/src/components/uPlot/plugins/ZoomPlugin.tsx delete mode 100644 public/app/core/components/GraphNG/hooks.ts delete mode 100644 public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx delete mode 100644 public/app/plugins/panel/state-timeline/StateTimelineTooltip.tsx delete mode 100644 public/app/plugins/panel/status-history/StatusHistoryTooltip.tsx delete mode 100644 public/app/plugins/panel/timeseries/plugins/AnnotationEditorPlugin.tsx delete mode 100644 public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin.tsx delete mode 100644 public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx delete mode 100644 public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditor.tsx delete mode 100644 public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditorForm.tsx delete mode 100644 public/app/plugins/panel/timeseries/plugins/annotations/AnnotationMarker.tsx delete mode 100644 public/app/plugins/panel/timeseries/plugins/annotations/AnnotationTooltip.tsx delete mode 100644 public/app/plugins/panel/timeseries/plugins/styles.ts delete mode 100644 public/app/plugins/panel/xychart/TooltipView.tsx diff --git a/.betterer.results b/.betterer.results index 10765429aa9..5fef2168770 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1166,9 +1166,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "8"], [0, 0, 0, "Do not use any type assertions.", "9"] ], - "public/app/core/components/GraphNG/hooks.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/core/components/NestedFolderPicker/Trigger.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], @@ -5872,29 +5869,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "5"], [0, 0, 0, "Styles should be written using objects.", "6"] ], - "public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditor.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditorForm.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"] - ], - "public/app/plugins/panel/timeseries/plugins/annotations/AnnotationTooltip.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"], - [0, 0, 0, "Styles should be written using objects.", "8"] - ], "public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -5904,10 +5878,6 @@ exports[`better eslint`] = { "public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/panel/timeseries/plugins/styles.ts:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "public/app/plugins/panel/traces/TracesPanel.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], @@ -5927,9 +5897,6 @@ exports[`better eslint`] = { "public/app/plugins/panel/xychart/ManualEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/plugins/panel/xychart/TooltipView.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/plugins/panel/xychart/XYChartPanel.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx index 802debb59c8..5bb8a3d276e 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx @@ -13,15 +13,22 @@ interface VizTooltipContentProps { children?: ReactNode; scrollable?: boolean; isPinned: boolean; + maxHeight?: number; } -export const VizTooltipContent = ({ items, children, isPinned, scrollable = false }: VizTooltipContentProps) => { +export const VizTooltipContent = ({ + items, + children, + isPinned, + scrollable = false, + maxHeight, +}: VizTooltipContentProps) => { const styles = useStyles2(getStyles); const scrollableStyle: CSSProperties = scrollable ? { - maxHeight: 400, - overflowY: 'auto', + maxHeight: maxHeight, + overflowY: 'scroll', } : {}; diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index baf292e42b4..68aa92e6f66 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -4,7 +4,7 @@ import uPlot, { Cursor, Band, Hooks, Select, AlignedData, Padding, Series } from import { DataFrame, DefaultTimeZone, Field, getTimeZoneInfo, GrafanaTheme2, TimeRange, TimeZone } from '@grafana/data'; import { AxisPlacement, VizOrientation } from '@grafana/schema'; -import { FacetedData, PlotConfig, PlotTooltipInterpolator } from '../types'; +import { FacetedData, PlotConfig } from '../types'; import { DEFAULT_PLOT_CONFIG, getStackingBands, pluginLog, StackingGroup } from '../utils'; import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder'; @@ -30,9 +30,11 @@ type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData; type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData; export class UPlotConfigBuilder { + readonly uid = Math.random().toString(36).slice(2); + series: UPlotSeriesBuilder[] = []; private axes: Record = {}; - private scales: UPlotScaleBuilder[] = []; + readonly scales: UPlotScaleBuilder[] = []; private bands: Band[] = []; private stackingGroups: StackingGroup[] = []; private cursor: Cursor | undefined; @@ -40,13 +42,10 @@ export class UPlotConfigBuilder { private hasLeftAxis = false; private hooks: Hooks.Arrays = {}; private tz: string | undefined = undefined; - private sync = false; private mode: uPlot.Mode = 1; private frames: DataFrame[] | undefined = undefined; // to prevent more than one threshold per scale private thresholds: Record = {}; - // Custom handler for closest datapoint and series lookup - private tooltipInterpolator: PlotTooltipInterpolator | undefined = undefined; private padding?: Padding = undefined; private cachedConfig?: PlotConfig; @@ -155,14 +154,6 @@ export class UPlotConfigBuilder { return this.stackingGroups; } - setTooltipInterpolator(interpolator: PlotTooltipInterpolator) { - this.tooltipInterpolator = interpolator; - } - - getTooltipInterpolator() { - return this.tooltipInterpolator; - } - setPrepData(prepData: PreDataStacked) { this.prepData = (frames) => { this.frames = frames; @@ -170,14 +161,6 @@ export class UPlotConfigBuilder { }; } - setSync() { - this.sync = true; - } - - hasSync() { - return this.sync; - } - setPadding(padding: Padding) { this.padding = padding; } @@ -301,8 +284,6 @@ type UPlotConfigPrepOpts = {}> = { renderers?: Renderers; tweakScale?: (opts: ScaleProps, forField: Field) => ScaleProps; tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps; - // Identifies the shared key for uPlot cursor sync - eventsScope?: string; hoverProximity?: number; orientation?: VizOrientation; } & T; diff --git a/packages/grafana-ui/src/components/uPlot/config/addTooltipSupport.ts b/packages/grafana-ui/src/components/uPlot/config/addTooltipSupport.ts deleted file mode 100644 index 4932cb1ab70..00000000000 --- a/packages/grafana-ui/src/components/uPlot/config/addTooltipSupport.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { Dispatch, MutableRefObject, SetStateAction } from 'react'; - -import { CartesianCoords2D, DashboardCursorSync } from '@grafana/data'; - -import { positionTooltip } from '../plugins/TooltipPlugin'; - -import { UPlotConfigBuilder } from './UPlotConfigBuilder'; - -export type HoverEvent = { - xIndex: number; - yIndex: number; - pageX: number; - pageY: number; -}; - -type SetupConfigParams = { - config: UPlotConfigBuilder; - onUPlotClick: () => void; - setFocusedSeriesIdx: Dispatch>; - setFocusedPointIdx: Dispatch>; - setCoords: Dispatch>; - setHover: Dispatch>; - isToolTipOpen: MutableRefObject; - isActive: boolean; - setIsActive: Dispatch>; - sync?: (() => DashboardCursorSync) | undefined; -}; - -// This applies config hooks to setup tooltip listener. Ideally this could happen in the same `prepConfig` function -// however the GraphNG structures do not allow access to the `setHover` callback -export const addTooltipSupport = ({ - config, - onUPlotClick, - setFocusedSeriesIdx, - setFocusedPointIdx, - setCoords, - setHover, - isToolTipOpen, - isActive, - setIsActive, - sync, -}: SetupConfigParams): UPlotConfigBuilder => { - // Ensure tooltip is closed on config changes - isToolTipOpen.current = false; - - const onMouseEnter = () => { - if (setIsActive) { - setIsActive(true); - } - }; - - const onMouseLeave = () => { - if (!isToolTipOpen.current) { - setCoords(null); - - if (setIsActive) { - setIsActive(false); - } - } - }; - - let ref_parent: HTMLElement | null = null; - let ref_over: HTMLElement | null = null; - config.addHook('init', (u) => { - ref_parent = u.root.parentElement; - ref_over = u.over; - ref_parent?.addEventListener('click', onUPlotClick); - ref_over.addEventListener('mouseleave', onMouseLeave); - ref_over.addEventListener('mouseenter', onMouseEnter); - - if (sync && sync() === DashboardCursorSync.Crosshair) { - u.root.classList.add('shared-crosshair'); - } - }); - - const clearPopupIfOpened = () => { - if (isToolTipOpen.current) { - setCoords(null); - onUPlotClick(); - } - }; - - config.addHook('drawClear', clearPopupIfOpened); - - config.addHook('destroy', () => { - ref_parent?.removeEventListener('click', onUPlotClick); - ref_over?.removeEventListener('mouseleave', onMouseLeave); - ref_over?.removeEventListener('mouseenter', onMouseEnter); - clearPopupIfOpened(); - }); - - let rect: DOMRect; - // rect of .u-over (grid area) - config.addHook('syncRect', (u, r) => { - rect = r; - }); - - const tooltipInterpolator = config.getTooltipInterpolator(); - if (tooltipInterpolator) { - config.addHook('setCursor', (u) => { - if (isToolTipOpen.current) { - return; - } - - tooltipInterpolator( - setFocusedSeriesIdx, - setFocusedPointIdx, - (clear) => { - if (clear) { - setCoords(null); - return; - } - - if (!rect) { - return; - } - - const { x, y } = positionTooltip(u, rect); - if (x !== undefined && y !== undefined) { - setCoords({ canvas: { x: u.cursor.left!, y: u.cursor.top! }, viewport: { x, y } }); - } - }, - u - ); - }); - } - - config.addHook('setLegend', (u) => { - if (!isToolTipOpen.current && !tooltipInterpolator) { - setFocusedPointIdx(u.legend.idx!); - } - if (u.cursor.idxs != null) { - for (let i = 0; i < u.cursor.idxs.length; i++) { - const sel = u.cursor.idxs[i]; - if (sel != null) { - const hover: HoverEvent = { - xIndex: sel, - yIndex: 0, - pageX: rect.left + u.cursor.left!, - pageY: rect.top + u.cursor.top!, - }; - - if (!isToolTipOpen.current || !hover) { - setHover(hover); - } - - return; // only show the first one - } - } - } - }); - - config.addHook('setSeries', (_, idx) => { - if (!isToolTipOpen.current) { - setFocusedSeriesIdx(idx); - } - }); - - return config; -}; diff --git a/packages/grafana-ui/src/components/uPlot/plugins/EventBusPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/EventBusPlugin.tsx index f3528620dec..1de635d5de6 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/EventBusPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/EventBusPlugin.tsx @@ -17,14 +17,13 @@ import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; interface EventBusPluginProps { config: UPlotConfigBuilder; eventBus: EventBus; - sync: () => boolean; frame?: DataFrame; } /** * @alpha */ -export const EventBusPlugin = ({ config, eventBus, sync, frame }: EventBusPluginProps) => { +export const EventBusPlugin = ({ config, eventBus, frame }: EventBusPluginProps) => { const frameRef = useRef(frame); frameRef.current = frame; @@ -51,7 +50,7 @@ export const EventBusPlugin = ({ config, eventBus, sync, frame }: EventBusPlugin config.addHook('setLegend', () => { let viaSync = u!.cursor.event == null; - if (!viaSync && sync()) { + if (!viaSync) { let dataIdx = u!.cursor.idxs!.find((v) => v != null); if (dataIdx == null) { diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx deleted file mode 100644 index 6387573973f..00000000000 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx +++ /dev/null @@ -1,333 +0,0 @@ -import { css } from '@emotion/css'; -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { useMountedState } from 'react-use'; -import uPlot from 'uplot'; - -import { - arrayUtils, - CartesianCoords2D, - DashboardCursorSync, - DataFrame, - FALLBACK_COLOR, - FieldType, - formattedValueToString, - getDisplayProcessor, - getFieldDisplayName, - GrafanaTheme2, - TimeZone, -} from '@grafana/data'; -import { TooltipDisplayMode, SortOrder } from '@grafana/schema'; - -import { useStyles2, useTheme2 } from '../../../themes/ThemeContext'; -import { Portal } from '../../Portal/Portal'; -import { SeriesTable, SeriesTableRowProps, VizTooltipContainer } from '../../VizTooltip'; -import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; -import { findMidPointYPosition, pluginLog } from '../utils'; - -interface TooltipPluginProps { - timeZone: TimeZone; - data: DataFrame; - frames?: DataFrame[]; - config: UPlotConfigBuilder; - mode?: TooltipDisplayMode; - sortOrder?: SortOrder; - sync?: () => DashboardCursorSync; - // Allows custom tooltip content rendering. Exposes aligned data frame with relevant indexes for data inspection - // Use field.state.origin indexes from alignedData frame field to get access to original data frame and field index. - renderTooltip?: (alignedFrame: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => React.ReactNode; -} - -const TOOLTIP_OFFSET = 10; - -/** - * @alpha - */ -export const TooltipPlugin = ({ - mode = TooltipDisplayMode.Single, - sortOrder = SortOrder.None, - sync, - timeZone, - config, - renderTooltip, - ...otherProps -}: TooltipPluginProps) => { - const plotInstance = useRef(); - const theme = useTheme2(); - const [focusedSeriesIdx, setFocusedSeriesIdx] = useState(null); - const [focusedPointIdx, setFocusedPointIdx] = useState(null); - const [focusedPointIdxs, setFocusedPointIdxs] = useState>([]); - const [coords, setCoords] = useState(null); - const [isActive, setIsActive] = useState(false); - const isMounted = useMountedState(); - let parentWithFocus: HTMLElement | null = null; - - const pluginId = `TooltipPlugin`; - - const style = useStyles2(getStyles); - - // Debug logs - useEffect(() => { - pluginLog(pluginId, true, `Focused series: ${focusedSeriesIdx}, focused point: ${focusedPointIdx}`); - }, [focusedPointIdx, focusedSeriesIdx]); - - // Add uPlot hooks to the config, or re-add when the config changed - useLayoutEffect(() => { - let bbox: DOMRect | undefined = undefined; - - const plotEnter = () => { - if (!isMounted()) { - return; - } - setIsActive(true); - plotInstance.current?.root.classList.add('plot-active'); - }; - - const plotLeave = () => { - if (!isMounted()) { - return; - } - setCoords(null); - setIsActive(false); - plotInstance.current?.root.classList.remove('plot-active'); - }; - - // cache uPlot plotting area bounding box - config.addHook('syncRect', (u, rect) => (bbox = rect)); - - config.addHook('init', (u) => { - plotInstance.current = u; - - u.over.addEventListener('mouseenter', plotEnter); - u.over.addEventListener('mouseleave', plotLeave); - - parentWithFocus = u.root.closest('[tabindex]'); - - if (parentWithFocus) { - parentWithFocus.addEventListener('focus', plotEnter); - parentWithFocus.addEventListener('blur', plotLeave); - } - - if (sync && sync() === DashboardCursorSync.Crosshair) { - u.root.classList.add('shared-crosshair'); - } - }); - - const tooltipInterpolator = config.getTooltipInterpolator(); - - if (tooltipInterpolator) { - // Custom toolitp positioning - config.addHook('setCursor', (u) => { - tooltipInterpolator( - setFocusedSeriesIdx, - setFocusedPointIdx, - (clear) => { - if (clear) { - setCoords(null); - return; - } - - if (!bbox) { - return; - } - - const { x, y } = positionTooltip(u, bbox); - if (x !== undefined && y !== undefined) { - setCoords({ x, y }); - } - }, - u - ); - }); - } else { - config.addHook('setLegend', (u) => { - if (!isMounted()) { - return; - } - setFocusedPointIdx(u.legend.idx!); - setFocusedPointIdxs(u.legend.idxs!.slice()); - }); - - // default series/datapoint idx retireval - config.addHook('setCursor', (u) => { - if (!bbox || !isMounted()) { - return; - } - - const { x, y } = positionTooltip(u, bbox); - if (x !== undefined && y !== undefined) { - setCoords({ x, y }); - } else { - setCoords(null); - } - }); - - config.addHook('setSeries', (_, idx) => { - if (!isMounted()) { - return; - } - setFocusedSeriesIdx(idx); - }); - } - - return () => { - setCoords(null); - - if (plotInstance.current) { - plotInstance.current.over.removeEventListener('mouseleave', plotLeave); - plotInstance.current.over.removeEventListener('mouseenter', plotEnter); - - if (parentWithFocus) { - parentWithFocus.removeEventListener('focus', plotEnter); - parentWithFocus.removeEventListener('blur', plotLeave); - } - } - }; - }, [config, setCoords, setIsActive, setFocusedPointIdx, setFocusedPointIdxs]); - - if (focusedPointIdx === null || (!isActive && sync && sync() === DashboardCursorSync.Crosshair)) { - return null; - } - - // GraphNG expects aligned data, let's take field 0 as x field. FTW - let xField = otherProps.data.fields[0]; - if (!xField) { - return null; - } - const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, timeZone, theme }); - let tooltip: React.ReactNode = null; - - let xVal = xFieldFmt(xField!.values[focusedPointIdx]).text; - - if (!renderTooltip) { - // when interacting with a point in single mode - if (mode === TooltipDisplayMode.Single && focusedSeriesIdx !== null) { - const field = otherProps.data.fields[focusedSeriesIdx]; - - if (!field) { - return null; - } - - const dataIdx = focusedPointIdxs?.[focusedSeriesIdx] ?? focusedPointIdx; - xVal = xFieldFmt(xField!.values[dataIdx]).text; - const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme }); - const display = fieldFmt(field.values[dataIdx]); - - tooltip = ( - - ); - } - - if (mode === TooltipDisplayMode.Multi) { - let series: SeriesTableRowProps[] = []; - const frame = otherProps.data; - const fields = frame.fields; - const sortIdx: unknown[] = []; - - for (let i = 0; i < fields.length; i++) { - const field = frame.fields[i]; - if ( - !field || - field === xField || - field.type === FieldType.time || - field.type !== FieldType.number || - field.config.custom?.hideFrom?.tooltip || - field.config.custom?.hideFrom?.viz - ) { - continue; - } - - const v = otherProps.data.fields[i].values[focusedPointIdxs[i]!]; - const display = field.display!(v); - - sortIdx.push(v); - series.push({ - color: display.color || FALLBACK_COLOR, - label: getFieldDisplayName(field, frame, otherProps.frames), - value: display ? formattedValueToString(display) : null, - isActive: focusedSeriesIdx === i, - }); - } - - if (sortOrder !== SortOrder.None) { - // create sort reference series array, as Array.sort() mutates the original array - const sortRef = [...series]; - const sortFn = arrayUtils.sortValues(sortOrder); - - series.sort((a, b) => { - // get compared values indices to retrieve raw values from sortIdx - const aIdx = sortRef.indexOf(a); - const bIdx = sortRef.indexOf(b); - return sortFn(sortIdx[aIdx], sortIdx[bIdx]); - }); - } - - tooltip = ; - } - } else { - tooltip = renderTooltip(otherProps.data, focusedSeriesIdx, focusedPointIdx); - } - - return ( - - {tooltip && coords && ( - - {tooltip} - - )} - - ); -}; - -function isCursorOutsideCanvas({ left, top }: uPlot.Cursor, canvas: DOMRect) { - if (left === undefined || top === undefined) { - return false; - } - return left < 0 || left > canvas.width || top < 0 || top > canvas.height; -} - -/** - * Given uPlot cursor position, figure out position of the tooltip withing the canvas bbox - * Tooltip is positioned relatively to a viewport - * @internal - **/ -export function positionTooltip(u: uPlot, bbox: DOMRect) { - let x, y; - const cL = u.cursor.left || 0; - const cT = u.cursor.top || 0; - - if (isCursorOutsideCanvas(u.cursor, bbox)) { - const idx = u.posToIdx(cL); - // when cursor outside of uPlot's canvas - if (cT < 0 || cT > bbox.height) { - let pos = findMidPointYPosition(u, idx); - - if (pos) { - y = bbox.top + pos; - if (cL >= 0 && cL <= bbox.width) { - // find x-scale position for a current cursor left position - x = bbox.left + u.valToPos(u.data[0][u.posToIdx(cL)], u.series[0].scale!); - } - } - } - } else { - x = bbox.left + cL; - y = bbox.top + cT; - } - - return { x, y }; -} - -const getStyles = (theme: GrafanaTheme2) => ({ - tooltipWrapper: css({ - 'z-index': theme.zIndex.portal + 1 + ' !important', - }), -}); diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index 0a54874a5d0..13c6b797c08 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -4,6 +4,7 @@ import { createPortal } from 'react-dom'; import uPlot from 'uplot'; import { GrafanaTheme2 } from '@grafana/data'; +import { DashboardCursorSync } from '@grafana/schema'; import { useStyles2 } from '../../../themes'; import { getPortalContainer } from '../../Portal/Portal'; @@ -29,7 +30,8 @@ interface TooltipPlugin2Props { config: UPlotConfigBuilder; hoverMode: TooltipHoverMode; - syncTooltip?: () => boolean; + syncMode?: DashboardCursorSync; + syncScope?: string; // x only queryZoom?: (range: { from: number; to: number }) => void; @@ -48,7 +50,6 @@ interface TooltipPlugin2Props { ) => React.ReactNode; maxWidth?: number; - maxHeight?: number; } interface TooltipContainerState { @@ -108,8 +109,8 @@ export const TooltipPlugin2 = ({ clientZoom = false, queryZoom, maxWidth, - maxHeight, - syncTooltip = () => false, + syncMode = DashboardCursorSync.Off, + syncScope = 'global', // eventsScope }: TooltipPlugin2Props) => { const domRef = useRef(null); const portalRoot = useRef(null); @@ -123,8 +124,7 @@ export const TooltipPlugin2 = ({ const sizeRef = useRef(); maxWidth = isPinned ? DEFAULT_TOOLTIP_WIDTH : maxWidth ?? DEFAULT_TOOLTIP_WIDTH; - maxHeight ??= DEFAULT_TOOLTIP_HEIGHT; - const styles = useStyles2(getStyles, maxWidth, maxHeight); + const styles = useStyles2(getStyles, maxWidth); const renderRef = useRef(render); renderRef.current = render; @@ -159,9 +159,20 @@ export const TooltipPlugin2 = ({ let plotVisible = false; + const syncTooltip = syncMode === DashboardCursorSync.Tooltip; + + if (syncMode !== DashboardCursorSync.Off && config.scales[0].props.isTime) { + config.setCursor({ + sync: { + key: syncScope, + scales: ['x', null], + }, + }); + } + const updateHovering = () => { if (viaSync) { - _isHovering = plotVisible && _someSeriesIdx && syncTooltip(); + _isHovering = plotVisible && _someSeriesIdx && syncTooltip; } else { _isHovering = closestSeriesIdx != null || (hoverMode === TooltipHoverMode.xAll && _someSeriesIdx); } @@ -570,7 +581,7 @@ export const TooltipPlugin2 = ({ return null; }; -const getStyles = (theme: GrafanaTheme2, maxWidth?: number, maxHeight?: number) => ({ +const getStyles = (theme: GrafanaTheme2, maxWidth?: number) => ({ tooltipWrapper: css({ top: 0, left: 0, @@ -583,8 +594,6 @@ const getStyles = (theme: GrafanaTheme2, maxWidth?: number, maxHeight?: number) boxShadow: theme.shadows.z2, userSelect: 'text', maxWidth: maxWidth ?? 'none', - maxHeight: maxHeight ?? 'none', - overflowY: 'auto', }), pinned: css({ boxShadow: theme.shadows.z3, diff --git a/packages/grafana-ui/src/components/uPlot/plugins/ZoomPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/ZoomPlugin.tsx deleted file mode 100644 index 1e412e41fcc..00000000000 --- a/packages/grafana-ui/src/components/uPlot/plugins/ZoomPlugin.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useLayoutEffect } from 'react'; - -import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; - -interface ZoomPluginProps { - onZoom: (range: { from: number; to: number }) => void; - withZoomY?: boolean; - config: UPlotConfigBuilder; -} - -// min px width that triggers zoom -const MIN_ZOOM_DIST = 5; - -const maybeZoomAction = (e?: MouseEvent | null) => e != null && !e.ctrlKey && !e.metaKey; - -/** - * @alpha - */ -export const ZoomPlugin = ({ onZoom, config, withZoomY = false }: ZoomPluginProps) => { - useLayoutEffect(() => { - let yZoomed = false; - let yDrag = false; - - if (withZoomY) { - config.addHook('init', (u) => { - u.over!.addEventListener( - 'mousedown', - (e) => { - if (!maybeZoomAction(e)) { - return; - } - - if (e.button === 0 && e.shiftKey) { - yDrag = true; - - u.cursor!.drag!.x = false; - u.cursor!.drag!.y = true; - - let onUp = (e: MouseEvent) => { - u.cursor!.drag!.x = true; - u.cursor!.drag!.y = false; - document.removeEventListener('mouseup', onUp, true); - }; - - document.addEventListener('mouseup', onUp, true); - } - }, - true - ); - }); - } - - config.addHook('setSelect', (u) => { - const isXAxisHorizontal = u.scales.x.ori === 0; - if (maybeZoomAction(u.cursor!.event)) { - if (withZoomY && yDrag) { - if (u.select.height >= MIN_ZOOM_DIST) { - for (let key in u.scales!) { - if (key !== 'x') { - const maxY = isXAxisHorizontal - ? u.posToVal(u.select.top, key) - : u.posToVal(u.select.left + u.select.width, key); - const minY = isXAxisHorizontal - ? u.posToVal(u.select.top + u.select.height, key) - : u.posToVal(u.select.left, key); - u.setScale(key, { min: minY, max: maxY }); - } - } - - yZoomed = true; - } - - yDrag = false; - } else { - if (u.select.width >= MIN_ZOOM_DIST) { - const minX = isXAxisHorizontal - ? u.posToVal(u.select.left, 'x') - : u.posToVal(u.select.top + u.select.height, 'x'); - const maxX = isXAxisHorizontal - ? u.posToVal(u.select.left + u.select.width, 'x') - : u.posToVal(u.select.top, 'x'); - - onZoom({ from: minX, to: maxX }); - - yZoomed = false; - } - } - } - - // manually hide selected region (since cursor.drag.setScale = false) - u.setSelect({ left: 0, width: 0, top: 0, height: 0 }, false); - }); - - config.setCursor({ - bind: { - dblclick: (u) => () => { - if (!maybeZoomAction(u.cursor!.event)) { - return null; - } - - if (withZoomY && yZoomed) { - for (let key in u.scales!) { - if (key !== 'x') { - // @ts-ignore (this is not typed correctly in uPlot, assigning nulls means auto-scale / reset) - u.setScale(key, { min: null, max: null }); - } - } - - yZoomed = false; - } else { - let xScale = u.scales.x; - - const frTs = xScale.min!; - const toTs = xScale.max!; - const pad = (toTs - frTs) / 2; - - onZoom({ from: frTs - pad, to: toTs + pad }); - } - - return null; - }, - }, - }); - }, [config]); - - return null; -}; diff --git a/packages/grafana-ui/src/components/uPlot/plugins/index.ts b/packages/grafana-ui/src/components/uPlot/plugins/index.ts index 4f07a8d80f7..0c5f3123c2d 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/index.ts +++ b/packages/grafana-ui/src/components/uPlot/plugins/index.ts @@ -1,5 +1,3 @@ -export { ZoomPlugin } from './ZoomPlugin'; -export { TooltipPlugin } from './TooltipPlugin'; export { TooltipPlugin2 } from './TooltipPlugin2'; export { EventBusPlugin } from './EventBusPlugin'; export { KeyboardPlugin } from './KeyboardPlugin'; diff --git a/packages/grafana-ui/src/components/uPlot/types.ts b/packages/grafana-ui/src/components/uPlot/types.ts index 86f3c99c277..864e5ec274a 100644 --- a/packages/grafana-ui/src/components/uPlot/types.ts +++ b/packages/grafana-ui/src/components/uPlot/types.ts @@ -13,10 +13,6 @@ export type PlotConfig = Pick< 'mode' | 'series' | 'scales' | 'axes' | 'cursor' | 'bands' | 'hooks' | 'select' | 'tzDate' | 'padding' >; -export interface PlotPluginProps { - id: string; -} - export type FacetValues = any[]; export type FacetSeries = FacetValues[]; export type FacetedData = [_: null, ...series: FacetSeries]; diff --git a/packages/grafana-ui/src/components/uPlot/utils.test.ts b/packages/grafana-ui/src/components/uPlot/utils.test.ts index 3ecbc180bd9..a72b806d622 100644 --- a/packages/grafana-ui/src/components/uPlot/utils.test.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.test.ts @@ -3,21 +3,7 @@ import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, Stacki import { preparePlotFrame } from '..'; -import { getStackingGroups, preparePlotData2, timeFormatToTemplate } from './utils'; - -describe('timeFormatToTemplate', () => { - it.each` - format | expected - ${'HH:mm:ss'} | ${'{HH}:{mm}:{ss}'} - ${'HH:mm'} | ${'{HH}:{mm}'} - ${'MM/DD HH:mm'} | ${'{MM}/{DD} {HH}:{mm}'} - ${'MM/DD'} | ${'{MM}/{DD}'} - ${'YYYY-MM'} | ${'{YYYY}-{MM}'} - ${'YYYY'} | ${'{YYYY}'} - `('should convert $format to $expected', ({ format, expected }) => { - expect(timeFormatToTemplate(format)).toEqual(expected); - }); -}); +import { getStackingGroups, preparePlotData2 } from './utils'; describe('preparePlotData2', () => { const df = new MutableDataFrame({ diff --git a/packages/grafana-ui/src/components/uPlot/utils.ts b/packages/grafana-ui/src/components/uPlot/utils.ts index 85f67b256b7..50f6bbc8409 100644 --- a/packages/grafana-ui/src/components/uPlot/utils.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.ts @@ -8,12 +8,6 @@ import { createLogger } from '../../utils/logger'; import { buildScaleKey } from './internal'; -const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g; - -export function timeFormatToTemplate(f: string) { - return f.replace(ALLOWED_FORMAT_STRINGS_REGEX, (match) => `{${match}}`); -} - const paddingSide: PaddingSide = (u, side, sidesWithAxes) => { let hasCrossAxis = side % 2 ? sidesWithAxes[0] || sidesWithAxes[2] : sidesWithAxes[1] || sidesWithAxes[3]; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap index 2aff27a97fa..51fa99f228d 100644 --- a/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap +++ b/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap @@ -74,13 +74,6 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = ` "stroke": [Function], "width": [Function], }, - "sync": { - "key": "__global_", - "scales": [ - "x", - null, - ], - }, }, "focus": { "alpha": 1, diff --git a/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts b/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts index 23775b1df23..c8f17094a27 100644 --- a/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts +++ b/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts @@ -1,9 +1,7 @@ import { createTheme, - DashboardCursorSync, DataFrame, DefaultTimeZone, - // EventBusSrv, FieldColorModeId, FieldConfig, FieldMatcherID, @@ -215,7 +213,6 @@ describe('GraphNG utils', () => { theme: createTheme(), timeZones: [DefaultTimeZone], getTimeRange: getDefaultTimeRange, - sync: () => DashboardCursorSync.Tooltip, allFrames: [frame!], }).getConfig(); expect(result).toMatchSnapshot(); diff --git a/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx b/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx index 9ee08192383..5f6e2070b2b 100644 --- a/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx +++ b/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx @@ -19,7 +19,6 @@ export class UnthemedTimeSeries extends Component { declare context: React.ContextType; prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { - const { eventsScope, sync } = this.context; const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props; return preparePlotConfigBuilder({ @@ -27,12 +26,10 @@ export class UnthemedTimeSeries extends Component { theme, timeZones: Array.isArray(timeZone) ? timeZone : [timeZone], getTimeRange, - sync, allFrames, renderers, tweakScale, tweakAxis, - eventsScope, }); }; diff --git a/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts b/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts index 41090a35a47..f7600b81d92 100644 --- a/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts @@ -2,7 +2,6 @@ import { isNumber } from 'lodash'; import uPlot from 'uplot'; import { - DashboardCursorSync, DataFrame, FieldConfig, FieldType, @@ -71,19 +70,15 @@ const defaultConfig: GraphFieldConfig = { axisPlacement: AxisPlacement.Auto, }; -export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ - sync?: () => DashboardCursorSync; -}> = ({ +export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ frame, theme, timeZones, getTimeRange, - sync, allFrames, renderers, tweakScale = (opts) => opts, tweakAxis = (opts) => opts, - eventsScope = '__global_', }) => { const builder = new UPlotConfigBuilder(timeZones[0]); @@ -103,7 +98,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ } const xScaleKey = 'x'; - let yScaleKey = ''; const xFieldAxisPlacement = xField.config.custom?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden; @@ -264,10 +258,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ ) ); - if (!yScaleKey) { - yScaleKey = scaleKey; - } - if (customConfig.axisPlacement !== AxisPlacement.Hidden) { let axisColor: uPlot.Axis.Stroke | undefined; @@ -543,8 +533,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ r.init(builder, fieldIndices); }); - builder.scaleKeys = [xScaleKey, yScaleKey]; - // if hovered value is null, how far we may scan left/right to hover nearest non-null const hoverProximityPx = 15; @@ -597,20 +585,12 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ }, }; - if (sync && sync() !== DashboardCursorSync.Off) { - cursor.sync = { - key: eventsScope, - scales: [xScaleKey, null], - }; - } - - builder.setSync(); builder.setCursor(cursor); return builder; }; -export function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map { +function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map { const originNames = new Map(); frame.fields.forEach((field, i) => { const origin = field.state?.origin; diff --git a/packages/grafana-ui/src/options/builder/tooltip.tsx b/packages/grafana-ui/src/options/builder/tooltip.tsx index 0ff12becc3e..cce60cdc13c 100644 --- a/packages/grafana-ui/src/options/builder/tooltip.tsx +++ b/packages/grafana-ui/src/options/builder/tooltip.tsx @@ -72,10 +72,10 @@ export function addTooltipOptions( path: 'tooltip.maxHeight', name: 'Max height', category, - defaultValue: 600, + defaultValue: undefined, settings: { integer: true, }, - showIf: (options: T) => false, //options.tooltip?.mode !== TooltipDisplayMode.None, + showIf: (options: T) => options.tooltip?.mode !== TooltipDisplayMode.None, }); } diff --git a/public/app/core/components/GraphNG/GraphNG.tsx b/public/app/core/components/GraphNG/GraphNG.tsx index 193c29705b3..346f43d3aaf 100644 --- a/public/app/core/components/GraphNG/GraphNG.tsx +++ b/public/app/core/components/GraphNG/GraphNG.tsx @@ -13,8 +13,8 @@ import { TimeRange, TimeZone, } from '@grafana/data'; -import { VizLegendOptions } from '@grafana/schema'; -import { Themeable2, PanelContextRoot, VizLayout } from '@grafana/ui'; +import { DashboardCursorSync, VizLegendOptions } from '@grafana/schema'; +import { Themeable2, VizLayout } from '@grafana/ui'; import { UPlotChart } from '@grafana/ui/src/components/uPlot/Plot'; import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; import { Renderers, UPlotConfigBuilder } from '@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder'; @@ -49,6 +49,7 @@ export interface GraphNGProps extends Themeable2 { renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null; replaceVariables: InterpolateFunction; dataLinkPostProcessor?: DataLinkPostProcessor; + cursorSync?: DashboardCursorSync; /** * needed for propsToDiff to re-init the plot & config @@ -86,7 +87,6 @@ export interface GraphNGState { * "Time as X" core component, expects ascending x */ export class GraphNG extends Component { - static contextType = PanelContextRoot; private plotInstance: React.RefObject; constructor(props: GraphNGProps) { @@ -173,17 +173,23 @@ export class GraphNG extends Component { } componentDidUpdate(prevProps: GraphNGProps) { - const { frames, structureRev, timeZone, propsToDiff } = this.props; + const { frames, structureRev, timeZone, cursorSync, propsToDiff } = this.props; const propsChanged = !sameProps(prevProps, this.props, propsToDiff); - if (frames !== prevProps.frames || propsChanged || timeZone !== prevProps.timeZone) { + 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; diff --git a/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap b/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap index 8443971551d..3c8f6a99d86 100644 --- a/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap +++ b/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap @@ -79,13 +79,6 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = ` "stroke": [Function], "width": [Function], }, - "sync": { - "key": "__global_", - "scales": [ - "x", - null, - ], - }, }, "focus": { "alpha": 1, diff --git a/public/app/core/components/GraphNG/hooks.ts b/public/app/core/components/GraphNG/hooks.ts deleted file mode 100644 index c21a7a9f775..00000000000 --- a/public/app/core/components/GraphNG/hooks.ts +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useCallback, useContext } from 'react'; - -import { DataFrame, DataFrameFieldIndex, Field } from '@grafana/data'; - -import { XYFieldMatchers } from './types'; - -/** @alpha */ -interface GraphNGContextType { - mapSeriesIndexToDataFrameFieldIndex: (index: number) => DataFrameFieldIndex; - dimFields: XYFieldMatchers; - data: DataFrame; -} - -/** @alpha */ -export const GraphNGContext = React.createContext({} as GraphNGContextType); - -/** - * @alpha - * Exposes API for data frame inspection in Plot plugins - */ -export const useGraphNGContext = () => { - const { data, dimFields, mapSeriesIndexToDataFrameFieldIndex } = useContext(GraphNGContext); - - const getXAxisField = useCallback(() => { - const xFieldMatcher = dimFields.x; - let xField: Field | null = null; - - for (let j = 0; j < data.fields.length; j++) { - if (xFieldMatcher(data.fields[j], data, [data])) { - xField = data.fields[j]; - break; - } - } - - return xField; - }, [data, dimFields]); - - return { - dimFields, - mapSeriesIndexToDataFrameFieldIndex, - getXAxisField, - alignedData: data, - }; -}; diff --git a/public/app/core/components/GraphNG/utils.test.ts b/public/app/core/components/GraphNG/utils.test.ts index 234a587d0e7..faf0953967f 100644 --- a/public/app/core/components/GraphNG/utils.test.ts +++ b/public/app/core/components/GraphNG/utils.test.ts @@ -1,6 +1,5 @@ import { createTheme, - DashboardCursorSync, DataFrame, DefaultTimeZone, FieldColorModeId, @@ -214,7 +213,6 @@ describe('GraphNG utils', () => { theme: createTheme(), timeZones: [DefaultTimeZone], getTimeRange: getDefaultTimeRange, - sync: () => DashboardCursorSync.Tooltip, allFrames: [frame!], }).getConfig(); expect(result).toMatchSnapshot(); diff --git a/public/app/core/components/TimeSeries/TimeSeries.tsx b/public/app/core/components/TimeSeries/TimeSeries.tsx index 4ecc98b5b51..45626aa2a18 100644 --- a/public/app/core/components/TimeSeries/TimeSeries.tsx +++ b/public/app/core/components/TimeSeries/TimeSeries.tsx @@ -1,7 +1,6 @@ import React, { Component } from 'react'; import { DataFrame, TimeRange } from '@grafana/data'; -import { PanelContextRoot } from '@grafana/ui/src/components/PanelChrome/PanelContext'; import { hasVisibleLegendSeries, PlotLegend } from '@grafana/ui/src/components/uPlot/PlotLegend'; import { UPlotConfigBuilder } from '@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder'; import { withTheme2 } from '@grafana/ui/src/themes/ThemeContext'; @@ -15,11 +14,7 @@ const propsToDiff: Array = ['legend', 'options', 'theme']; type TimeSeriesProps = Omit; export class UnthemedTimeSeries extends Component { - static contextType = PanelContextRoot; - declare context: React.ContextType; - prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { - const { eventsScope, sync } = this.context; const { theme, timeZone, options, renderers, tweakAxis, tweakScale } = this.props; return preparePlotConfigBuilder({ @@ -27,12 +22,10 @@ export class UnthemedTimeSeries extends Component { theme, timeZones: Array.isArray(timeZone) ? timeZone : [timeZone], getTimeRange, - sync, allFrames, renderers, tweakScale, tweakAxis, - eventsScope, hoverProximity: options?.tooltip?.hoverProximity, orientation: options?.orientation, }); diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts index 82b78da39cd..833db010556 100644 --- a/public/app/core/components/TimeSeries/utils.ts +++ b/public/app/core/components/TimeSeries/utils.ts @@ -2,7 +2,6 @@ import { isNumber } from 'lodash'; import uPlot from 'uplot'; import { - DashboardCursorSync, DataFrame, FieldConfig, FieldType, @@ -73,19 +72,15 @@ const defaultConfig: GraphFieldConfig = { axisPlacement: AxisPlacement.Auto, }; -export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ - sync?: () => DashboardCursorSync; -}> = ({ +export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ frame, theme, timeZones, getTimeRange, - sync, allFrames, renderers, tweakScale = (opts) => opts, tweakAxis = (opts) => opts, - eventsScope = '__global_', hoverProximity, orientation = VizOrientation.Horizontal, }) => { @@ -555,8 +550,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ r.init(builder, fieldIndices); }); - builder.scaleKeys = [xScaleKey, yScaleKey]; - // if hovered value is null, how far we may scan left/right to hover nearest non-null const DEFAULT_HOVER_NULL_PROXIMITY = 15; const DEFAULT_FOCUS_PROXIMITY = 30; @@ -586,21 +579,12 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ }, }; - if (xField.type === FieldType.time && sync && sync() !== DashboardCursorSync.Off) { - cursor.sync = { - key: eventsScope, - scales: [xScaleKey, null], - // match: [() => true, () => false], - }; - } - - builder.setSync(); builder.setCursor(cursor); return builder; }; -export function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map { +function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map { const originNames = new Map(); frame.fields.forEach((field, i) => { const origin = field.state?.origin; diff --git a/public/app/core/components/TimelineChart/TimelineChart.tsx b/public/app/core/components/TimelineChart/TimelineChart.tsx index 57048baed16..81ae09d2a7c 100644 --- a/public/app/core/components/TimelineChart/TimelineChart.tsx +++ b/public/app/core/components/TimelineChart/TimelineChart.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data'; import { VisibilityMode, TimelineValueAlignment, TooltipDisplayMode, VizTooltipOptions } from '@grafana/schema'; -import { PanelContext, PanelContextRoot, UPlotConfigBuilder, VizLayout, VizLegend, VizLegendItem } from '@grafana/ui'; +import { UPlotConfigBuilder, VizLayout, VizLegend, VizLegendItem } from '@grafana/ui'; import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG'; @@ -24,10 +24,6 @@ export interface TimelineProps extends Omit { - declare context: React.ContextType; - static contextType = PanelContextRoot; - panelContext: PanelContext | undefined; - getValueColor = (frameIdx: number, fieldIdx: number, value: unknown) => { const field = this.props.frames[frameIdx].fields[fieldIdx]; @@ -42,13 +38,9 @@ export class TimelineChart extends React.Component { }; prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { - this.panelContext = this.context; - const { sync } = this.panelContext; - return preparePlotConfigBuilder({ frame: alignedFrame, getTimeRange, - sync, allFrames: this.props.frames, ...this.props, @@ -92,7 +84,6 @@ export class TimelineChart extends React.Component { prepConfig={this.prepConfig} propsToDiff={propsToDiff} renderLegend={this.renderLegend} - dataLinkPostProcessor={this.panelContext?.dataLinkPostProcessor} /> ); } diff --git a/public/app/core/components/TimelineChart/timeline.ts b/public/app/core/components/TimelineChart/timeline.ts index 5afd52afbdf..09cb5ba8c7c 100644 --- a/public/app/core/components/TimelineChart/timeline.ts +++ b/public/app/core/components/TimelineChart/timeline.ts @@ -53,8 +53,6 @@ export interface TimelineCoreOptions { getTimeRange: () => TimeRange; formatValue?: (seriesIdx: number, value: unknown) => string; getFieldConfig: (seriesIdx: number) => StateTimeLineFieldConfig | StatusHistoryFieldConfig; - onHover: (seriesIdx: number, valueIdx: number, rect: Rect) => void; - onLeave: () => void; hoverMulti: boolean; } @@ -78,8 +76,6 @@ export function getConfig(opts: TimelineCoreOptions) { getTimeRange, getValueColor, getFieldConfig, - onHover, - onLeave, hoverMulti, } = opts; @@ -426,17 +422,7 @@ export function getConfig(opts: TimelineCoreOptions) { let cx = u.cursor.left! * uPlot.pxRatio; let cy = u.cursor.top! * uPlot.pxRatio; - let prevHovered = hoveredAtCursor; - setHovered(cx, cy, u.cursor.event == null); - - if (hoveredAtCursor != null) { - if (hoveredAtCursor !== prevHovered) { - onHover(hoveredAtCursor.sidx, hoveredAtCursor.didx, hoveredAtCursor); - } - } else if (prevHovered != null) { - onLeave(); - } } return hovered[seriesIdx]?.didx; diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 6a7bccfed0a..156dbd1990d 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -1,9 +1,5 @@ -import React from 'react'; -import uPlot from 'uplot'; - import { DataFrame, - DashboardCursorSync, FALLBACK_COLOR, Field, FieldColorModeId, @@ -34,14 +30,7 @@ import { HideableFieldConfig, MappingType, } from '@grafana/schema'; -import { - FIXED_UNIT, - SeriesVisibilityChangeMode, - UPlotConfigBuilder, - UPlotConfigPrepFn, - VizLegendItem, -} from '@grafana/ui'; -import { PlotTooltipInterpolator } from '@grafana/ui/src/components/uPlot/types'; +import { FIXED_UNIT, UPlotConfigBuilder, UPlotConfigPrepFn, VizLegendItem } from '@grafana/ui'; import { preparePlotData2, getStackingGroups } from '@grafana/ui/src/components/uPlot/utils'; import { getConfig, TimelineCoreOptions } from './timeline'; @@ -53,15 +42,12 @@ interface UPlotConfigOptions { frame: DataFrame; theme: GrafanaTheme2; mode: TimelineMode; - sync?: () => DashboardCursorSync; rowHeight?: number; colWidth?: number; showValue: VisibilityMode; alignValue?: TimelineValueAlignment; mergeValues?: boolean; getValueColor: (frameIdx: number, fieldIdx: number, value: unknown) => string; - // Identifies the shared key for uPlot cursor sync - eventsScope?: string; hoverMulti: boolean; } @@ -83,27 +69,18 @@ const defaultConfig: PanelFieldConfig = { fillOpacity: 80, }; -export function mapMouseEventToMode(event: React.MouseEvent): SeriesVisibilityChangeMode { - if (event.ctrlKey || event.metaKey || event.shiftKey) { - return SeriesVisibilityChangeMode.AppendToSelection; - } - return SeriesVisibilityChangeMode.ToggleSelection; -} - export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ frame, theme, timeZones, getTimeRange, mode, - sync, rowHeight, colWidth, showValue, alignValue, mergeValues, getValueColor, - eventsScope = '__global_', hoverMulti, }) => { const builder = new UPlotConfigBuilder(timeZones[0]); @@ -154,50 +131,14 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ( getTimeRange, // hardcoded formatter for state values formatValue: (seriesIdx, value) => formattedValueToString(frame.fields[seriesIdx].display!(value)), - onHover: (seriesIndex, valueIndex) => { - hoveredSeriesIdx = seriesIndex; - hoveredDataIdx = valueIndex; - shouldChangeHover = true; - }, - onLeave: () => { - hoveredSeriesIdx = null; - hoveredDataIdx = null; - shouldChangeHover = true; - }, hoverMulti, }; - let shouldChangeHover = false; - let hoveredSeriesIdx: number | null = null; - let hoveredDataIdx: number | null = null; - const coreConfig = getConfig(opts); builder.addHook('init', coreConfig.init); builder.addHook('drawClear', coreConfig.drawClear); - // in TooltipPlugin, this gets invoked and the result is bound to a setCursor hook - // which fires after the above setCursor hook, so can take advantage of hoveringOver - // already set by the above onHover/onLeave callbacks that fire from coreConfig.setCursor - const interpolateTooltip: PlotTooltipInterpolator = ( - updateActiveSeriesIdx, - updateActiveDatapointIdx, - updateTooltipPosition - ) => { - if (shouldChangeHover) { - if (hoveredSeriesIdx != null) { - updateActiveSeriesIdx(hoveredSeriesIdx); - updateActiveDatapointIdx(hoveredDataIdx); - } - - shouldChangeHover = false; - } - - updateTooltipPosition(hoveredSeriesIdx == null); - }; - - builder.setTooltipInterpolator(interpolateTooltip); - builder.setPrepData((frames) => preparePlotData2(frames[0], getStackingGroups(frames[0]))); builder.setCursor(coreConfig.cursor); @@ -274,58 +215,9 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ( }); } - if (sync && sync() !== DashboardCursorSync.Off) { - let cursor: Partial = {}; - - cursor.sync = { - key: eventsScope, - scales: [xScaleKey, null], - }; - builder.setSync(); - builder.setCursor(cursor); - } - return builder; }; -export function getNamesToFieldIndex(frame: DataFrame): Map { - const names = new Map(); - for (let i = 0; i < frame.fields.length; i++) { - names.set(getFieldDisplayName(frame.fields[i], frame), i); - } - return names; -} - -/** - * If any sequential duplicate values exist, this will return a new array - * with the future values set to undefined. - * - * in: 1, 1,undefined, 1,2, 2,null,2,3 - * out: 1,undefined,undefined,undefined,2,undefined,null,2,3 - */ -export function unsetSameFutureValues(values: unknown[]): unknown[] | undefined { - let prevVal = values[0]; - let clone: unknown[] | undefined = undefined; - - for (let i = 1; i < values.length; i++) { - let value = values[i]; - - if (value === null) { - prevVal = null; - } else { - if (value === prevVal) { - if (!clone) { - clone = [...values]; - } - clone[i] = undefined; - } else if (value != null) { - prevVal = value; - } - } - } - return clone; -} - function getSpanNulls(field: Field) { let spanNulls = field.config.custom?.spanNulls; diff --git a/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx b/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx index f0e2d72fc3b..4703fb4c912 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx @@ -1,26 +1,22 @@ -import { noop } from 'lodash'; -import React, { useEffect, useRef } from 'react'; +import React from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { BehaviorSubject } from 'rxjs'; import { DataFrame, InterpolateFunction, TimeRange } from '@grafana/data'; import { VisibilityMode } from '@grafana/schema'; -import { LegendDisplayMode, UPlotConfigBuilder, useTheme2 } from '@grafana/ui'; +import { LegendDisplayMode, useTheme2 } from '@grafana/ui'; import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart'; import { TimelineMode } from 'app/core/components/TimelineChart/utils'; interface LogTimelineViewerProps { frames: DataFrame[]; timeRange: TimeRange; - onPointerMove?: (seriesIdx: number, pointerIdx: number) => void; } // noop const replaceVariables: InterpolateFunction = (v) => v; -export const LogTimelineViewer = React.memo(({ frames, timeRange, onPointerMove = noop }: LogTimelineViewerProps) => { +export const LogTimelineViewer = React.memo(({ frames, timeRange }: LogTimelineViewerProps) => { const theme = useTheme2(); - const { setupCursorTracking } = useCursorTimelinePosition(onPointerMove); return ( @@ -49,61 +45,10 @@ export const LogTimelineViewer = React.memo(({ frames, timeRange, onPointerMove { label: 'Mixed', color: theme.colors.text.secondary, yAxis: 1 }, ]} replaceVariables={replaceVariables} - > - {(builder) => { - setupCursorTracking(builder); - return null; - }} - + /> )} ); }); -function useCursorTimelinePosition(onPointerMove: (seriesIdx: number, pointIdx: number) => void) { - const pointerSubject = useRef( - new BehaviorSubject<{ seriesIdx: number; pointIdx: number }>({ seriesIdx: 0, pointIdx: 0 }) - ); - - useEffect(() => { - const subscription = pointerSubject.current.subscribe(({ seriesIdx, pointIdx }) => { - onPointerMove && onPointerMove(seriesIdx, pointIdx); - }); - - return () => { - subscription.unsubscribe(); - }; - }, [onPointerMove]); - - // Applies cursor tracking to the UPlot chart - const setupCursorTracking = (builder: UPlotConfigBuilder) => { - builder.setSync(); - const interpolator = builder.getTooltipInterpolator(); - - // I found this in TooltipPlugin.tsx - if (interpolator) { - builder.addHook('setCursor', (u) => { - interpolator( - (seriesIdx) => { - if (seriesIdx) { - const currentPointer = pointerSubject.current.getValue(); - pointerSubject.current.next({ ...currentPointer, seriesIdx }); - } - }, - (pointIdx) => { - if (pointIdx) { - const currentPointer = pointerSubject.current.getValue(); - pointerSubject.current.next({ ...currentPointer, pointIdx }); - } - }, - () => {}, - u - ); - }); - } - }; - - return { setupCursorTracking }; -} - LogTimelineViewer.displayName = 'LogTimelineViewer'; diff --git a/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx b/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx index dd1a16b6908..33de4bfc980 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx @@ -50,7 +50,7 @@ const LokiStateHistory = ({ ruleUID }: Props) => { instancesFilter ); - const { frameSubset, frameSubsetTimestamps, frameTimeRange } = useFrameSubset(dataFrames); + const { frameSubset, frameTimeRange } = useFrameSubset(dataFrames); const onLogRecordLabelClick = useCallback( (label: string) => { @@ -66,26 +66,6 @@ const LokiStateHistory = ({ ruleUID }: Props) => { setValue('query', ''); }, [setInstancesFilter, setValue]); - const refToHighlight = useRef(undefined); - - const onTimelinePointerMove = useCallback( - (seriesIdx: number, pointIdx: number) => { - // remove the highlight from the previous refToHighlight - refToHighlight.current?.classList.remove(styles.highlightedLogRecord); - - const timestamp = frameSubsetTimestamps[pointIdx]; - const newTimestampRef = logsRef.current.get(timestamp); - - // now we have the new ref, add the styles - newTimestampRef?.classList.add(styles.highlightedLogRecord); - // keeping this here (commented) in case we decide we want to go back to this - // newTimestampRef?.scrollIntoView({ behavior: 'smooth', block: 'center' }); - - refToHighlight.current = newTimestampRef; - }, - [frameSubsetTimestamps, styles.highlightedLogRecord] - ); - if (isLoading) { return
Loading...
; } @@ -138,7 +118,7 @@ const LokiStateHistory = ({ ruleUID }: Props) => { ) : ( <>
- +
{hasMoreInstances && (
diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index 4bb4be5753b..37a7bfa1013 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -1,25 +1,20 @@ -import React, { useMemo, useRef, useState } from 'react'; +import React, { useMemo, useRef } from 'react'; import { - CartesianCoords2D, compareDataFrameStructures, DataFrame, Field, FieldColorModeId, FieldType, - getFieldDisplayName, PanelProps, TimeRange, VizOrientation, } from '@grafana/data'; -import { PanelDataErrorView, config } from '@grafana/runtime'; -import { SortOrder } from '@grafana/schema'; +import { PanelDataErrorView } from '@grafana/runtime'; import { GraphGradientMode, measureText, PlotLegend, - Portal, - StackingMode, TooltipDisplayMode, UPlotConfigBuilder, UPLOT_AXIS_FONT_SIZE, @@ -27,23 +22,17 @@ import { useTheme2, VizLayout, VizLegend, - VizTooltipContainer, TooltipPlugin2, } from '@grafana/ui'; -import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport'; import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; -import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { GraphNG, GraphNGProps, PropDiffFn } from 'app/core/components/GraphNG/GraphNG'; import { getFieldLegendItem } from 'app/core/components/TimelineChart/utils'; -import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView'; import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip'; import { Options } from './panelcfg.gen'; import { prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils'; -const TOOLTIP_OFFSET = 10; - /** * @alpha */ @@ -73,29 +62,6 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ const theme = useTheme2(); const { dataLinkPostProcessor } = usePanelContext(); - const oldConfig = useRef(undefined); - const isToolTipOpen = useRef(false); - - const [hover, setHover] = useState(undefined); - const [coords, setCoords] = useState<{ viewport: CartesianCoords2D; canvas: CartesianCoords2D } | null>(null); - const [focusedSeriesIdx, setFocusedSeriesIdx] = useState(null); - const [focusedPointIdx, setFocusedPointIdx] = useState(null); - const [isActive, setIsActive] = useState(false); - const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState(false); - - const onCloseToolTip = () => { - isToolTipOpen.current = false; - setCoords(null); - setShouldDisplayCloseButton(false); - }; - - const onUPlotClick = () => { - isToolTipOpen.current = !isToolTipOpen.current; - - // Linking into useState required to re-render tooltip - setShouldDisplayCloseButton(isToolTipOpen.current); - }; - const frame0Ref = useRef(); const colorByFieldRef = useRef(); @@ -157,51 +123,9 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ ); } - const renderTooltip = (alignedFrame: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => { - const field = seriesIdx == null ? null : alignedFrame.fields[seriesIdx]; - if (field) { - const disp = getFieldDisplayName(field, alignedFrame); - seriesIdx = info.aligned.fields.findIndex((f) => disp === getFieldDisplayName(f, info.aligned)); - } - const tooltipMode = - options.fullHighlight && options.stacking !== StackingMode.None ? TooltipDisplayMode.Multi : options.tooltip.mode; - - const tooltipSort = options.tooltip.mode === TooltipDisplayMode.Multi ? options.tooltip.sort : SortOrder.None; - - return ( - <> - {shouldDisplayCloseButton && ( -
- -
- )} - - - ); - }; - const renderLegend = (config: UPlotConfigBuilder) => { const { legend } = options; + if (!config || legend.showLegend === false) { return null; } @@ -309,8 +233,6 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ }); }; - const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); - return ( {(config) => { - if (showNewVizTooltips && options.tooltip.mode !== TooltipDisplayMode.None) { + if (options.tooltip.mode !== TooltipDisplayMode.None) { return ( ); } - if (!showNewVizTooltips && oldConfig.current !== config) { - oldConfig.current = addTooltipSupport({ - config, - onUPlotClick, - setFocusedSeriesIdx, - setFocusedPointIdx, - setCoords, - setHover, - isToolTipOpen, - isActive, - setIsActive, - }); - } - - if (options.tooltip.mode === TooltipDisplayMode.None) { - return null; - } - - return ( - - {hover && coords && focusedSeriesIdx && ( - - {renderTooltip(info.viz[0], focusedSeriesIdx, focusedPointIdx)} - - )} - - ); + return null; }} ); diff --git a/public/app/plugins/panel/barchart/bars.ts b/public/app/plugins/panel/barchart/bars.ts index af1d287eae7..392b1c0722a 100644 --- a/public/app/plugins/panel/barchart/bars.ts +++ b/public/app/plugins/panel/barchart/bars.ts @@ -10,7 +10,7 @@ import { VizTextDisplayOptions, VizLegendOptions, } from '@grafana/schema'; -import { measureText, PlotTooltipInterpolator } from '@grafana/ui'; +import { measureText } from '@grafana/ui'; import { timeUnitSize } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; import { StackingGroup, preparePlotData2 } from '@grafana/ui/src/components/uPlot/utils'; @@ -56,8 +56,6 @@ export interface BarsOptions { formatShortValue: (seriesIdx: number, value: unknown) => string; timeZone?: TimeZone; text?: VizTextDisplayOptions; - onHover?: (seriesIdx: number, valueIdx: number) => void; - onLeave?: (seriesIdx: number, valueIdx: number) => void; hoverMulti?: boolean; legend?: VizLegendOptions; xSpacing?: number; @@ -634,22 +632,6 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { u.ctx.restore(); }; - // handle hover interaction with quadtree probing - const interpolateTooltip: PlotTooltipInterpolator = ( - updateActiveSeriesIdx, - updateActiveDatapointIdx, - updateTooltipPosition, - u - ) => { - if (hRect) { - updateActiveSeriesIdx(hRect.sidx); - updateActiveDatapointIdx(hRect.didx); - updateTooltipPosition(); - } else { - updateTooltipPosition(true); - } - }; - let alignedTotals: AlignedData | null = null; function prepData(frames: DataFrame[], stackingGroups: StackingGroup[]) { @@ -673,7 +655,6 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { init, drawClear, draw, - interpolateTooltip, prepData, }; } diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index e170ead77bd..3c466c40376 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -17,7 +17,6 @@ import { getFieldDisplayName, } from '@grafana/data'; import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames'; -import { config as runtimeConfig } from '@grafana/runtime'; import { AxisColorMode, AxisPlacement, @@ -138,9 +137,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ builder.addHook('drawClear', config.drawClear); builder.addHook('draw', config.draw); - const showNewVizTooltips = Boolean(runtimeConfig.featureToggles.newVizTooltips); - !showNewVizTooltips && builder.setTooltipInterpolator(config.interpolateTooltip); - if (xTickLabelRotation !== 0) { // these are the amount of space we already have available between plot edge and first label // TODO: removing these hardcoded value requires reading back uplot instance props diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index fc920337f0b..5c535d61fd9 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -1,7 +1,7 @@ // this file is pretty much a copy-paste of TimeSeriesPanel.tsx :( // with some extra renderers passed to the component -import React, { useMemo, useState, useCallback } from 'react'; +import React, { useMemo, useState } from 'react'; import uPlot from 'uplot'; import { Field, getDisplayProcessor, PanelProps } from '@grafana/data'; @@ -10,12 +10,10 @@ import { DashboardCursorSync, TooltipDisplayMode } from '@grafana/schema'; import { EventBusPlugin, KeyboardPlugin, - TooltipPlugin, TooltipPlugin2, UPlotConfigBuilder, usePanelContext, useTheme2, - ZoomPlugin, } from '@grafana/ui'; import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder'; @@ -24,10 +22,7 @@ import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; import { config } from 'app/core/config'; import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip'; -import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin'; -import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin'; import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2'; -import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin'; import { ExemplarsPlugin } from '../timeseries/plugins/ExemplarsPlugin'; import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin'; import { ThresholdControlsPlugin } from '../timeseries/plugins/ThresholdControlsPlugin'; @@ -53,6 +48,7 @@ export const CandlestickPanel = ({ }: CandlestickPanelProps) => { const { sync, + eventsScope, canAddAnnotations, onThresholdsChange, canEditThresholds, @@ -63,25 +59,13 @@ export const CandlestickPanel = ({ const theme = useTheme2(); - // TODO: we should just re-init when this changes, and have this be a static setting - const syncTooltip = useCallback( - () => sync?.() === DashboardCursorSync.Tooltip, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const syncAny = useCallback( - () => sync?.() !== DashboardCursorSync.Off, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - const info = useMemo(() => { return prepareCandlestickFields(data.series, options, theme, timeRange); }, [data.series, options, theme, timeRange]); // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 const [newAnnotationRange, setNewAnnotationRange] = useState(null); + const cursorSync = sync?.() ?? DashboardCursorSync.Off; const { renderers, tweakScale, tweakAxis, shouldRenderPrice } = useMemo(() => { let tweakScale = (opts: ScaleProps, forField: Field) => opts; @@ -264,7 +248,6 @@ export const CandlestickPanel = ({ } const enableAnnotationCreation = Boolean(canAddAnnotations?.()); - const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); return ( {(uplotConfig, alignedFrame) => { return ( <> - - {showNewVizTooltips ? ( + {cursorSync !== DashboardCursorSync.Off && ( + + )} + {options.tooltip.mode !== TooltipDisplayMode.None && ( { if (enableAnnotationCreation && timeRange2 != null) { setNewAnnotationRange(timeRange2); @@ -321,88 +308,24 @@ export const CandlestickPanel = ({ isPinned={isPinned} annotate={enableAnnotationCreation ? annotate : undefined} scrollable={isTooltipScrollable(options.tooltip)} + maxHeight={options.tooltip.maxHeight} /> ); }} maxWidth={options.tooltip.maxWidth} - maxHeight={options.tooltip.maxHeight} /> - ) : ( - <> - - - )} - {/* Renders annotation markers*/} - {showNewVizTooltips ? ( - - ) : ( - data.annotations && ( - - ) - )} - {/* Enables annotations creation*/} - {!showNewVizTooltips ? ( - enableAnnotationCreation ? ( - - {({ startAnnotating }) => { - return ( - { - if (!p) { - return; - } - startAnnotating({ coords: p.coords }); - }, - }, - ], - }, - ] - : [] - } - /> - ); - }} - - ) : ( - - ) - ) : undefined} + + {data.annotations && ( )} - {((canEditThresholds && onThresholdsChange) || showThresholds) && ( )} - - ); }} diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx b/public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx deleted file mode 100644 index f7c73022063..00000000000 --- a/public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import uPlot from 'uplot'; - -import { - DataFrameType, - Field, - FieldType, - formattedValueToString, - getFieldDisplayName, - LinkModel, - TimeRange, - InterpolateFunction, -} from '@grafana/data'; -import { HeatmapCellLayout } from '@grafana/schema'; -import { LinkButton, VerticalGroup } from '@grafana/ui'; -import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; -import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView'; - -import { getDataLinks } from '../status-history/utils'; - -import { HeatmapData } from './fields'; -import { renderHistogram } from './renderHistogram'; -import { HeatmapHoverEvent } from './utils'; - -type Props = { - data: HeatmapData; - hover: HeatmapHoverEvent; - showHistogram?: boolean; - timeRange: TimeRange; - replaceVars: InterpolateFunction; -}; - -export const HeatmapHoverView = (props: Props) => { - if (props.hover.seriesIdx === 2) { - return ; - } - return ; -}; - -const HeatmapHoverCell = ({ data, hover, showHistogram = false }: Props) => { - const index = hover.dataIdx; - - const [isSparse] = useState( - () => data.heatmap?.meta?.type === DataFrameType.HeatmapCells && !isHeatmapCellsDense(data.heatmap) - ); - - const xField = data.heatmap?.fields[0]; - const yField = data.heatmap?.fields[1]; - const countField = data.heatmap?.fields[2]; - - const xDisp = (v: number) => { - if (xField?.display) { - return formattedValueToString(xField.display(v)); - } - if (xField?.type === FieldType.time) { - const tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss'; - const dashboard = getDashboardSrv().getCurrent(); - return dashboard?.formatDate(v, tooltipTimeFormat); - } - return `${v}`; - }; - - const xVals = xField?.values; - const yVals = yField?.values; - const countVals = countField?.values; - - // labeled buckets - const meta = readHeatmapRowsCustomMeta(data.heatmap); - const yDisp = yField?.display ? (v: string) => formattedValueToString(yField.display!(v)) : (v: string) => `${v}`; - - const yValueIdx = index % (data.yBucketCount ?? 1); - const xValueIdx = Math.floor(index / (data.yBucketCount ?? 1)); - - let yBucketMin: string; - let yBucketMax: string; - - let nonNumericOrdinalDisplay: string | undefined = undefined; - - if (meta.yOrdinalDisplay) { - const yMinIdx = data.yLayout === HeatmapCellLayout.le ? yValueIdx - 1 : yValueIdx; - const yMaxIdx = data.yLayout === HeatmapCellLayout.le ? yValueIdx : yValueIdx + 1; - yBucketMin = yMinIdx < 0 ? meta.yMinDisplay! : `${meta.yOrdinalDisplay[yMinIdx]}`; - yBucketMax = `${meta.yOrdinalDisplay[yMaxIdx]}`; - - // e.g. "pod-xyz123" - if (!meta.yOrdinalLabel || Number.isNaN(+meta.yOrdinalLabel[0])) { - nonNumericOrdinalDisplay = data.yLayout === HeatmapCellLayout.le ? yBucketMax : yBucketMin; - } - } else { - const value = yVals?.[yValueIdx]; - - if (data.yLayout === HeatmapCellLayout.le) { - yBucketMax = `${value}`; - - if (data.yLog) { - let logFn = data.yLog === 2 ? Math.log2 : Math.log10; - let exp = logFn(value) - 1 / data.yLogSplit!; - yBucketMin = `${data.yLog ** exp}`; - } else { - yBucketMin = `${value - data.yBucketSize!}`; - } - } else { - yBucketMin = `${value}`; - - if (data.yLog) { - let logFn = data.yLog === 2 ? Math.log2 : Math.log10; - let exp = logFn(value) + 1 / data.yLogSplit!; - yBucketMax = `${data.yLog ** exp}`; - } else { - yBucketMax = `${value + data.yBucketSize!}`; - } - } - } - - let xBucketMin: number; - let xBucketMax: number; - - if (data.xLayout === HeatmapCellLayout.le) { - xBucketMax = xVals?.[index]; - xBucketMin = xBucketMax - data.xBucketSize!; - } else { - xBucketMin = xVals?.[index]; - xBucketMax = xBucketMin + data.xBucketSize!; - } - - const count = countVals?.[index]; - - let links: Array> = []; - - const linksField = data.series?.fields[yValueIdx + 1]; - - if (linksField != null) { - const visible = !Boolean(linksField.config.custom?.hideFrom?.tooltip); - const hasLinks = (linksField.config.links?.length ?? 0) > 0; - - if (visible && hasLinks) { - links = getDataLinks(linksField, xValueIdx); - } - } - - let can = useRef(null); - - let histCssWidth = 264; - let histCssHeight = 64; - let histCanWidth = Math.round(histCssWidth * uPlot.pxRatio); - let histCanHeight = Math.round(histCssHeight * uPlot.pxRatio); - - useEffect( - () => { - if (showHistogram && xVals != null && countVals != null) { - renderHistogram(can, histCanWidth, histCanHeight, xVals, countVals, index, data.yBucketCount!); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [index] - ); - - if (isSparse) { - return ( -
- -
- ); - } - - const renderYBucket = () => { - if (nonNumericOrdinalDisplay) { - return
Name: {nonNumericOrdinalDisplay}
; - } - - switch (data.yLayout) { - case HeatmapCellLayout.unknown: - return
{yDisp(yBucketMin)}
; - } - return ( -
- Bucket: {yDisp(yBucketMin)} - {yDisp(yBucketMax)} -
- ); - }; - - return ( - <> -
-
{xDisp(xBucketMin)}
- {data.xLayout !== HeatmapCellLayout.unknown &&
{xDisp(xBucketMax)}
} -
- {showHistogram && ( - - )} -
- {renderYBucket()} -
- {getFieldDisplayName(countField!, data.heatmap)}: {data.display!(count)} -
-
- {links.length > 0 && ( - - {links.map((link, i) => ( - - {link.title} - - ))} - - )} - - ); -}; diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index 0a0863cf642..26cb18a7af8 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -1,36 +1,33 @@ import { css } from '@emotion/css'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; -import { DashboardCursorSync, DataFrameType, GrafanaTheme2, PanelProps, TimeRange } from '@grafana/data'; -import { config, PanelDataErrorView } from '@grafana/runtime'; +import { DashboardCursorSync, PanelProps, TimeRange } from '@grafana/data'; +import { PanelDataErrorView } from '@grafana/runtime'; import { ScaleDistributionConfig } from '@grafana/schema'; import { - Portal, ScaleDistribution, TooltipPlugin2, TooltipDisplayMode, - ZoomPlugin, UPlotChart, usePanelContext, useStyles2, useTheme2, VizLayout, - VizTooltipContainer, EventBusPlugin, } from '@grafana/ui'; import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { ColorScale } from 'app/core/components/ColorScale/ColorScale'; -import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; +import { readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2'; +import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin'; +import { isTooltipScrollable } from '../timeseries/utils'; -import { ExemplarModalHeader } from './ExemplarModalHeader'; -import { HeatmapHoverView } from './HeatmapHoverViewOld'; import { HeatmapTooltip } from './HeatmapTooltip'; import { prepareHeatmapData } from './fields'; import { quantizeScheme } from './palettes'; import { Options } from './types'; -import { HeatmapHoverEvent, prepConfig } from './utils'; +import { prepConfig } from './utils'; interface HeatmapPanelProps extends PanelProps {} @@ -49,20 +46,8 @@ export const HeatmapPanel = ({ }: HeatmapPanelProps) => { const theme = useTheme2(); const styles = useStyles2(getStyles); - const { sync, canAddAnnotations } = usePanelContext(); - - // TODO: we should just re-init when this changes, and have this be a static setting - const syncTooltip = useCallback( - () => sync?.() === DashboardCursorSync.Tooltip, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const syncAny = useCallback( - () => sync?.() !== DashboardCursorSync.Off, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); + const { sync, eventsScope, canAddAnnotations } = usePanelContext(); + const cursorSync = sync?.() ?? DashboardCursorSync.Off; // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 const [newAnnotationRange, setNewAnnotationRange] = useState(null); @@ -111,35 +96,9 @@ export const HeatmapPanel = ({ return [null, info.heatmap?.fields.map((f) => f.values), [exemplarsXFacet, exemplarsYFacet]]; }, [info.heatmap, info.exemplars]); - const [hover, setHover] = useState(undefined); - const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState(false); - const isToolTipOpen = useRef(false); - - const onCloseToolTip = () => { - isToolTipOpen.current = false; - setShouldDisplayCloseButton(false); - onhover(null); - }; - - const onclick = () => { - isToolTipOpen.current = !isToolTipOpen.current; - - // Linking into useState required to re-render tooltip - setShouldDisplayCloseButton(isToolTipOpen.current); - }; - - const onhover = useCallback( - (evt?: HeatmapHoverEvent | null) => { - setHover(evt ?? undefined); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [options, data.structureRev] - ); - // ugh const dataRef = useRef(info); dataRef.current = info; - const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); const builder = useMemo(() => { const scaleConfig: ScaleDistributionConfig = dataRef.current?.heatmap?.fields[1].config?.custom?.scaleDistribution; @@ -147,12 +106,8 @@ export const HeatmapPanel = ({ return prepConfig({ dataRef, theme, - onhover: !showNewVizTooltips ? onhover : null, - onclick: !showNewVizTooltips && options.tooltip.mode !== TooltipDisplayMode.None ? onclick : null, - isToolTipOpen, timeZone, getTimeRange: () => timeRangeRef.current, - sync, cellGap: options.cellGap, hideLE: options.filterValues?.le, hideGE: options.filterValues?.ge, @@ -160,24 +115,26 @@ export const HeatmapPanel = ({ yAxisConfig: options.yAxis, ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1, }); + // eslint-disable-next-line react-hooks/exhaustive-deps - }, [options, timeZone, data.structureRev]); + }, [options, timeZone, data.structureRev, cursorSync]); const renderLegend = () => { if (!info.heatmap || !options.legend.show) { return null; } - let heatmapType = dataRef.current?.heatmap?.meta?.type; - let isSparseHeatmap = heatmapType === DataFrameType.HeatmapCells && !isHeatmapCellsDense(dataRef.current?.heatmap!); - let countFieldIdx = !isSparseHeatmap ? 2 : 3; - const countField = info.heatmap.fields[countFieldIdx]; - let hoverValue: number | undefined = undefined; + + // let heatmapType = dataRef.current?.heatmap?.meta?.type; + // let isSparseHeatmap = heatmapType === DataFrameType.HeatmapCells && !isHeatmapCellsDense(dataRef.current?.heatmap!); + // let countFieldIdx = !isSparseHeatmap ? 2 : 3; + // const countField = info.heatmap.fields[countFieldIdx]; + // seriesIdx: 1 is heatmap layer; 2 is exemplar layer - if (hover && info.heatmap.fields && hover.seriesIdx === 1) { - hoverValue = countField.values[hover.dataIdx]; - } + // if (hover && info.heatmap.fields && hover.seriesIdx === 1) { + // hoverValue = countField.values[hover.dataIdx]; + // } return ( @@ -212,90 +169,70 @@ export const HeatmapPanel = ({ <> {(vizWidth: number, vizHeight: number) => ( - - - {!showNewVizTooltips && } - {showNewVizTooltips && ( - <> - {options.tooltip.mode !== TooltipDisplayMode.None && ( - { - if (enableAnnotationCreation && timeRange2 != null) { - setNewAnnotationRange(timeRange2); - dismiss(); - return; - } - - const annotate = () => { - let xVal = u.posToVal(u.cursor.left!, 'x'); - - setNewAnnotationRange({ from: xVal, to: xVal }); - dismiss(); - }; - - return ( - - ); - }} - maxWidth={options.tooltip.maxWidth} - maxHeight={options.tooltip.maxHeight} - /> - )} - - + + {cursorSync !== DashboardCursorSync.Off && ( + )} + {options.tooltip.mode !== TooltipDisplayMode.None && ( + { + if (enableAnnotationCreation && timeRange2 != null) { + setNewAnnotationRange(timeRange2); + dismiss(); + return; + } + + const annotate = () => { + let xVal = u.posToVal(u.cursor.left!, 'x'); + + setNewAnnotationRange({ from: xVal, to: xVal }); + dismiss(); + }; + + return ( + + ); + }} + maxWidth={options.tooltip.maxWidth} + /> + )} + + )} - {!showNewVizTooltips && ( - <> - - {hover && options.tooltip.mode !== TooltipDisplayMode.None && ( - - {shouldDisplayCloseButton && } - - - )} - - - )} ); }; -const getStyles = (theme: GrafanaTheme2) => ({ +const getStyles = () => ({ colorScaleWrapper: css({ marginLeft: '25px', padding: '10px 0', diff --git a/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx index ed104d63f2f..db9aa891945 100644 --- a/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx @@ -39,6 +39,8 @@ interface HeatmapTooltipProps { dismiss: () => void; panelData: PanelData; annotate?: () => void; + scrollable?: boolean; + maxHeight?: number; } export const HeatmapTooltip = (props: HeatmapTooltipProps) => { @@ -64,6 +66,8 @@ const HeatmapHoverCell = ({ showColorScale = false, mode, annotate, + scrollable, + maxHeight, }: HeatmapTooltipProps) => { const index = dataIdxs[1]!; const data = dataRef.current; @@ -349,7 +353,7 @@ const HeatmapHoverCell = ({ return (
- + {customContent?.map((content, i) => (
{content} diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index 5ac97b441c6..8468ee7128c 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -428,11 +428,11 @@ export const plugin = new PanelPlugin(HeatmapPanel) path: 'tooltip.maxHeight', name: 'Max height', category, - defaultValue: 600, + defaultValue: undefined, settings: { integer: true, }, - showIf: (options) => false, // config.featureToggles.newVizTooltips && options.tooltip?.mode !== TooltipDisplayMode.None, + showIf: (options) => options.tooltip?.mode !== TooltipDisplayMode.None, }); category = ['Legend']; diff --git a/public/app/plugins/panel/heatmap/utils.ts b/public/app/plugins/panel/heatmap/utils.ts index d6c9589811d..43692cc0cd1 100644 --- a/public/app/plugins/panel/heatmap/utils.ts +++ b/public/app/plugins/panel/heatmap/utils.ts @@ -1,8 +1,7 @@ -import { MutableRefObject, RefObject } from 'react'; +import { RefObject } from 'react'; import uPlot, { Cursor } from 'uplot'; import { - DashboardCursorSync, DataFrameType, formattedValueToString, getValueFormat, @@ -41,25 +40,9 @@ interface PointsBuilderOpts { each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void; } -export interface HeatmapHoverEvent { - seriesIdx: number; - dataIdx: number; - pageX: number; - pageY: number; -} - -export interface HeatmapZoomEvent { - xMin: number; - xMax: number; -} - interface PrepConfigOpts { dataRef: RefObject; theme: GrafanaTheme2; - onhover?: null | ((evt?: HeatmapHoverEvent | null) => void); - onclick?: null | ((evt?: Object) => void); - onzoom?: null | ((evt: HeatmapZoomEvent) => void); - isToolTipOpen?: MutableRefObject; timeZone: string; getTimeRange: () => TimeRange; exemplarColor: string; @@ -68,28 +51,10 @@ interface PrepConfigOpts { hideGE?: number; yAxisConfig: YAxisConfig; ySizeDivisor?: number; - sync?: () => DashboardCursorSync; - // Identifies the shared key for uPlot cursor sync - eventsScope?: string; } export function prepConfig(opts: PrepConfigOpts) { - const { - dataRef, - theme, - onhover, - onclick, - isToolTipOpen, - timeZone, - getTimeRange, - cellGap, - hideLE, - hideGE, - yAxisConfig, - ySizeDivisor, - sync, - eventsScope = '__global_', - } = opts; + const { dataRef, theme, timeZone, getTimeRange, cellGap, hideLE, hideGE, yAxisConfig, ySizeDivisor } = opts; const xScaleKey = 'x'; let isTime = true; @@ -108,8 +73,6 @@ export function prepConfig(opts: PrepConfigOpts) { let builder = new UPlotConfigBuilder(timeZone); - let rect: DOMRect; - builder.addHook('init', (u) => { u.root.querySelectorAll('.u-cursor-pt').forEach((el) => { Object.assign(el.style, { @@ -118,20 +81,6 @@ export function prepConfig(opts: PrepConfigOpts) { background: 'transparent', }); }); - - onclick && - u.over.addEventListener( - 'mouseup', - (e) => { - // @ts-ignore - let isDragging: boolean = u.cursor.drag._x || u.cursor.drag._y; - - if (!isDragging) { - onclick(e); - } - }, - true - ); }); if (isTime) { @@ -153,48 +102,6 @@ export function prepConfig(opts: PrepConfigOpts) { }); } - // rect of .u-over (grid area) - builder.addHook('syncRect', (u, r) => { - rect = r; - }); - - let pendingOnleave: ReturnType | 0; - - onhover && - builder.addHook('setLegend', (u) => { - if (u.cursor.idxs != null) { - for (let i = 0; i < u.cursor.idxs.length; i++) { - const sel = u.cursor.idxs[i]; - if (sel != null) { - const { left, top } = u.cursor; - - if (!isToolTipOpen?.current) { - if (pendingOnleave) { - clearTimeout(pendingOnleave); - pendingOnleave = 0; - } - onhover({ - seriesIdx: i, - dataIdx: sel, - pageX: rect.left + left!, - pageY: rect.top + top!, - }); - } - return; - } - } - } - - if (!isToolTipOpen?.current) { - // if tiles have gaps, reduce flashing / re-render (debounce onleave by 100ms) - if (!pendingOnleave) { - pendingOnleave = setTimeout(() => { - onhover(null); - }, 100); - } - } - }); - builder.addHook('drawClear', (u) => { qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height); @@ -583,15 +490,6 @@ export function prepConfig(opts: PrepConfigOpts) { }, }; - if (sync && sync() !== DashboardCursorSync.Off) { - cursor.sync = { - key: eventsScope, - scales: [xScaleKey, null], - }; - - builder.setSync(); - } - builder.setCursor(cursor); return builder; diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index 93874e7ba4f..d0328fbe018 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -1,22 +1,9 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useMemo, useState } from 'react'; -import { CartesianCoords2D, DashboardCursorSync, DataFrame, FieldType, PanelProps } from '@grafana/data'; +import { DashboardCursorSync, PanelProps } from '@grafana/data'; import { getLastStreamingDataFramePacket } from '@grafana/data/src/dataframe/StreamingDataFrame'; -import { config } from '@grafana/runtime'; -import { - EventBusPlugin, - Portal, - TooltipDisplayMode, - TooltipPlugin2, - UPlotConfigBuilder, - usePanelContext, - useTheme2, - VizTooltipContainer, - ZoomPlugin, -} from '@grafana/ui'; -import { addTooltipSupport, HoverEvent } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport'; +import { EventBusPlugin, TooltipDisplayMode, TooltipPlugin2, usePanelContext, useTheme2 } from '@grafana/ui'; import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; -import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart'; import { prepareTimelineFields, @@ -24,18 +11,13 @@ import { TimelineMode, } from 'app/core/components/TimelineChart/utils'; -import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin'; -import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin'; import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2'; import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin'; -import { getTimezones } from '../timeseries/utils'; +import { getTimezones, isTooltipScrollable } from '../timeseries/utils'; -import { StateTimelineTooltip } from './StateTimelineTooltip'; import { StateTimelineTooltip2 } from './StateTimelineTooltip2'; import { Options } from './panelcfg.gen'; -const TOOLTIP_OFFSET = 10; - interface TimelinePanelProps extends PanelProps {} /** @@ -53,43 +35,10 @@ export const StateTimelinePanel = ({ }: TimelinePanelProps) => { const theme = useTheme2(); - // TODO: we should just re-init when this changes, and have this be a static setting - const syncTooltip = useCallback( - () => sync?.() === DashboardCursorSync.Tooltip, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const syncAny = useCallback( - () => sync?.() !== DashboardCursorSync.Off, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const oldConfig = useRef(undefined); - const isToolTipOpen = useRef(false); - - const [hover, setHover] = useState(undefined); - const [coords, setCoords] = useState<{ viewport: CartesianCoords2D; canvas: CartesianCoords2D } | null>(null); - const [focusedSeriesIdx, setFocusedSeriesIdx] = useState(null); - const [focusedPointIdx, setFocusedPointIdx] = useState(null); - const [isActive, setIsActive] = useState(false); - const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState(false); // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 const [newAnnotationRange, setNewAnnotationRange] = useState(null); - const { sync, canAddAnnotations, dataLinkPostProcessor, eventBus } = usePanelContext(); - - const onCloseToolTip = () => { - isToolTipOpen.current = false; - setCoords(null); - setShouldDisplayCloseButton(false); - }; - - const onUPlotClick = () => { - isToolTipOpen.current = !isToolTipOpen.current; - // Linking into useState required to re-render tooltip - setShouldDisplayCloseButton(isToolTipOpen.current); - }; + const { sync, eventsScope, canAddAnnotations, dataLinkPostProcessor, eventBus } = usePanelContext(); + const cursorSync = sync?.() ?? DashboardCursorSync.Off; const { frames, warn } = useMemo( () => prepareTimelineFields(data.series, options.mergeValues ?? true, timeRange, theme), @@ -103,65 +52,6 @@ export const StateTimelinePanel = ({ const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]); - const renderCustomTooltip = useCallback( - (alignedData: DataFrame, seriesIdx: number | null, datapointIdx: number | null, onAnnotationAdd?: () => void) => { - const data = frames ?? []; - // Count value fields in the state-timeline-ready frame - const valueFieldsCount = data.reduce( - (acc, frame) => acc + frame.fields.filter((field) => field.type !== FieldType.time).length, - 0 - ); - - // Not caring about multi mode in StateTimeline - if (seriesIdx === null || datapointIdx === null) { - return null; - } - - /** - * There could be a case when the tooltip shows a data from one of a multiple query and the other query finishes first - * from refreshing. This causes data to be out of sync. alignedData - 1 because Time field doesn't count. - * Render nothing in this case to prevent error. - * See https://github.com/grafana/support-escalations/issues/932 - */ - if (alignedData.fields.length - 1 !== valueFieldsCount || !alignedData.fields[seriesIdx]) { - return null; - } - - return ( - <> - {shouldDisplayCloseButton && ( -
- -
- )} - - - ); - }, - [timeZone, frames, shouldDisplayCloseButton] - ); - if (!frames || warn) { return (
@@ -177,7 +67,6 @@ export const StateTimelinePanel = ({ } } const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); - const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); return ( {(builder, alignedFrame) => { - if (oldConfig.current !== builder && !showNewVizTooltips) { - oldConfig.current = addTooltipSupport({ - config: builder, - onUPlotClick, - setFocusedSeriesIdx, - setFocusedPointIdx, - setCoords, - setHover, - isToolTipOpen, - isActive, - setIsActive, - sync, - }); - } - return ( <> - - {showNewVizTooltips ? ( - <> - {options.tooltip.mode !== TooltipDisplayMode.None && ( - { - if (enableAnnotationCreation && timeRange2 != null) { - setNewAnnotationRange(timeRange2); - dismiss(); - return; - } - - const annotate = () => { - let xVal = u.posToVal(u.cursor.left!, 'x'); - - setNewAnnotationRange({ from: xVal, to: xVal }); - dismiss(); - }; - - return ( - - ); - }} - maxWidth={options.tooltip.maxWidth} - maxHeight={options.tooltip.maxHeight} - /> - )} - {/* Renders annotations */} - - - ) : ( - <> - - - {/* Renders annotation markers*/} - {data.annotations && ( - - )} - - {enableAnnotationCreation ? ( - - {({ startAnnotating }) => { - if (options.tooltip.mode === TooltipDisplayMode.None) { - return null; - } - - if (focusedPointIdx === null || (!isActive && sync && sync() === DashboardCursorSync.Crosshair)) { - return null; - } - - return ( - - {hover && coords && focusedSeriesIdx && ( - - {renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx, () => { - startAnnotating({ coords: { plotCanvas: coords.canvas, viewport: coords.viewport } }); - onCloseToolTip(); - })} - - )} - - ); - }} - - ) : ( - - {options.tooltip.mode !== TooltipDisplayMode.None && hover && coords && ( - - {renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx)} - - )} - - )} - + {cursorSync !== DashboardCursorSync.Off && ( + )} + {options.tooltip.mode !== TooltipDisplayMode.None && ( + { + if (enableAnnotationCreation && timeRange2 != null) { + setNewAnnotationRange(timeRange2); + dismiss(); + return; + } + + const annotate = () => { + let xVal = u.posToVal(u.cursor.left!, 'x'); + + setNewAnnotationRange({ from: xVal, to: xVal }); + dismiss(); + }; + + return ( + + ); + }} + maxWidth={options.tooltip.maxWidth} + /> + )} + {/* Renders annotations */} + + ); }} diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip.tsx deleted file mode 100644 index 345f0c8265b..00000000000 --- a/public/app/plugins/panel/state-timeline/StateTimelineTooltip.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; - -import { - DataFrame, - FALLBACK_COLOR, - Field, - getDisplayProcessor, - getFieldDisplayName, - TimeZone, - LinkModel, -} from '@grafana/data'; -import { MenuItem, SeriesTableRow, useTheme2 } from '@grafana/ui'; -import { findNextStateIndex, fmtDuration } from 'app/core/components/TimelineChart/utils'; - -interface StateTimelineTooltipProps { - data: DataFrame[]; - alignedData: DataFrame; - seriesIdx: number; - datapointIdx: number; - timeZone: TimeZone; - onAnnotationAdd?: () => void; -} - -export const StateTimelineTooltip = ({ - data, - alignedData, - seriesIdx, - datapointIdx, - timeZone, - onAnnotationAdd, -}: StateTimelineTooltipProps) => { - const theme = useTheme2(); - - if (!data || datapointIdx == null) { - return null; - } - - const field = alignedData.fields[seriesIdx!]; - - const links: Array> = []; - const linkLookup = new Set(); - - if (field.getLinks) { - const v = field.values[datapointIdx]; - const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v }; - field.getLinks({ calculatedValue: disp, valueRowIndex: datapointIdx }).forEach((link) => { - const key = `${link.title}/${link.href}`; - if (!linkLookup.has(key)) { - links.push(link); - linkLookup.add(key); - } - }); - } - - const xField = alignedData.fields[0]; - const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, timeZone, theme }); - - const dataFrameFieldIndex = field.state?.origin; - const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme }); - const value = field.values[datapointIdx!]; - const display = fieldFmt(value); - const fieldDisplayName = dataFrameFieldIndex - ? getFieldDisplayName( - data[dataFrameFieldIndex.frameIndex].fields[dataFrameFieldIndex.fieldIndex], - data[dataFrameFieldIndex.frameIndex], - data - ) - : null; - - const nextStateIdx = findNextStateIndex(field, datapointIdx!); - let nextStateTs; - if (nextStateIdx) { - nextStateTs = xField.values[nextStateIdx!]; - } - - const stateTs = xField.values[datapointIdx!]; - - let toFragment = null; - let durationFragment = null; - - if (nextStateTs) { - const duration = nextStateTs && fmtDuration(nextStateTs - stateTs); - durationFragment = ( - <> -
- Duration: {duration} - - ); - toFragment = ( - <> - {' to'} {xFieldFmt(xField.values[nextStateIdx!]).text} - - ); - } - - return ( -
-
- {fieldDisplayName} -
- - From {xFieldFmt(xField.values[datapointIdx!]).text} - {toFragment} - {durationFragment} -
-
- {onAnnotationAdd && } - {links.length > 0 && - links.map((link, i) => ( - - ))} -
-
- ); -}; - -StateTimelineTooltip.displayName = 'StateTimelineTooltip'; diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx index 8c2af2b7c80..3d89544b18c 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx @@ -30,6 +30,7 @@ export const StateTimelineTooltip2 = ({ annotate, timeRange, withDuration, + maxHeight, }: StateTimelineTooltip2Props) => { const styles = useStyles2(getStyles); @@ -83,7 +84,7 @@ export const StateTimelineTooltip2 = ({ return (
- + {footer}
); diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index 57178bf15c8..2d607b3b935 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -1,21 +1,8 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useMemo, useState } from 'react'; -import { CartesianCoords2D, DashboardCursorSync, DataFrame, FieldType, PanelProps } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { - EventBusPlugin, - Portal, - TooltipDisplayMode, - TooltipPlugin2, - UPlotConfigBuilder, - usePanelContext, - useTheme2, - VizTooltipContainer, - ZoomPlugin, -} from '@grafana/ui'; -import { addTooltipSupport, HoverEvent } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport'; +import { DashboardCursorSync, PanelProps } from '@grafana/data'; +import { EventBusPlugin, TooltipDisplayMode, TooltipPlugin2, usePanelContext, useTheme2 } from '@grafana/ui'; import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; -import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart'; import { prepareTimelineFields, @@ -24,16 +11,12 @@ import { } from 'app/core/components/TimelineChart/utils'; import { StateTimelineTooltip2 } from '../state-timeline/StateTimelineTooltip2'; -import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin'; import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2'; import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin'; -import { getTimezones } from '../timeseries/utils'; +import { getTimezones, isTooltipScrollable } from '../timeseries/utils'; -import { StatusHistoryTooltip } from './StatusHistoryTooltip'; import { Options } from './panelcfg.gen'; -const TOOLTIP_OFFSET = 10; - interface TimelinePanelProps extends PanelProps {} /** @@ -51,47 +34,13 @@ export const StatusHistoryPanel = ({ }: TimelinePanelProps) => { const theme = useTheme2(); - // TODO: we should just re-init when this changes, and have this be a static setting - const syncTooltip = useCallback( - () => sync?.() === DashboardCursorSync.Tooltip, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const syncAny = useCallback( - () => sync?.() !== DashboardCursorSync.Off, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const oldConfig = useRef(undefined); - const isToolTipOpen = useRef(false); - - const [hover, setHover] = useState(undefined); - const [coords, setCoords] = useState<{ viewport: CartesianCoords2D; canvas: CartesianCoords2D } | null>(null); - const [focusedSeriesIdx, setFocusedSeriesIdx] = useState(null); - const [focusedPointIdx, setFocusedPointIdx] = useState(null); - const [isActive, setIsActive] = useState(false); - const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState(false); // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 const [newAnnotationRange, setNewAnnotationRange] = useState(null); - const { sync, canAddAnnotations, dataLinkPostProcessor, eventBus } = usePanelContext(); + const { sync, eventsScope, canAddAnnotations, dataLinkPostProcessor, eventBus } = usePanelContext(); + const cursorSync = sync?.() ?? DashboardCursorSync.Off; const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); - const onCloseToolTip = () => { - isToolTipOpen.current = false; - setCoords(null); - setShouldDisplayCloseButton(false); - }; - - const onUPlotClick = () => { - isToolTipOpen.current = !isToolTipOpen.current; - - // Linking into useState required to re-render tooltip - setShouldDisplayCloseButton(isToolTipOpen.current); - }; - const { frames, warn } = useMemo( () => prepareTimelineFields(data.series, false, timeRange, theme), [data.series, timeRange, theme] @@ -102,89 +51,6 @@ export const StatusHistoryPanel = ({ [frames, options.legend, theme] ); - const renderCustomTooltip = useCallback( - (alignedData: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => { - const data = frames ?? []; - - // Count value fields in the state-timeline-ready frame - const valueFieldsCount = data.reduce( - (acc, frame) => acc + frame.fields.filter((field) => field.type !== FieldType.time).length, - 0 - ); - - // Not caring about multi mode in StatusHistory - if (seriesIdx === null || datapointIdx === null) { - return null; - } - - /** - * There could be a case when the tooltip shows a data from one of a multiple query and the other query finishes first - * from refreshing. This causes data to be out of sync. alignedData - 1 because Time field doesn't count. - * Render nothing in this case to prevent error. - * See https://github.com/grafana/support-escalations/issues/932 - */ - if (alignedData.fields.length - 1 !== valueFieldsCount || !alignedData.fields[seriesIdx]) { - return null; - } - - return ( - <> - {shouldDisplayCloseButton && ( -
- -
- )} - - - ); - }, - [timeZone, frames, shouldDisplayCloseButton] - ); - - const renderTooltip = (alignedFrame: DataFrame) => { - if (options.tooltip.mode === TooltipDisplayMode.None) { - return null; - } - - if (focusedPointIdx === null || (!isActive && sync && sync() === DashboardCursorSync.Crosshair)) { - return null; - } - - return ( - - {hover && coords && focusedSeriesIdx && ( - - {renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx)} - - )} - - ); - }; - const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]); if (!frames || warn) { @@ -207,8 +73,6 @@ export const StatusHistoryPanel = ({ ); } - const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); - return ( {(builder, alignedFrame) => { - if (oldConfig.current !== builder && !showNewVizTooltips) { - oldConfig.current = addTooltipSupport({ - config: builder, - onUPlotClick, - setFocusedSeriesIdx, - setFocusedPointIdx, - setCoords, - setHover, - isToolTipOpen, - isActive, - setIsActive, - }); - } - return ( <> - - {showNewVizTooltips ? ( - <> - {options.tooltip.mode !== TooltipDisplayMode.None && ( - { - if (enableAnnotationCreation && timeRange2 != null) { - setNewAnnotationRange(timeRange2); - dismiss(); - return; - } - - const annotate = () => { - let xVal = u.posToVal(u.cursor.left!, 'x'); - - setNewAnnotationRange({ from: xVal, to: xVal }); - dismiss(); - }; - - return ( - - ); - }} - maxWidth={options.tooltip.maxWidth} - maxHeight={options.tooltip.maxHeight} - /> - )} - - - ) : ( - <> - - {renderTooltip(alignedFrame)} - - {data.annotations && ( - - )} - + {cursorSync !== DashboardCursorSync.Off && ( + )} + {options.tooltip.mode !== TooltipDisplayMode.None && ( + { + if (enableAnnotationCreation && timeRange2 != null) { + setNewAnnotationRange(timeRange2); + dismiss(); + return; + } + + const annotate = () => { + let xVal = u.posToVal(u.cursor.left!, 'x'); + + setNewAnnotationRange({ from: xVal, to: xVal }); + dismiss(); + }; + + return ( + + ); + }} + maxWidth={options.tooltip.maxWidth} + /> + )} + + ); }} diff --git a/public/app/plugins/panel/status-history/StatusHistoryTooltip.tsx b/public/app/plugins/panel/status-history/StatusHistoryTooltip.tsx deleted file mode 100644 index f9ed786ab2c..00000000000 --- a/public/app/plugins/panel/status-history/StatusHistoryTooltip.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react'; - -import { - DataFrame, - FALLBACK_COLOR, - Field, - getDisplayProcessor, - getFieldDisplayName, - TimeZone, - LinkModel, -} from '@grafana/data'; -import { MenuItem, SeriesTableRow, useTheme2 } from '@grafana/ui'; - -interface StatusHistoryTooltipProps { - data: DataFrame[]; - alignedData: DataFrame; - seriesIdx: number; - datapointIdx: number; - timeZone: TimeZone; -} - -export const StatusHistoryTooltip = ({ - data, - alignedData, - seriesIdx, - datapointIdx, - timeZone, -}: StatusHistoryTooltipProps) => { - const theme = useTheme2(); - - if (!data || datapointIdx == null) { - return null; - } - - const field = alignedData.fields[seriesIdx!]; - - const links: Array> = []; - const linkLookup = new Set(); - - if (field.getLinks) { - const v = field.values[datapointIdx]; - const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v }; - field.getLinks({ calculatedValue: disp, valueRowIndex: datapointIdx }).forEach((link) => { - const key = `${link.title}/${link.href}`; - if (!linkLookup.has(key)) { - links.push(link); - linkLookup.add(key); - } - }); - } - - const xField = alignedData.fields[0]; - const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, timeZone, theme }); - - const dataFrameFieldIndex = field.state?.origin; - const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme }); - const value = field.values[datapointIdx!]; - const display = fieldFmt(value); - const fieldDisplayName = dataFrameFieldIndex - ? getFieldDisplayName( - data[dataFrameFieldIndex.frameIndex].fields[dataFrameFieldIndex.fieldIndex], - data[dataFrameFieldIndex.frameIndex], - data - ) - : null; - - return ( -
-
- {xFieldFmt(xField.values[datapointIdx]).text} -
- - {fieldDisplayName} -
- {links.length > 0 && ( -
- {links.map((link, i) => ( - - ))} -
- )} -
- ); -}; - -StatusHistoryTooltip.displayName = 'StatusHistoryTooltip'; diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 792d1e4000c..fd689f934ad 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -1,26 +1,16 @@ -import React, { useMemo, useState, useCallback } from 'react'; +import React, { useMemo, useState } from 'react'; import { PanelProps, DataFrameType, DashboardCursorSync } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { TooltipDisplayMode, VizOrientation } from '@grafana/schema'; -import { - EventBusPlugin, - KeyboardPlugin, - TooltipPlugin, - TooltipPlugin2, - usePanelContext, - ZoomPlugin, -} from '@grafana/ui'; +import { EventBusPlugin, KeyboardPlugin, TooltipPlugin2, usePanelContext } from '@grafana/ui'; import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; import { config } from 'app/core/config'; import { TimeSeriesTooltip } from './TimeSeriesTooltip'; import { Options } from './panelcfg.gen'; -import { AnnotationEditorPlugin } from './plugins/AnnotationEditorPlugin'; -import { AnnotationsPlugin } from './plugins/AnnotationsPlugin'; import { AnnotationsPlugin2 } from './plugins/AnnotationsPlugin2'; -import { ContextMenuPlugin } from './plugins/ContextMenuPlugin'; import { ExemplarsPlugin, getVisibleLabels } from './plugins/ExemplarsPlugin'; import { OutsideRangePlugin } from './plugins/OutsideRangePlugin'; import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin'; @@ -43,6 +33,7 @@ export const TimeSeriesPanel = ({ }: TimeSeriesPanelProps) => { const { sync, + eventsScope, canAddAnnotations, onThresholdsChange, canEditThresholds, @@ -67,22 +58,8 @@ export const TimeSeriesPanel = ({ }, [frames, id]); const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); - const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); - // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 const [newAnnotationRange, setNewAnnotationRange] = useState(null); - - // TODO: we should just re-init when this changes, and have this be a static setting - const syncTooltip = useCallback( - () => sync?.() === DashboardCursorSync.Tooltip, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const syncAny = useCallback( - () => sync?.() !== DashboardCursorSync.Off, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); + const cursorSync = sync?.() ?? DashboardCursorSync.Off; if (!frames || suggestions) { return ( @@ -98,14 +75,6 @@ export const TimeSeriesPanel = ({ ); } - // which annotation are we editing? - // are we adding a new annotation? is annotating? - // console.log(data.annotations); - - // annotations plugin includes the editor and the renderer - // its annotation state is managed here for now - // tooltipplugin2 receives render with annotate range, callback should setstate here that gets passed to annotationsplugin as newAnnotaton or editAnnotation - return ( {(uplotConfig, alignedFrame) => { return ( <> - - {options.tooltip.mode === TooltipDisplayMode.None || ( - <> - {showNewVizTooltips ? ( - { - if (enableAnnotationCreation && timeRange2 != null) { - setNewAnnotationRange(timeRange2); - dismiss(); - return; - } + {cursorSync !== DashboardCursorSync.Off && ( + + )} + {options.tooltip.mode !== TooltipDisplayMode.None && ( + { + if (enableAnnotationCreation && timeRange2 != null) { + setNewAnnotationRange(timeRange2); + dismiss(); + return; + } - const annotate = () => { - let xVal = u.posToVal(u.cursor.left!, 'x'); + const annotate = () => { + let xVal = u.posToVal(u.cursor.left!, 'x'); - setNewAnnotationRange({ from: xVal, to: xVal }); - dismiss(); - }; + setNewAnnotationRange({ from: xVal, to: xVal }); + dismiss(); + }; - return ( - // not sure it header time here works for annotations, since it's taken from nearest datapoint index - - ); - }} - maxWidth={options.tooltip.maxWidth} - maxHeight={options.tooltip.maxHeight} - /> - ) : ( - <> - - - + ); + }} + maxWidth={options.tooltip.maxWidth} + /> + )} + {!isVerticallyOriented && ( + <> + + + {data.annotations && ( + + )} + {((canEditThresholds && onThresholdsChange) || showThresholds) && ( + )} )} - {/* Renders annotation markers*/} - {!isVerticallyOriented && showNewVizTooltips ? ( - - ) : ( - !isVerticallyOriented && - data.annotations && ( - - ) - )} - - {/*Enables annotations creation*/} - {!showNewVizTooltips ? ( - enableAnnotationCreation && !isVerticallyOriented ? ( - - {({ startAnnotating }) => { - return ( - { - if (!p) { - return; - } - startAnnotating({ coords: p.coords }); - }, - }, - ], - }, - ]} - /> - ); - }} - - ) : ( - - ) - ) : undefined} - {data.annotations && !isVerticallyOriented && ( - - )} - - {((canEditThresholds && onThresholdsChange) || showThresholds) && !isVerticallyOriented && ( - - )} - - ); }} diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx index 997d7e43c21..97e4620a679 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -30,6 +30,7 @@ export interface TimeSeriesTooltipProps { scrollable?: boolean; annotate?: () => void; + maxHeight?: number; } export const TimeSeriesTooltip = ({ @@ -42,6 +43,7 @@ export const TimeSeriesTooltip = ({ scrollable = false, isPinned, annotate, + maxHeight, }: TimeSeriesTooltipProps) => { const styles = useStyles2(getStyles); @@ -77,7 +79,7 @@ export const TimeSeriesTooltip = ({ return (
- + {footer}
); diff --git a/public/app/plugins/panel/timeseries/plugins/AnnotationEditorPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/AnnotationEditorPlugin.tsx deleted file mode 100644 index a74425947f1..00000000000 --- a/public/app/plugins/panel/timeseries/plugins/AnnotationEditorPlugin.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'; -import { useMountedState } from 'react-use'; -import uPlot from 'uplot'; - -import { CartesianCoords2D, DataFrame, TimeZone } from '@grafana/data'; -import { PlotSelection, UPlotConfigBuilder } from '@grafana/ui'; - -import { AnnotationEditor } from './annotations/AnnotationEditor'; - -type StartAnnotatingFn = (props: { - // pixel coordinates of the clicked point on the uPlot canvas - coords: { viewport: CartesianCoords2D; plotCanvas: CartesianCoords2D } | null; -}) => void; - -interface AnnotationEditorPluginProps { - data: DataFrame; - timeZone: TimeZone; - config: UPlotConfigBuilder; - children?: (props: { startAnnotating: StartAnnotatingFn }) => React.ReactNode; -} - -/** - * @alpha - */ -export const AnnotationEditorPlugin = ({ data, timeZone, config, children }: AnnotationEditorPluginProps) => { - const plotInstance = useRef(); - const [bbox, setBbox] = useState(); - const [isAddingAnnotation, setIsAddingAnnotation] = useState(false); - const [selection, setSelection] = useState(null); - const isMounted = useMountedState(); - - const clearSelection = useCallback(() => { - setSelection(null); - - if (plotInstance.current) { - plotInstance.current.setSelect({ top: 0, left: 0, width: 0, height: 0 }); - } - setIsAddingAnnotation(false); - }, [setIsAddingAnnotation, setSelection]); - - useLayoutEffect(() => { - let annotating = false; - - config.addHook('init', (u) => { - plotInstance.current = u; - // Wrap all setSelect hooks to prevent them from firing if user is annotating - const setSelectHooks = u.hooks.setSelect; - - if (setSelectHooks) { - for (let i = 0; i < setSelectHooks.length; i++) { - const hook = setSelectHooks[i]; - - if (hook !== setSelect) { - setSelectHooks[i] = (...args) => { - !annotating && hook!(...args); - }; - } - } - } - }); - - // cache uPlot plotting area bounding box - config.addHook('syncRect', (u, rect) => { - if (!isMounted()) { - return; - } - setBbox(rect); - }); - - const setSelect = (u: uPlot) => { - if (annotating) { - setIsAddingAnnotation(true); - setSelection({ - min: u.posToVal(u.select.left, 'x'), - max: u.posToVal(u.select.left + u.select.width, 'x'), - bbox: { - left: u.select.left, - top: 0, - height: u.select.height, - width: u.select.width, - }, - }); - annotating = false; - } - }; - - config.addHook('setSelect', setSelect); - - config.setCursor({ - bind: { - mousedown: (u, targ, handler) => (e) => { - annotating = e.button === 0 && (e.metaKey || e.ctrlKey); - handler(e); - return null; - }, - mouseup: (u, targ, handler) => (e) => { - // uPlot will not fire setSelect hooks for 0-width && 0-height selections - // so we force it to fire on single-point clicks by mutating left & height - if (annotating && u.select.width === 0) { - u.select.left = u.cursor.left!; - u.select.height = u.bbox.height / window.devicePixelRatio; - } - handler(e); - return null; - }, - }, - }); - }, [config, setBbox, isMounted]); - - const startAnnotating = useCallback( - ({ coords }) => { - if (!plotInstance.current || !bbox || !coords) { - return; - } - - const min = plotInstance.current.posToVal(coords.plotCanvas.x, 'x'); - - if (!min) { - return; - } - - setSelection({ - min, - max: min, - bbox: { - left: coords.plotCanvas.x, - top: 0, - height: bbox.height, - width: 0, - }, - }); - setIsAddingAnnotation(true); - }, - [bbox] - ); - - return ( - <> - {isAddingAnnotation && selection && bbox && ( - - )} - {children ? children({ startAnnotating }) : null} - - ); -}; diff --git a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin.tsx deleted file mode 100644 index 384ec4acd30..00000000000 --- a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import React, { useCallback, useEffect, useLayoutEffect, useRef } from 'react'; -import uPlot from 'uplot'; - -import { colorManipulator, DataFrame, DataFrameFieldIndex, DataFrameView, TimeZone } from '@grafana/data'; -import { EventsCanvas, UPlotConfigBuilder, useTheme2 } from '@grafana/ui'; - -import { AnnotationMarker } from './annotations/AnnotationMarker'; -import { AnnotationsDataFrameViewDTO } from './types'; - -interface AnnotationsPluginProps { - config: UPlotConfigBuilder; - annotations: DataFrame[]; - timeZone: TimeZone; - disableCanvasRendering?: boolean; -} - -export const AnnotationsPlugin = ({ - annotations, - timeZone, - config, - disableCanvasRendering = false, -}: AnnotationsPluginProps) => { - const theme = useTheme2(); - const plotInstance = useRef(); - - const annotationsRef = useRef>>(); - - // Update annotations views when new annotations came - useEffect(() => { - const views: Array> = []; - - for (const frame of annotations) { - views.push(new DataFrameView(frame)); - } - - annotationsRef.current = views; - - return () => { - // clear on unmount - annotationsRef.current = []; - }; - }, [annotations]); - - useLayoutEffect(() => { - config.addHook('init', (u) => { - plotInstance.current = u; - }); - - config.addHook('draw', (u) => { - // Render annotation lines on the canvas - /** - * We cannot rely on state value here, as it would require this effect to be dependent on the state value. - */ - if (!annotationsRef.current) { - return null; - } - - const ctx = u.ctx; - if (!ctx) { - return; - } - ctx.save(); - ctx.beginPath(); - ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height); - ctx.clip(); - - const renderLine = (x: number, color: string) => { - ctx.beginPath(); - ctx.lineWidth = 2; - ctx.strokeStyle = color; - ctx.setLineDash([5, 5]); - ctx.moveTo(x, u.bbox.top); - ctx.lineTo(x, u.bbox.top + u.bbox.height); - ctx.stroke(); - ctx.closePath(); - }; - - if (!disableCanvasRendering) { - for (let i = 0; i < annotationsRef.current.length; i++) { - const annotationsView = annotationsRef.current[i]; - for (let j = 0; j < annotationsView.length; j++) { - const annotation = annotationsView.get(j); - - if (!annotation.time) { - continue; - } - - let x0 = u.valToPos(annotation.time, 'x', true); - const color = theme.visualization.getColorByName(annotation.color); - - renderLine(x0, color); - - if (annotation.isRegion && annotation.timeEnd) { - let x1 = u.valToPos(annotation.timeEnd, 'x', true); - - renderLine(x1, color); - - ctx.fillStyle = colorManipulator.alpha(color, 0.1); - ctx.rect(x0, u.bbox.top, x1 - x0, u.bbox.height); - ctx.fill(); - } - } - } - } - - ctx.restore(); - return; - }); - }, [config, theme, disableCanvasRendering]); - - const mapAnnotationToXYCoords = useCallback((frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { - const view = new DataFrameView(frame); - const annotation = view.get(dataFrameFieldIndex.fieldIndex); - - if (!annotation.time || !plotInstance.current) { - return undefined; - } - let x = plotInstance.current.valToPos(annotation.time, 'x'); - - if (x < 0) { - x = 0; - } - return { - x, - y: plotInstance.current.bbox.height / window.devicePixelRatio + 4, - }; - }, []); - - const renderMarker = useCallback( - (frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { - let width = 0; - const view = new DataFrameView(frame); - const annotation = view.get(dataFrameFieldIndex.fieldIndex); - const isRegionAnnotation = Boolean(annotation.isRegion); - - if (isRegionAnnotation && plotInstance.current) { - let x0 = plotInstance.current.valToPos(annotation.time, 'x'); - let x1 = plotInstance.current.valToPos(annotation.timeEnd, 'x'); - - // markers are rendered relatively to uPlot canvas overly, not caring about axes width - if (x0 < 0) { - x0 = 0; - } - - if (x1 > plotInstance.current.bbox.width / window.devicePixelRatio) { - x1 = plotInstance.current.bbox.width / window.devicePixelRatio; - } - width = x1 - x0; - } - - return ; - }, - [timeZone] - ); - - return ( - - ); -}; diff --git a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx index 8c0313d1ca4..ee801a8dd4d 100644 --- a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx @@ -187,13 +187,13 @@ export const AnnotationsPlugin2 = ({ for (let i = 0; i < vals.time.length; i++) { let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR); - let left = plot.valToPos(vals.time[i], 'x'); + let left = Math.round(plot.valToPos(vals.time[i], 'x')) || 0; // handles -0 let style: React.CSSProperties | null = null; let className = ''; let isVisible = true; if (vals.isRegion?.[i]) { - let right = plot.valToPos(vals.timeEnd?.[i], 'x'); + let right = Math.round(plot.valToPos(vals.timeEnd?.[i], 'x')) || 0; // handles -0 isVisible = left < plot.rect.width && right > 0; @@ -205,7 +205,7 @@ export const AnnotationsPlugin2 = ({ className = styles.annoRegion; } } else { - isVisible = left > 0 && left <= plot.rect.width; + isVisible = left >= 0 && left <= plot.rect.width; if (isVisible) { style = { left, borderBottomColor: color }; diff --git a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx deleted file mode 100644 index 59c6ad9dceb..00000000000 --- a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { useClickAway } from 'react-use'; - -import { CartesianCoords2D, DataFrame, getFieldDisplayName, InterpolateFunction, TimeZone } from '@grafana/data'; -import { - ContextMenu, - GraphContextMenuHeader, - MenuItemProps, - MenuItemsGroup, - MenuGroup, - MenuItem, - UPlotConfigBuilder, -} from '@grafana/ui'; - -type ContextMenuSelectionCoords = { viewport: CartesianCoords2D; plotCanvas: CartesianCoords2D }; -type ContextMenuSelectionPoint = { seriesIdx: number | null; dataIdx: number | null }; - -export interface ContextMenuItemClickPayload { - coords: ContextMenuSelectionCoords; -} - -interface ContextMenuPluginProps { - data: DataFrame; - frames?: DataFrame[]; - config: UPlotConfigBuilder; - defaultItems?: Array>; - timeZone: TimeZone; - onOpen?: () => void; - onClose?: () => void; - replaceVariables?: InterpolateFunction; -} - -export const ContextMenuPlugin = ({ - data, - config, - onClose, - timeZone, - replaceVariables, - ...otherProps -}: ContextMenuPluginProps) => { - const [coords, setCoords] = useState(null); - const [point, setPoint] = useState(null); - const [isOpen, setIsOpen] = useState(false); - - useLayoutEffect(() => { - let seriesIdx: number | null = null; - - config.addHook('init', (u) => { - u.over.addEventListener('click', (e) => { - // only open when have a focused point, and not for explicit annotations, zooms, etc. - if (seriesIdx != null && !e.metaKey && !e.ctrlKey && !e.shiftKey) { - setCoords({ - viewport: { - x: e.clientX, - y: e.clientY, - }, - plotCanvas: { - x: e.clientX - u.rect.left, - y: e.clientY - u.rect.top, - }, - }); - setPoint({ seriesIdx, dataIdx: u.cursor.idxs![seriesIdx] }); - setIsOpen(true); - } - }); - }); - - config.addHook('setSeries', (u, _seriesIdx) => { - seriesIdx = _seriesIdx; - }); - }, [config]); - - const defaultItems = useMemo(() => { - return otherProps.defaultItems - ? otherProps.defaultItems.map((i) => { - return { - ...i, - items: i.items.map((j) => { - return { - ...j, - onClick: (e: React.MouseEvent) => { - if (!coords) { - return; - } - - j.onClick?.(e, { coords }); - }, - }; - }), - }; - }) - : []; - }, [coords, otherProps.defaultItems]); - - return ( - <> - {isOpen && coords && ( - { - setPoint(null); - setIsOpen(false); - if (onClose) { - onClose(); - } - }} - /> - )} - - ); -}; - -interface ContextMenuViewProps { - data: DataFrame; - frames?: DataFrame[]; - defaultItems?: MenuItemsGroup[]; - timeZone: TimeZone; - onClose?: () => void; - selection: { - point?: { seriesIdx: number | null; dataIdx: number | null } | null; - coords: { plotCanvas: CartesianCoords2D; viewport: CartesianCoords2D }; - }; - replaceVariables?: InterpolateFunction; -} - -export const ContextMenuView = ({ - selection, - timeZone, - defaultItems, - replaceVariables, - data, - ...otherProps -}: ContextMenuViewProps) => { - const ref = useRef(null); - - const onClose = () => { - if (otherProps.onClose) { - otherProps.onClose(); - } - }; - - useClickAway(ref, () => { - onClose(); - }); - - const xField = data.fields[0]; - - if (!xField) { - return null; - } - const items = defaultItems ? [...defaultItems] : []; - let renderHeader: () => JSX.Element | null = () => null; - - if (selection.point) { - const { seriesIdx, dataIdx } = selection.point; - const xFieldFmt = xField.display!; - - if (seriesIdx && dataIdx !== null) { - const field = data.fields[seriesIdx]; - - const displayValue = field.display!(field.values[dataIdx]); - - const hasLinks = field.config.links && field.config.links.length > 0; - - if (hasLinks) { - if (field.getLinks) { - items.push({ - items: field - .getLinks({ - valueRowIndex: dataIdx, - }) - .map((link) => { - return { - label: link.title, - ariaLabel: link.title, - url: link.href, - target: link.target, - icon: link.target === '_self' ? 'link' : 'external-link-alt', - onClick: link.onClick, - }; - }), - }); - } - } - - // eslint-disable-next-line react/display-name - renderHeader = () => ( - - ); - } - } - - const renderMenuGroupItems = () => { - return items?.map((group, index) => ( - - {(group.items || []).map((item) => ( - - ))} - - )); - }; - - return ( - - ); -}; diff --git a/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditor.tsx b/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditor.tsx deleted file mode 100644 index 1702de0ce4a..00000000000 --- a/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditor.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { css, cx } from '@emotion/css'; -import { autoUpdate, flip, shift, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; -import React, { HTMLAttributes } from 'react'; - -import { colorManipulator, DataFrame, getDisplayProcessor, GrafanaTheme2, TimeZone } from '@grafana/data'; -import { PlotSelection, useStyles2, useTheme2, Portal, DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; - -import { getCommonAnnotationStyles } from '../styles'; -import { AnnotationsDataFrameViewDTO } from '../types'; - -import { AnnotationEditorForm } from './AnnotationEditorForm'; - -interface AnnotationEditorProps extends HTMLAttributes { - data: DataFrame; - timeZone: TimeZone; - selection: PlotSelection; - onSave: () => void; - onDismiss: () => void; - annotation?: AnnotationsDataFrameViewDTO; -} - -export const AnnotationEditor = ({ - onDismiss, - onSave, - timeZone, - data, - selection, - annotation, - style, -}: AnnotationEditorProps) => { - const theme = useTheme2(); - const styles = useStyles2(getStyles); - const commonStyles = useStyles2(getCommonAnnotationStyles); - - // the order of middleware is important! - const middleware = [ - flip({ - fallbackAxisSideDirection: 'end', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), - ]; - - const { context, refs, floatingStyles } = useFloating({ - open: true, - placement: 'bottom', - onOpenChange: (open) => { - if (!open) { - onDismiss(); - } - }, - middleware, - whileElementsMounted: autoUpdate, - strategy: 'fixed', - }); - - const dismiss = useDismiss(context); - - const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]); - - let xField = data.fields[0]; - if (!xField) { - return null; - } - const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, timeZone, theme }); - const isRegionAnnotation = selection.min !== selection.max; - - return ( - - <> -
-
-
-
-
- - xFieldFmt(v).text} - onSave={onSave} - onDismiss={onDismiss} - ref={refs.setFloating} - style={floatingStyles} - {...getFloatingProps()} - /> - - - ); -}; - -const getStyles = (theme: GrafanaTheme2) => { - return { - overlay: (annotation?: AnnotationsDataFrameViewDTO) => { - const color = theme.visualization.getColorByName(annotation?.color || DEFAULT_ANNOTATION_COLOR); - return css({ - borderLeft: `1px dashed ${color}`, - }); - }, - overlayRange: (annotation?: AnnotationsDataFrameViewDTO) => { - const color = theme.visualization.getColorByName(annotation?.color || DEFAULT_ANNOTATION_COLOR); - return css({ - background: colorManipulator.alpha(color, 0.1), - borderLeft: `1px dashed ${color}`, - borderRight: `1px dashed ${color}`, - }); - }, - markerTriangle: css({ - top: `calc(100% + 2px)`, - left: '-4px', - position: 'absolute', - }), - markerBar: css({ - top: '100%', - left: 0, - position: 'absolute', - }), - }; -}; diff --git a/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditorForm.tsx b/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditorForm.tsx deleted file mode 100644 index c8de3bd3934..00000000000 --- a/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditorForm.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { css, cx } from '@emotion/css'; -import React, { HTMLAttributes, useRef } from 'react'; -import { Controller } from 'react-hook-form'; -import useAsyncFn from 'react-use/lib/useAsyncFn'; -import useClickAway from 'react-use/lib/useClickAway'; - -import { AnnotationEventUIModel, GrafanaTheme2 } from '@grafana/data'; -import { Button, Field, Stack, TextArea, usePanelContext, useStyles2 } from '@grafana/ui'; -import { Form } from 'app/core/components/Form/Form'; -import { TagFilter } from 'app/core/components/TagFilter/TagFilter'; -import { getAnnotationTags } from 'app/features/annotations/api'; - -import { AnnotationsDataFrameViewDTO } from '../types'; - -interface AnnotationEditFormDTO { - description: string; - tags: string[]; -} - -interface AnnotationEditorFormProps extends HTMLAttributes { - annotation: AnnotationsDataFrameViewDTO; - timeFormatter: (v: number) => string; - onSave: () => void; - onDismiss: () => void; -} - -export const AnnotationEditorForm = React.forwardRef( - ({ annotation, onSave, onDismiss, timeFormatter, className, ...otherProps }, ref) => { - const styles = useStyles2(getStyles); - const panelContext = usePanelContext(); - const clickAwayRef = useRef(null); - - useClickAway(clickAwayRef, () => { - onDismiss(); - }); - - const [createAnnotationState, createAnnotation] = useAsyncFn(async (event: AnnotationEventUIModel) => { - const result = await panelContext.onAnnotationCreate!(event); - if (onSave) { - onSave(); - } - return result; - }); - - const [updateAnnotationState, updateAnnotation] = useAsyncFn(async (event: AnnotationEventUIModel) => { - const result = await panelContext.onAnnotationUpdate!(event); - if (onSave) { - onSave(); - } - return result; - }); - - const isUpdatingAnnotation = annotation.id !== undefined; - const isRegionAnnotation = annotation.time !== annotation.timeEnd; - const operation = isUpdatingAnnotation ? updateAnnotation : createAnnotation; - const stateIndicator = isUpdatingAnnotation ? updateAnnotationState : createAnnotationState; - const ts = isRegionAnnotation - ? `${timeFormatter(annotation.time)} - ${timeFormatter(annotation.timeEnd)}` - : timeFormatter(annotation.time); - - const onSubmit = ({ tags, description }: AnnotationEditFormDTO) => { - operation({ - id: annotation.id, - tags, - description, - from: Math.round(annotation.time!), - to: Math.round(annotation.timeEnd!), - }); - }; - - const form = ( -
-
- -
Add annotation
-
{ts}
-
-
-
- - onSubmit={onSubmit} - defaultValues={{ description: annotation?.text, tags: annotation?.tags || [] }} - > - {({ register, errors, control }) => { - return ( - <> - -