- {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",