diff --git a/packages/grafana-ui/src/components/uPlot/geometries/EventsCanvas.tsx b/packages/grafana-ui/src/components/uPlot/geometries/EventsCanvas.tsx index a246bfd59f8..f4f2e71b7ae 100644 --- a/packages/grafana-ui/src/components/uPlot/geometries/EventsCanvas.tsx +++ b/packages/grafana-ui/src/components/uPlot/geometries/EventsCanvas.tsx @@ -1,9 +1,9 @@ -import { useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useLayoutEffect, useMemo, useRef, useState, ReactNode } from 'react'; import * as React from 'react'; import { useMountedState } from 'react-use'; import uPlot from 'uplot'; -import { DataFrame, DataFrameFieldIndex } from '@grafana/data'; +import { DataFrame } from '@grafana/data'; import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; @@ -14,11 +14,8 @@ interface EventsCanvasProps { id: string; config: UPlotConfigBuilder; events: DataFrame[]; - renderEventMarker: (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => React.ReactNode; - mapEventToXYCoords: ( - dataFrame: DataFrame, - dataFrameFieldIndex: DataFrameFieldIndex - ) => { x: number; y: number } | undefined; + renderEventMarker: (dataFrame: DataFrame, rowIndex: number) => ReactNode; + mapEventToXYCoords: (dataFrame: DataFrame, rowIndex: number) => { x: number; y: number } | undefined; } export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords, config }: EventsCanvasProps) { @@ -50,14 +47,15 @@ export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords for (let i = 0; i < events.length; i++) { const frame = events[i]; + for (let j = 0; j < frame.length; j++) { - const coords = mapEventToXYCoords(frame, { fieldIndex: j, frameIndex: i }); + const coords = mapEventToXYCoords(frame, j); if (!coords) { continue; } markers.push( - {renderEventMarker(frame, { fieldIndex: j, frameIndex: i })} + {renderEventMarker(frame, j)} ); } diff --git a/public/app/features/visualization/data-hover/DataHoverView.test.tsx b/public/app/features/visualization/data-hover/DataHoverView.test.tsx index b5b2eef3c55..d756d41b250 100644 --- a/public/app/features/visualization/data-hover/DataHoverView.test.tsx +++ b/public/app/features/visualization/data-hover/DataHoverView.test.tsx @@ -1,12 +1,12 @@ import { render, screen } from '@testing-library/react'; -import { ArrayDataFrame } from '@grafana/data'; +import { arrayToDataFrame } from '@grafana/data'; import { DataHoverView } from './DataHoverView'; describe('DataHoverView component', () => { it('should default to multi mode if mode is null or undefined', () => { - const data = new ArrayDataFrame([{ foo: 'bar' }]); + const data = arrayToDataFrame([{ foo: 'bar' }]); render(); expect(screen.queryByText('bar')).toBeInTheDocument(); diff --git a/public/app/features/visualization/data-hover/DataHoverView.tsx b/public/app/features/visualization/data-hover/DataHoverView.tsx index 0c51b28da32..01c999f7fbf 100644 --- a/public/app/features/visualization/data-hover/DataHoverView.tsx +++ b/public/app/features/visualization/data-hover/DataHoverView.tsx @@ -1,116 +1,61 @@ import { css } from '@emotion/css'; -import { - arrayUtils, - DataFrame, - Field, - formattedValueToString, - getFieldDisplayName, - GrafanaTheme2, - LinkModel, -} from '@grafana/data'; +import { DataFrame, Field, formattedValueToString, getFieldDisplayName, GrafanaTheme2, LinkModel } from '@grafana/data'; import { Trans } from '@grafana/i18n'; -import { SortOrder, TooltipDisplayMode } from '@grafana/schema'; import { TextLink, useStyles2 } from '@grafana/ui'; import { renderValue } from 'app/plugins/panel/geomap/utils/uiUtils'; - -import { ExemplarHoverView } from './ExemplarHoverView'; +import { getDataLinks } from 'app/plugins/panel/status-history/utils'; export interface Props { data?: DataFrame; // source data rowIndex?: number | null; // the hover row columnIndex?: number | null; // the hover column - sortOrder?: SortOrder; - mode?: TooltipDisplayMode | null; header?: string; padding?: number; - maxHeight?: number; } export interface DisplayValue { name: string; value: unknown; valueString: string; - highlight: boolean; } -export function getDisplayValuesAndLinks( - data: DataFrame, - rowIndex: number, - columnIndex?: number | null, - sortOrder?: SortOrder, - mode?: TooltipDisplayMode | null -) { - const fields = data.fields; - const hoveredField = columnIndex != null ? fields[columnIndex] : null; +export function getDisplayValuesAndLinks(data: DataFrame, rowIndex: number, columnIndex?: number) { + const visibleFields = data.fields.filter( + (f, i) => !Boolean(f.config.custom?.hideFrom?.tooltip) && (columnIndex == null || i === columnIndex) + ); - const visibleFields = fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip)); - const traceIDField = visibleFields.find((field) => field.name === 'traceID') || fields[0]; - const orderedVisibleFields = []; - // Only include traceID if it's visible and put it in front. - if (visibleFields.filter((field) => traceIDField === field).length > 0) { - orderedVisibleFields.push(traceIDField); - } - orderedVisibleFields.push(...visibleFields.filter((field) => traceIDField !== field)); - - if (orderedVisibleFields.length === 0) { + if (visibleFields.length === 0) { return null; } const displayValues: DisplayValue[] = []; const links: Array> = []; - const linkLookup = new Set(); - - for (const field of orderedVisibleFields) { - if (mode === TooltipDisplayMode.Single && field !== hoveredField) { - continue; - } + for (const field of visibleFields) { const value = field.values[rowIndex]; const fieldDisplay = field.display ? field.display(value) : { text: `${value}`, numeric: +value }; - if (field.getLinks) { - field.getLinks({ calculatedValue: fieldDisplay, valueRowIndex: rowIndex }).forEach((link) => { - const key = `${link.title}/${link.href}`; - if (!linkLookup.has(key)) { - links.push(link); - linkLookup.add(key); - } - }); - } + links.push(...getDataLinks(field, rowIndex)); displayValues.push({ name: getFieldDisplayName(field, data), value, valueString: formattedValueToString(fieldDisplay), - highlight: field === hoveredField, }); } - if (sortOrder && sortOrder !== SortOrder.None) { - displayValues.sort((a, b) => arrayUtils.sortValues(sortOrder)(a.value, b.value)); - } - return { displayValues, links }; } -export const DataHoverView = ({ - data, - rowIndex, - columnIndex, - sortOrder, - mode, - header, - padding = 0, - maxHeight, -}: Props) => { +export const DataHoverView = ({ data, rowIndex, header, padding = 0 }: Props) => { const styles = useStyles2(getStyles, padding); if (!data || rowIndex == null) { return null; } - const dispValuesAndLinks = getDisplayValuesAndLinks(data, rowIndex, columnIndex, sortOrder, mode); + const dispValuesAndLinks = getDisplayValuesAndLinks(data, rowIndex); if (dispValuesAndLinks == null) { return null; @@ -118,10 +63,6 @@ export const DataHoverView = ({ const { displayValues, links } = dispValuesAndLinks; - if (header === 'Exemplar') { - return ; - } - return (
{header && ( @@ -154,6 +95,7 @@ export const DataHoverView = ({
); }; + const getStyles = (theme: GrafanaTheme2, padding = 0) => { return { wrapper: css({ diff --git a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx deleted file mode 100644 index 3325188ee5e..00000000000 --- a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { css } from '@emotion/css'; - -import { GrafanaTheme2, LinkModel } from '@grafana/data'; -import { DataLinkButton, useStyles2 } from '@grafana/ui'; -import { VizTooltipRow } from '@grafana/ui/internal'; -import { renderValue } from 'app/plugins/panel/geomap/utils/uiUtils'; - -import { DisplayValue } from './DataHoverView'; - -export interface Props { - displayValues: DisplayValue[]; - links?: LinkModel[]; - header?: string; - maxHeight?: number; -} - -export const ExemplarHoverView = ({ displayValues, links, header = 'Exemplar', maxHeight }: Props) => { - const styles = useStyles2(getStyles, 0, maxHeight); - - const time = displayValues.find((val) => val.name === 'Time'); - displayValues = displayValues.filter((val) => val.name !== 'Time'); // time? - - return ( -
-
- {header} - {time && {renderValue(time.valueString)}} -
-
- {displayValues.map((displayValue, i) => { - return ( - - ); - })} -
- {links && links.length > 0 && ( -
- {links.map((link, i) => ( - - ))} -
- )} -
- ); -}; - -const getStyles = (theme: GrafanaTheme2, padding = 0, maxHeight?: number) => { - return { - exemplarWrapper: css({ - display: 'flex', - flexDirection: 'column', - flex: 1, - gap: 4, - whiteSpace: 'pre', - borderRadius: theme.shape.radius.default, - background: theme.colors.background.primary, - border: `1px solid ${theme.colors.border.weak}`, - }), - exemplarHeader: css({ - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - gap: theme.spacing(0.5), - color: theme.colors.text.secondary, - padding: theme.spacing(1), - }), - time: css({ - color: theme.colors.text.primary, - }), - exemplarContent: css({ - display: 'flex', - flexDirection: 'column', - flex: 1, - gap: 4, - borderTop: `1px solid ${theme.colors.border.medium}`, - padding: theme.spacing(1), - overflowY: 'auto', - maxHeight: maxHeight, - }), - exemplarFooter: css({ - display: 'flex', - flexDirection: 'column', - padding: theme.spacing(1), - borderTop: `1px solid ${theme.colors.border.medium}`, - gap: 4, - }), - linkButton: css({ - width: 'fit-content', - }), - label: css({ - color: theme.colors.text.secondary, - fontWeight: 400, - textOverflow: 'ellipsis', - overflow: 'hidden', - marginRight: theme.spacing(0.5), - }), - value: css({ - fontWeight: 500, - textOverflow: 'ellipsis', - overflow: 'hidden', - }), - title: css({ - fontWeight: theme.typography.fontWeightMedium, - overflow: 'hidden', - display: 'inline-block', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - flexGrow: 1, - }), - }; -}; diff --git a/public/app/features/visualization/data-hover/ExemplarTooltip.tsx b/public/app/features/visualization/data-hover/ExemplarTooltip.tsx new file mode 100644 index 00000000000..9473c7b70d5 --- /dev/null +++ b/public/app/features/visualization/data-hover/ExemplarTooltip.tsx @@ -0,0 +1,39 @@ +import { LinkModel } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { + VizTooltipContent, + VizTooltipFooter, + VizTooltipItem, + VizTooltipHeader, + VizTooltipWrapper, +} from '@grafana/ui/internal'; + +export interface Props { + items: VizTooltipItem[]; + links?: LinkModel[]; + isPinned: boolean; + maxHeight?: number; +} + +export const ExemplarTooltip = ({ items, links, isPinned, maxHeight }: Props) => { + const timeItem = items.find((val) => val.label === 'Time'); + + return ( + + + item !== timeItem)} + isPinned={isPinned} + maxHeight={maxHeight} + scrollable={maxHeight != null} + /> + + + ); +}; diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index 39d68078b51..b07ec3fe71f 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -324,7 +324,13 @@ export const CandlestickPanel = ({ /> {data.annotations && ( - + )} {((canEditThresholds && onThresholdsChange) || showThresholds) && ( void; style?: React.CSSProperties }) { - const defaultStyle: CSSProperties = { - position: 'relative', - top: 'auto', - right: 'auto', - marginRight: 0, - }; - - return ( -
- -
- ); -} diff --git a/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx index 5859e93ac79..48ca1fe7578 100644 --- a/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx @@ -27,7 +27,8 @@ import { import { ColorScale } from 'app/core/components/ColorScale/ColorScale'; 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 { getDisplayValuesAndLinks } from 'app/features/visualization/data-hover/DataHoverView'; +import { ExemplarTooltip } from 'app/features/visualization/data-hover/ExemplarTooltip'; import { getDataLinks, getFieldActions } from '../status-history/utils'; import { isTooltipScrollable } from '../timeseries/utils'; @@ -54,13 +55,23 @@ interface HeatmapTooltipProps { export const HeatmapTooltip = (props: HeatmapTooltipProps) => { if (props.seriesIdx === 2) { + const dispValuesAndLinks = getDisplayValuesAndLinks(props.dataRef.current!.exemplars!, props.dataIdxs[2]!); + + if (dispValuesAndLinks == null) { + return null; + } + + const { displayValues, links } = dispValuesAndLinks; + return ( - ({ + label: dispVal.name, + value: dispVal.valueString, + }))} + links={links} maxHeight={props.maxHeight} + isPinned={props.isPinned} /> ); } diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index c948b75b296..49d34358a93 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -160,6 +160,7 @@ export const TimeSeriesPanel = ({ exemplars={data.annotations} timeZone={timeZone} maxHeight={options.tooltip.maxHeight} + maxWidth={options.tooltip.maxWidth} /> )} {((canEditThresholds && onThresholdsChange) || showThresholds) && ( diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx index f41e5e3af7a..3ac418177f9 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx @@ -9,40 +9,45 @@ import { useHover, useInteractions, } from '@floating-ui/react'; -import { CSSProperties, useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import * as React from 'react'; -import { DataFrame, DataFrameFieldIndex, Field, formattedValueToString, GrafanaTheme2, LinkModel } from '@grafana/data'; +import { DataFrame, Field, formattedValueToString, GrafanaTheme2, LinkModel } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { TimeZone } from '@grafana/schema'; import { Portal, UPlotConfigBuilder, useStyles2 } from '@grafana/ui'; -import { DisplayValue } from 'app/features/visualization/data-hover/DataHoverView'; -import { ExemplarHoverView } from 'app/features/visualization/data-hover/ExemplarHoverView'; +import { VizTooltipItem } from '@grafana/ui/internal'; +import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; +import { ExemplarTooltip } from 'app/features/visualization/data-hover/ExemplarTooltip'; -import { ExemplarModalHeader } from '../../heatmap/ExemplarModalHeader'; +import { getDataLinks } from '../../status-history/utils'; interface ExemplarMarkerProps { timeZone: TimeZone; dataFrame: DataFrame; - dataFrameFieldIndex: DataFrameFieldIndex; + frameIndex: number; + rowIndex: number; config: UPlotConfigBuilder; exemplarColor?: string; - clickedExemplarFieldIndex: DataFrameFieldIndex | undefined; - setClickedExemplarFieldIndex: React.Dispatch; + clickedRowIndex: number | undefined; + setClickedRowIndex: React.Dispatch; maxHeight?: number; + maxWidth?: number; } export const ExemplarMarker = ({ timeZone, dataFrame, - dataFrameFieldIndex, + frameIndex, + rowIndex, config, exemplarColor, - clickedExemplarFieldIndex, - setClickedExemplarFieldIndex, + clickedRowIndex, + setClickedRowIndex, maxHeight, + maxWidth, }: ExemplarMarkerProps) => { - const styles = useStyles2(getExemplarMarkerStyles); + const styles = useStyles2(getExemplarMarkerStyles, maxWidth); const [isOpen, setIsOpen] = useState(false); const [isLocked, setIsLocked] = useState(false); @@ -69,24 +74,19 @@ export const ExemplarMarker = ({ const dismiss = useDismiss(context); const hover = useHover(context, { handleClose: safePolygon(), - enabled: clickedExemplarFieldIndex === undefined, + enabled: clickedRowIndex === undefined, }); const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, hover]); useEffect(() => { - if ( - !( - clickedExemplarFieldIndex?.fieldIndex === dataFrameFieldIndex.fieldIndex && - clickedExemplarFieldIndex?.frameIndex === dataFrameFieldIndex.frameIndex - ) - ) { + if (clickedRowIndex !== rowIndex) { setIsLocked(false); } - }, [clickedExemplarFieldIndex, dataFrameFieldIndex]); + }, [clickedRowIndex, rowIndex]); const getSymbol = () => { - const symbols = [ + return ( , - , - , - , - , - , - ]; + /> + ); - return symbols[dataFrameFieldIndex.frameIndex % symbols.length]; + // const symbols = [ + // , + // , + // , + // , + // , + // , + // ]; + + // return symbols[dataFrameFieldIndex.frameIndex % symbols.length]; }; const lockExemplarModal = () => { @@ -122,70 +133,56 @@ export const ExemplarMarker = ({ }; const renderMarker = useCallback(() => { - //Put fields with links on the top - const fieldsWithLinks = - dataFrame.fields.filter((field) => field.config.links?.length && field.config.links?.length > 0) || []; - const orderedDataFrameFields = [ - ...fieldsWithLinks, - ...dataFrame.fields.filter((field) => !fieldsWithLinks.includes(field)), - ]; - const onClose = () => { setIsLocked(false); setIsOpen(false); - setClickedExemplarFieldIndex(undefined); + setClickedRowIndex(undefined); }; - let displayValues: DisplayValue[] = []; - let links: LinkModel[] | undefined = []; - orderedDataFrameFields.map((field: Field, i) => { - const value = field.values[dataFrameFieldIndex.fieldIndex]; + let items: VizTooltipItem[] = []; + let links: LinkModel[] = []; - if (field.config.links?.length) { - links?.push(...(field.getLinks?.({ valueRowIndex: dataFrameFieldIndex.fieldIndex }) || [])); - } + dataFrame.fields.forEach((field: Field) => { + const value = field.values[rowIndex]; - const fieldDisplay = field.display ? field.display(value) : { text: `${value}`, numeric: +value }; + links.push(...getDataLinks(field, rowIndex)); - displayValues.push({ - name: field.name, - value, - valueString: formattedValueToString(fieldDisplay), - highlight: false, + const fieldDisplay = field.display?.(value) ?? { text: `${value}`, numeric: +value }; + + items.push({ + label: field.state?.displayName ?? field.name, + value: formattedValueToString(fieldDisplay), }); }); - const exemplarHeaderCustomStyle: CSSProperties = { - position: 'relative', - top: '35px', - right: '5px', - marginRight: 0, - }; - return ( -
- {isLocked && } - +
+ {isLocked && } +
); }, [ dataFrame.fields, - dataFrameFieldIndex, + rowIndex, styles, isLocked, - setClickedExemplarFieldIndex, + setClickedRowIndex, floatingStyles, getFloatingProps, refs.setFloating, maxHeight, ]); - const seriesColor = config - .getSeries() - .find((s) => s.props.dataFrameFieldIndex?.frameIndex === dataFrameFieldIndex.frameIndex)?.props.lineColor; + const seriesColor = config.getSeries().find((s) => s.props.dataFrameFieldIndex?.frameIndex === frameIndex) + ?.props.lineColor; const onExemplarClick = () => { - setClickedExemplarFieldIndex(dataFrameFieldIndex); + setClickedRowIndex(rowIndex); lockExemplarModal(); }; @@ -220,12 +217,7 @@ export const ExemplarMarker = ({ ); }; -const getExemplarMarkerStyles = (theme: GrafanaTheme2) => { - const bg = theme.isDark ? theme.v1.palette.dark2 : theme.v1.palette.white; - const headerBg = theme.isDark ? theme.v1.palette.dark9 : theme.v1.palette.gray5; - const shadowColor = theme.isDark ? theme.v1.palette.black : theme.v1.palette.white; - const tableBgOdd = theme.isDark ? theme.v1.palette.dark3 : theme.v1.palette.gray6; - +const getExemplarMarkerStyles = (theme: GrafanaTheme2, maxWidth: number | undefined) => { return { markerWrapper: css({ padding: '0 4px 4px 4px', @@ -249,66 +241,6 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme2) => { borderBottom: `4px solid ${theme.v1.palette.red}`, pointerEvents: 'none', }), - wrapper: css({ - background: bg, - border: `1px solid ${headerBg}`, - borderRadius: theme.shape.borderRadius(2), - boxShadow: `0 0 20px ${shadowColor}`, - padding: theme.spacing(1), - }), - exemplarsTable: css({ - width: '100%', - 'tr td': { - padding: '5px 10px', - whiteSpace: 'nowrap', - borderBottom: `4px solid ${theme.components.panel.background}`, - }, - tr: { - backgroundColor: theme.colors.background.primary, - '&:nth-child(even)': { - backgroundColor: tableBgOdd, - }, - }, - }), - valueWrapper: css({ - display: 'flex', - flexDirection: 'row', - flexWrap: 'wrap', - columnGap: theme.spacing(1), - '> span': { - flexGrow: 0, - }, - '> *': { - flex: '1 1', - alignSelf: 'center', - }, - }), - tooltip: css({ - background: 'none', - padding: 0, - overflowY: 'auto', - maxHeight: '95vh', - boxShadow: theme.shadows.z2, - }), - header: css({ - background: headerBg, - padding: '6px 10px', - display: 'flex', - }), - title: css({ - fontWeight: theme.typography.fontWeightMedium, - paddingRight: theme.spacing(2), - overflow: 'hidden', - display: 'inline-block', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - flexGrow: 1, - }), - body: css({ - fontWeight: theme.typography.fontWeightMedium, - borderRadius: theme.shape.borderRadius(2), - overflow: 'hidden', - }), marble: css({ display: 'block', opacity: 0.5, @@ -321,5 +253,18 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme2) => { opacity: 1, filter: 'drop-shadow(0 0 8px rgba(0, 0, 0, 0.5))', }), + tooltipWrapper: css({ + background: theme.colors.background.elevated, + maxWidth: maxWidth ?? 'none', + whiteSpace: 'pre', + borderRadius: theme.shape.radius.default, + position: 'fixed', + border: `1px solid ${theme.colors.border.weak}`, + boxShadow: theme.shadows.z2, + userSelect: 'text', + }), + pinned: css({ + boxShadow: theme.shadows.z3, + }), }; }; diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 673f2288dc7..50974a7ac94 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -1,14 +1,7 @@ -import { useCallback, useLayoutEffect, useRef, useState } from 'react'; +import { ReactNode, useCallback, useLayoutEffect, useRef, useState } from 'react'; import uPlot from 'uplot'; -import { - DataFrame, - DataFrameFieldIndex, - Labels, - TIME_SERIES_TIME_FIELD_NAME, - TIME_SERIES_VALUE_FIELD_NAME, - TimeZone, -} from '@grafana/data'; +import { DataFrame, Labels, TIME_SERIES_TIME_FIELD_NAME, TIME_SERIES_VALUE_FIELD_NAME, TimeZone } from '@grafana/data'; import { FIXED_UNIT, EventsCanvas, UPlotConfigBuilder } from '@grafana/ui'; import { ExemplarMarker } from './ExemplarMarker'; @@ -19,12 +12,20 @@ interface ExemplarsPluginProps { timeZone: TimeZone; visibleSeries?: VisibleExemplarLabels; maxHeight?: number; + maxWidth?: number; } -export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries, maxHeight }: ExemplarsPluginProps) => { +export const ExemplarsPlugin = ({ + exemplars, + timeZone, + config, + visibleSeries, + maxHeight, + maxWidth, +}: ExemplarsPluginProps) => { const plotInstance = useRef(); - const [lockedExemplarFieldIndex, setLockedExemplarFieldIndex] = useState(); + const [lockedExemplarRowIndex, setLockedExemplarRowIndex] = useState(); useLayoutEffect(() => { config.addHook('init', (u) => { @@ -32,7 +33,7 @@ export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries, ma }); }, [config]); - const mapExemplarToXYCoords = useCallback((dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { + const mapExemplarToXYCoords = useCallback((dataFrame: DataFrame, rowIndex: number) => { const time = dataFrame.fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME); const value = dataFrame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME); @@ -47,7 +48,7 @@ export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries, ma const yMin = plotInstance.current.scales[yScale].min; const yMax = plotInstance.current.scales[yScale].max; - let y = value.values[dataFrameFieldIndex.fieldIndex]; + let y = value.values[rowIndex]; // To not to show exemplars outside of the graph we set the y value to min if it is smaller and max if it is bigger than the size of the graph if (yMin != null && y < yMin) { y = yMin; @@ -58,18 +59,17 @@ export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries, ma } return { - x: plotInstance.current.valToPos(time.values[dataFrameFieldIndex.fieldIndex], 'x'), + x: plotInstance.current.valToPos(time.values[rowIndex], 'x'), y: plotInstance.current.valToPos(y, yScale), }; }, []); const renderMarker = useCallback( - (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { - const showMarker = - visibleSeries !== undefined ? showExemplarMarker(visibleSeries, dataFrame, dataFrameFieldIndex) : true; + (dataFrame: DataFrame, rowIndex: number): ReactNode => { + const showMarker = visibleSeries !== undefined ? showExemplarMarker(visibleSeries, dataFrame, rowIndex) : true; const markerColor = - visibleSeries !== undefined ? getExemplarColor(dataFrame, dataFrameFieldIndex, visibleSeries) : undefined; + visibleSeries !== undefined ? getExemplarColor(dataFrame, rowIndex, visibleSeries) : undefined; if (!showMarker) { return <>; @@ -77,18 +77,20 @@ export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries, ma return ( ); }, - [config, timeZone, visibleSeries, setLockedExemplarFieldIndex, lockedExemplarFieldIndex, maxHeight] + [visibleSeries, lockedExemplarRowIndex, timeZone, config, maxHeight, maxWidth] ); return ( @@ -138,11 +140,7 @@ interface LabelWithExemplarUIData { /** * Get color of active series in legend */ -const getExemplarColor = ( - dataFrame: DataFrame, - dataFrameFieldIndex: DataFrameFieldIndex, - visibleLabels: VisibleExemplarLabels -) => { +const getExemplarColor = (dataFrame: DataFrame, rowIndex: number, visibleLabels: VisibleExemplarLabels) => { let exemplarColor; visibleLabels.labels.some((visibleLabel) => { const labelKeys = Object.keys(visibleLabel.labels); @@ -151,7 +149,7 @@ const getExemplarColor = ( }); if (fields.length) { const hasMatch = fields.every((field, index, fields) => { - const value = field.values[dataFrameFieldIndex.fieldIndex]; + const value = field.values[rowIndex]; return visibleLabel.labels[field.name] === value; }); @@ -168,11 +166,7 @@ const getExemplarColor = ( /** * Determine if the current exemplar marker is filtered by what series are selected in the legend UI */ -const showExemplarMarker = ( - visibleSeries: VisibleExemplarLabels, - dataFrame: DataFrame, - dataFrameFieldIndex: DataFrameFieldIndex -) => { +const showExemplarMarker = (visibleSeries: VisibleExemplarLabels, dataFrame: DataFrame, rowIndex: number) => { let showMarker = false; // If all series are visible, don't filter any exemplars if (visibleSeries.labels.length === visibleSeries.totalSeriesCount) { @@ -196,7 +190,7 @@ const showExemplarMarker = ( showMarker = visibleSeries.labels.some((series) => { return Object.keys(series.labels).every((label) => { const value = series.labels[label]; - return fields.find((field) => field.values[dataFrameFieldIndex.fieldIndex] === value); + return fields.find((field) => field.values[rowIndex] === value); }); }); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 3bfd1996f34..fddcbfc2009 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -6806,6 +6806,7 @@ } } }, + "exemplar-tooltip-header": "Exemplar", "explore": { "accordian-logs": { "events": "Events",