import React, { useCallback, useMemo, useRef, useState } from 'react'; import { CartesianCoords2D, DataFrame, FieldType, PanelProps } from '@grafana/data'; import { Portal, TooltipDisplayMode, UPlotConfigBuilder, useTheme2, VizTooltipContainer, ZoomPlugin, } from '@grafana/ui'; import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { TimelineChart } from '../state-timeline/TimelineChart'; import { TimelineMode } from '../state-timeline/types'; import { prepareTimelineFields, prepareTimelineLegendItems } from '../state-timeline/utils'; import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin'; import { getTimezones } from '../timeseries/utils'; import { StatusHistoryTooltip } from './StatusHistoryTooltip'; import { StatusPanelOptions } from './types'; const TOOLTIP_OFFSET = 10; interface TimelinePanelProps extends PanelProps {} /** * @alpha */ export const StatusHistoryPanel: React.FC = ({ data, timeRange, timeZone, options, width, height, onChangeTimeRange, }) => { const theme = useTheme2(); 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 [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 { frames, warn } = useMemo( () => prepareTimelineFields(data?.series, false, timeRange, theme), [data, timeRange, theme] ); const legendItems = useMemo( () => prepareTimelineLegendItems(frames, options.legend, theme), [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.meta?.transformations?.length && alignedData.fields.length - 1 !== valueFieldsCount) || !alignedData.fields[seriesIdx] ) { return null; } return ( <> {shouldDisplayCloseButton && (
)} ); }, [timeZone, frames, shouldDisplayCloseButton] ); const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]); if (!frames || warn) { return (

{warn ?? 'No data found in response'}

); } // Status grid requires some space between values if (frames[0].length > width / 2) { return (

Too many points to visualize properly.
Update the query to return fewer points.
({frames[0].length} points received)

); } return ( {(config, alignedFrame) => { if (oldConfig.current !== config) { oldConfig.current = addTooltipSupport({ config, onUPlotClick, setFocusedSeriesIdx, setFocusedPointIdx, setCoords, setHover, isToolTipOpen, }); } if (options.tooltip.mode === TooltipDisplayMode.None) { return null; } return ( <> {hover && coords && ( {renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx)} )} ); }} ); };