mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
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:
parent
7476219b0c
commit
1dad340ab3
26
public/app/plugins/panel/heatmap/ExemplarModalHeader.tsx
Normal file
26
public/app/plugins/panel/heatmap/ExemplarModalHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user