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
*/