diff --git a/public/app/plugins/panel/heatmap/ExemplarModalHeader.tsx b/public/app/plugins/panel/heatmap/ExemplarModalHeader.tsx new file mode 100644 index 00000000000..12bcaaa8656 --- /dev/null +++ b/public/app/plugins/panel/heatmap/ExemplarModalHeader.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import { CloseButton } from '../../../core/components/CloseButton/CloseButton'; + +export function ExemplarModalHeader(props: { onClick: () => void }) { + return ( +
+ +
+ ); +} diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index 775dd09fb86..e31d1f4bc5b 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -14,10 +14,10 @@ import { VizLayout, VizTooltipContainer, } from '@grafana/ui'; -import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { ColorScale } from 'app/core/components/ColorScale/ColorScale'; import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; +import { ExemplarModalHeader } from './ExemplarModalHeader'; import { HeatmapHoverView } from './HeatmapHoverView'; import { prepareHeatmapData } from './fields'; import { quantizeScheme } from './palettes'; @@ -206,26 +206,7 @@ export const HeatmapPanel = ({ offset={{ x: 10, y: 10 }} allowPointerEvents={isToolTipOpen.current} > - {shouldDisplayCloseButton && ( -
- -
- )} + {shouldDisplayCloseButton && } Array>; exemplarColor?: string; + clickedExemplarFieldIndex: DataFrameFieldIndex | undefined; + setClickedExemplarFieldIndex: React.Dispatch; } export const ExemplarMarker = ({ @@ -32,9 +36,12 @@ export const ExemplarMarker = ({ config, getFieldLinks, exemplarColor, + clickedExemplarFieldIndex, + setClickedExemplarFieldIndex, }: ExemplarMarkerProps) => { const styles = useStyles2(getExemplarMarkerStyles); const [isOpen, setIsOpen] = useState(false); + const [isLocked, setIsLocked] = useState(false); const [markerElement, setMarkerElement] = React.useState(null); const [popperElement, setPopperElement] = React.useState(null); const { styles: popperStyles, attributes } = usePopper(markerElement, popperElement, { @@ -55,6 +62,17 @@ export const ExemplarMarker = ({ }); const popoverRenderTimeout = useRef(); + useEffect(() => { + if ( + !( + clickedExemplarFieldIndex?.fieldIndex === dataFrameFieldIndex.fieldIndex && + clickedExemplarFieldIndex?.frameIndex === dataFrameFieldIndex.frameIndex + ) + ) { + setIsLocked(false); + } + }, [clickedExemplarFieldIndex, dataFrameFieldIndex]); + const getSymbol = () => { const symbols = [ { - if (popoverRenderTimeout.current) { - clearTimeout(popoverRenderTimeout.current); + if (clickedExemplarFieldIndex === undefined) { + if (popoverRenderTimeout.current) { + clearTimeout(popoverRenderTimeout.current); + } + setIsOpen(true); } - setIsOpen(true); - }, [setIsOpen]); + }, [setIsOpen, clickedExemplarFieldIndex]); + + const lockExemplarModal = () => { + setIsLocked(true); + }; const onMouseLeave = useCallback(() => { popoverRenderTimeout.current = setTimeout(() => { setIsOpen(false); - }, 100); + }, 150); }, [setIsOpen]); const renderMarker = useCallback(() => { @@ -112,6 +136,12 @@ export const ExemplarMarker = ({ }); }; + const onClose = () => { + setIsLocked(false); + setIsOpen(false); + setClickedExemplarFieldIndex(undefined); + }; + return (
-
- Exemplar -
+ {isLocked && }
+
+ Exemplars +
@@ -163,6 +194,8 @@ export const ExemplarMarker = ({ popperStyles.popper, styles, timeZone, + isLocked, + setClickedExemplarFieldIndex, ]); const seriesColor = config @@ -173,6 +206,10 @@ export const ExemplarMarker = ({ <>
{ + setClickedExemplarFieldIndex(dataFrameFieldIndex); + lockExemplarModal(); + }} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} className={styles.markerWrapper} @@ -183,12 +220,12 @@ export const ExemplarMarker = ({ width="7" height="7" style={{ fill: seriesColor }} - className={cx(styles.marble, isOpen && styles.activeMarble)} + className={cx(styles.marble, (isOpen || isLocked) && styles.activeMarble)} > {getSymbol()}
- {isOpen && {renderMarker()}} + {(isOpen || isLocked) && {renderMarker()}} ); }; @@ -228,6 +265,7 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme2) => { border: 1px solid ${headerBg}; border-radius: ${theme.shape.borderRadius(2)}; box-shadow: 0 0 20px ${shadowColor}; + padding: ${theme.spacing(1)}; `, exemplarsTable: css` width: 100%; @@ -240,6 +278,7 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme2) => { tr { background-color: ${theme.colors.background.primary}; + &:nth-child(even) { background-color: ${tableBgOdd}; } @@ -281,8 +320,9 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme2) => { flex-grow: 1; `, body: css` - padding: ${theme.spacing(1)}; font-weight: ${theme.typography.fontWeightMedium}; + border-radius: ${theme.shape.borderRadius(2)}; + overflow: hidden; `, marble: css` display: block; diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index c9d0d254e4d..46892b323f5 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useLayoutEffect, useRef } from 'react'; +import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'; import uPlot from 'uplot'; import { @@ -32,6 +32,8 @@ export const ExemplarsPlugin = ({ }: ExemplarsPluginProps) => { const plotInstance = useRef(); + const [lockedExemplarFieldIndex, setLockedExemplarFieldIndex] = useState(); + useLayoutEffect(() => { config.addHook('init', (u) => { plotInstance.current = u; @@ -83,6 +85,8 @@ export const ExemplarsPlugin = ({ return ( ); }, - [config, timeZone, getFieldLinks, visibleSeries] + [config, timeZone, getFieldLinks, visibleSeries, setLockedExemplarFieldIndex, lockedExemplarFieldIndex] ); return ( @@ -138,6 +142,7 @@ interface LabelWithExemplarUIData { labels: Labels; color?: string; } + /** * Get color of active series in legend */