Prometheus: Add exemplar modal "locking" for time series (#63896)

* add ability to lock exemplar open state until user clicks off
This commit is contained in:
Galen Kistler 2023-04-13 10:28:04 -05:00 committed by GitHub
parent 7476219b0c
commit 1dad340ab3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 87 additions and 35 deletions

View File

@ -0,0 +1,26 @@
import React from 'react';
import { CloseButton } from '../../../core/components/CloseButton/CloseButton';
export function ExemplarModalHeader(props: { onClick: () => void }) {
return (
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'flex-end',
paddingBottom: '6px',
}}
>
<CloseButton
onClick={props.onClick}
style={{
position: 'relative',
top: 'auto',
right: 'auto',
marginRight: 0,
}}
/>
</div>
);
}

View File

@ -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 && (
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'flex-end',
paddingBottom: '6px',
}}
>
<CloseButton
onClick={onCloseToolTip}
style={{
position: 'relative',
top: 'auto',
right: 'auto',
marginRight: 0,
}}
/>
</div>
)}
{shouldDisplayCloseButton && <ExemplarModalHeader onClick={onCloseToolTip} />}
<HeatmapHoverView
timeRange={timeRange}
data={info}

View File

@ -1,5 +1,5 @@
import { css, cx } from '@emotion/css';
import React, { useCallback, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { usePopper } from 'react-popper';
import {
@ -16,6 +16,8 @@ import {
import { selectors } from '@grafana/e2e-selectors';
import { FieldLinkList, Portal, UPlotConfigBuilder, useStyles2 } from '@grafana/ui';
import { ExemplarModalHeader } from '../../heatmap/ExemplarModalHeader';
interface ExemplarMarkerProps {
timeZone: TimeZone;
dataFrame: DataFrame;
@ -23,6 +25,8 @@ interface ExemplarMarkerProps {
config: UPlotConfigBuilder;
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
exemplarColor?: string;
clickedExemplarFieldIndex: DataFrameFieldIndex | undefined;
setClickedExemplarFieldIndex: React.Dispatch<DataFrameFieldIndex | undefined>;
}
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<HTMLDivElement | null>(null);
const [popperElement, setPopperElement] = React.useState<HTMLDivElement | null>(null);
const { styles: popperStyles, attributes } = usePopper(markerElement, popperElement, {
@ -55,6 +62,17 @@ export const ExemplarMarker = ({
});
const popoverRenderTimeout = useRef<NodeJS.Timer>();
useEffect(() => {
if (
!(
clickedExemplarFieldIndex?.fieldIndex === dataFrameFieldIndex.fieldIndex &&
clickedExemplarFieldIndex?.frameIndex === dataFrameFieldIndex.frameIndex
)
) {
setIsLocked(false);
}
}, [clickedExemplarFieldIndex, dataFrameFieldIndex]);
const getSymbol = () => {
const symbols = [
<rect
@ -88,16 +106,22 @@ export const ExemplarMarker = ({
};
const onMouseEnter = useCallback(() => {
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 (
<div
onMouseEnter={onMouseEnter}
@ -122,10 +152,11 @@ export const ExemplarMarker = ({
{...attributes.popper}
>
<div className={styles.wrapper}>
<div className={styles.header}>
<span className={styles.title}>Exemplar</span>
</div>
{isLocked && <ExemplarModalHeader onClick={onClose} />}
<div className={styles.body}>
<div className={styles.header}>
<span className={styles.title}>Exemplars</span>
</div>
<div>
<table className={styles.exemplarsTable}>
<tbody>
@ -163,6 +194,8 @@ export const ExemplarMarker = ({
popperStyles.popper,
styles,
timeZone,
isLocked,
setClickedExemplarFieldIndex,
]);
const seriesColor = config
@ -173,6 +206,10 @@ export const ExemplarMarker = ({
<>
<div
ref={setMarkerElement}
onClick={() => {
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()}
</svg>
</div>
{isOpen && <Portal>{renderMarker()}</Portal>}
{(isOpen || isLocked) && <Portal>{renderMarker()}</Portal>}
</>
);
};
@ -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;

View File

@ -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<uPlot>();
const [lockedExemplarFieldIndex, setLockedExemplarFieldIndex] = useState<DataFrameFieldIndex | undefined>();
useLayoutEffect(() => {
config.addHook('init', (u) => {
plotInstance.current = u;
@ -83,6 +85,8 @@ export const ExemplarsPlugin = ({
return (
<ExemplarMarker
setClickedExemplarFieldIndex={setLockedExemplarFieldIndex}
clickedExemplarFieldIndex={lockedExemplarFieldIndex}
timeZone={timeZone}
getFieldLinks={getFieldLinks}
dataFrame={dataFrame}
@ -92,7 +96,7 @@ export const ExemplarsPlugin = ({
/>
);
},
[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
*/