Prometheus: exemplars show different symbols (#34763) (#34899)

* Show different symbols for different queries

* Only run different exemplars

* Address review comment

* Do the same for dashboard + tests

(cherry picked from commit 4435895833)

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
Grot (@grafanabot) 2021-05-28 10:05:33 -04:00 committed by GitHub
parent 6d6e875a84
commit f704fa423d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 239 additions and 59 deletions

View File

@ -1,4 +1,4 @@
import { DataFrame } from '@grafana/data'; import { DataFrame, DataFrameFieldIndex } from '@grafana/data';
import React, { useLayoutEffect, useMemo, useState } from 'react'; import React, { useLayoutEffect, useMemo, useState } from 'react';
import { usePlotContext } from '../context'; import { usePlotContext } from '../context';
import { Marker } from './Marker'; import { Marker } from './Marker';
@ -9,8 +9,11 @@ interface EventsCanvasProps {
id: string; id: string;
config: UPlotConfigBuilder; config: UPlotConfigBuilder;
events: DataFrame[]; events: DataFrame[];
renderEventMarker: (dataFrame: DataFrame, index: number) => React.ReactNode; renderEventMarker: (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => React.ReactNode;
mapEventToXYCoords: (dataFrame: DataFrame, index: number) => { x: number; y: number } | undefined; mapEventToXYCoords: (
dataFrame: DataFrame,
dataFrameFieldIndex: DataFrameFieldIndex
) => { x: number; y: number } | undefined;
} }
export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords, config }: EventsCanvasProps) { export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords, config }: EventsCanvasProps) {
@ -35,13 +38,13 @@ export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords
for (let i = 0; i < events.length; i++) { for (let i = 0; i < events.length; i++) {
const frame = events[i]; const frame = events[i];
for (let j = 0; j < frame.length; j++) { for (let j = 0; j < frame.length; j++) {
const coords = mapEventToXYCoords(frame, j); const coords = mapEventToXYCoords(frame, { fieldIndex: j, frameIndex: i });
if (!coords) { if (!coords) {
continue; continue;
} }
markers.push( markers.push(
<Marker {...coords} key={`${id}-marker-${i}-${j}`}> <Marker {...coords} key={`${id}-marker-${i}-${j}`}>
{renderEventMarker(frame, j)} {renderEventMarker(frame, { fieldIndex: j, frameIndex: i })}
</Marker> </Marker>
); );
} }

View File

@ -1759,7 +1759,17 @@ describe('PrometheusDatasource for POST', () => {
}); });
}); });
const getPrepareTargetsContext = (target: PromQuery, app?: CoreApp, queryOptions?: Partial<QueryOptions>) => { function getPrepareTargetsContext({
targets,
app,
queryOptions,
languageProvider,
}: {
targets: PromQuery[];
app?: CoreApp;
queryOptions?: Partial<QueryOptions>;
languageProvider?: any;
}) {
const instanceSettings = ({ const instanceSettings = ({
url: 'proxied', url: 'proxied',
directUrl: 'direct', directUrl: 'direct',
@ -1771,7 +1781,7 @@ const getPrepareTargetsContext = (target: PromQuery, app?: CoreApp, queryOptions
const end = 1; const end = 1;
const panelId = '2'; const panelId = '2';
const options = ({ const options = ({
targets: [target], targets,
interval: '1s', interval: '1s',
panelId, panelId,
app, app,
@ -1779,6 +1789,9 @@ const getPrepareTargetsContext = (target: PromQuery, app?: CoreApp, queryOptions
} as any) as DataQueryRequest<PromQuery>; } as any) as DataQueryRequest<PromQuery>;
const ds = new PrometheusDatasource(instanceSettings, templateSrvStub as any, timeSrvStub as any); const ds = new PrometheusDatasource(instanceSettings, templateSrvStub as any, timeSrvStub as any);
if (languageProvider) {
ds.languageProvider = languageProvider;
}
const { queries, activeTargets } = ds.prepareTargets(options, start, end); const { queries, activeTargets } = ds.prepareTargets(options, start, end);
return { return {
@ -1788,7 +1801,7 @@ const getPrepareTargetsContext = (target: PromQuery, app?: CoreApp, queryOptions
end, end,
panelId, panelId,
}; };
}; }
describe('prepareTargets', () => { describe('prepareTargets', () => {
describe('when run from a Panel', () => { describe('when run from a Panel', () => {
@ -1799,7 +1812,7 @@ describe('prepareTargets', () => {
requestId: '2A', requestId: '2A',
}; };
const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target); const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext({ targets: [target] });
expect(queries.length).toBe(1); expect(queries.length).toBe(1);
expect(activeTargets.length).toBe(1); expect(activeTargets.length).toBe(1);
@ -1819,6 +1832,51 @@ describe('prepareTargets', () => {
}); });
expect(activeTargets[0]).toEqual(target); expect(activeTargets[0]).toEqual(target);
}); });
it('should give back 3 targets when multiple queries with exemplar enabled and same metric', () => {
const targetA: PromQuery = {
refId: 'A',
expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
exemplar: true,
};
const targetB: PromQuery = {
refId: 'B',
expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
exemplar: true,
};
const { queries, activeTargets } = getPrepareTargetsContext({
targets: [targetA, targetB],
languageProvider: {
histogramMetrics: ['tns_request_duration_seconds_bucket'],
},
});
expect(queries).toHaveLength(3);
expect(activeTargets).toHaveLength(3);
});
it('should give back 4 targets when multiple queries with exemplar enabled', () => {
const targetA: PromQuery = {
refId: 'A',
expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
exemplar: true,
};
const targetB: PromQuery = {
refId: 'B',
expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_bucket[5m])) by (le))',
exemplar: true,
};
const { queries, activeTargets } = getPrepareTargetsContext({
targets: [targetA, targetB],
languageProvider: {
histogramMetrics: ['tns_request_duration_seconds_bucket'],
},
});
expect(queries).toHaveLength(4);
expect(activeTargets).toHaveLength(4);
});
it('should give back 2 targets when exemplar enabled', () => { it('should give back 2 targets when exemplar enabled', () => {
const target: PromQuery = { const target: PromQuery = {
refId: 'A', refId: 'A',
@ -1826,7 +1884,7 @@ describe('prepareTargets', () => {
exemplar: true, exemplar: true,
}; };
const { queries, activeTargets } = getPrepareTargetsContext(target); const { queries, activeTargets } = getPrepareTargetsContext({ targets: [target] });
expect(queries).toHaveLength(2); expect(queries).toHaveLength(2);
expect(activeTargets).toHaveLength(2); expect(activeTargets).toHaveLength(2);
expect(activeTargets[0].exemplar).toBe(true); expect(activeTargets[0].exemplar).toBe(true);
@ -1840,7 +1898,7 @@ describe('prepareTargets', () => {
instant: true, instant: true,
}; };
const { queries, activeTargets } = getPrepareTargetsContext(target); const { queries, activeTargets } = getPrepareTargetsContext({ targets: [target] });
expect(queries).toHaveLength(1); expect(queries).toHaveLength(1);
expect(activeTargets).toHaveLength(1); expect(activeTargets).toHaveLength(1);
expect(activeTargets[0].instant).toBe(true); expect(activeTargets[0].instant).toBe(true);
@ -1849,6 +1907,60 @@ describe('prepareTargets', () => {
describe('when run from Explore', () => { describe('when run from Explore', () => {
describe('when query type Both is selected', () => { describe('when query type Both is selected', () => {
it('should give back 6 targets when multiple queries with exemplar enabled', () => {
const targetA: PromQuery = {
refId: 'A',
expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
instant: true,
range: true,
exemplar: true,
};
const targetB: PromQuery = {
refId: 'B',
expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_bucket[5m])) by (le))',
exemplar: true,
instant: true,
range: true,
};
const { queries, activeTargets } = getPrepareTargetsContext({
targets: [targetA, targetB],
app: CoreApp.Explore,
languageProvider: {
histogramMetrics: ['tns_request_duration_seconds_bucket'],
},
});
expect(queries).toHaveLength(6);
expect(activeTargets).toHaveLength(6);
});
it('should give back 5 targets when multiple queries with exemplar enabled and same metric', () => {
const targetA: PromQuery = {
refId: 'A',
expr: 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
instant: true,
range: true,
exemplar: true,
};
const targetB: PromQuery = {
refId: 'B',
expr: 'histogram_quantile(0.5, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))',
exemplar: true,
instant: true,
range: true,
};
const { queries, activeTargets } = getPrepareTargetsContext({
targets: [targetA, targetB],
app: CoreApp.Explore,
languageProvider: {
histogramMetrics: ['tns_request_duration_seconds_bucket'],
},
});
expect(queries).toHaveLength(5);
expect(activeTargets).toHaveLength(5);
});
it('then it should return both instant and time series related objects', () => { it('then it should return both instant and time series related objects', () => {
const target: PromQuery = { const target: PromQuery = {
refId: 'A', refId: 'A',
@ -1858,7 +1970,10 @@ describe('prepareTargets', () => {
requestId: '2A', requestId: '2A',
}; };
const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target, CoreApp.Explore); const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext({
targets: [target],
app: CoreApp.Explore,
});
expect(queries.length).toBe(2); expect(queries.length).toBe(2);
expect(activeTargets.length).toBe(2); expect(activeTargets.length).toBe(2);
@ -1916,7 +2031,10 @@ describe('prepareTargets', () => {
requestId: '2A', requestId: '2A',
}; };
const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target, CoreApp.Explore); const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext({
targets: [target],
app: CoreApp.Explore,
});
expect(queries.length).toBe(1); expect(queries.length).toBe(1);
expect(activeTargets.length).toBe(1); expect(activeTargets.length).toBe(1);
@ -1949,7 +2067,10 @@ describe('prepareTargets', () => {
requestId: '2A', requestId: '2A',
}; };
const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext(target, CoreApp.Explore); const { queries, activeTargets, panelId, end, start } = getPrepareTargetsContext({
targets: [target],
app: CoreApp.Explore,
});
expect(queries.length).toBe(1); expect(queries.length).toBe(1);
expect(activeTargets.length).toBe(1); expect(activeTargets.length).toBe(1);

View File

@ -206,6 +206,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
} }
target.requestId = options.panelId + target.refId; target.requestId = options.panelId + target.refId;
const metricName = this.languageProvider.histogramMetrics.find((m) => target.expr.includes(m));
// In Explore, we run both (instant and range) queries if both are true (selected) or both are undefined (legacy Explore queries) // In Explore, we run both (instant and range) queries if both are true (selected) or both are undefined (legacy Explore queries)
if (options.app === CoreApp.Explore && target.range === target.instant) { if (options.app === CoreApp.Explore && target.range === target.instant) {
@ -226,13 +227,19 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
// Create exemplar query // Create exemplar query
if (target.exemplar) { if (target.exemplar) {
const exemplarTarget = cloneDeep(target); // Only create exemplar target for different metric names
exemplarTarget.instant = false; if (
exemplarTarget.requestId += '_exemplar'; !metricName ||
(metricName && !activeTargets.some((activeTarget) => activeTarget.expr.includes(metricName)))
) {
const exemplarTarget = cloneDeep(target);
exemplarTarget.instant = false;
exemplarTarget.requestId += '_exemplar';
queries.push(this.createQuery(exemplarTarget, options, start, end));
activeTargets.push(exemplarTarget);
}
instantTarget.exemplar = false; instantTarget.exemplar = false;
rangeTarget.exemplar = false; rangeTarget.exemplar = false;
queries.push(this.createQuery(exemplarTarget, options, start, end));
activeTargets.push(exemplarTarget);
} }
// Add both targets to activeTargets and queries arrays // Add both targets to activeTargets and queries arrays
@ -250,12 +257,17 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
} else { } else {
// It doesn't make sense to query for exemplars in dashboard if only instant is selected // It doesn't make sense to query for exemplars in dashboard if only instant is selected
if (target.exemplar && !target.instant) { if (target.exemplar && !target.instant) {
const exemplarTarget = cloneDeep(target); if (
exemplarTarget.requestId += '_exemplar'; !metricName ||
(metricName && !activeTargets.some((activeTarget) => activeTarget.expr.includes(metricName)))
) {
const exemplarTarget = cloneDeep(target);
exemplarTarget.requestId += '_exemplar';
queries.push(this.createQuery(exemplarTarget, options, start, end));
activeTargets.push(exemplarTarget);
this.exemplarErrors.next();
}
target.exemplar = false; target.exemplar = false;
queries.push(this.createQuery(exemplarTarget, options, start, end));
activeTargets.push(exemplarTarget);
this.exemplarErrors.next();
} }
if (target.exemplar && target.instant) { if (target.exemplar && target.instant) {
this.exemplarErrors.next('Exemplars are not available for instant queries.'); this.exemplarErrors.next('Exemplars are not available for instant queries.');

View File

@ -1,4 +1,4 @@
import { DataFrame, DataFrameView, getColorForTheme, TimeZone } from '@grafana/data'; import { DataFrame, DataFrameFieldIndex, DataFrameView, getColorForTheme, TimeZone } from '@grafana/data';
import { EventsCanvas, UPlotConfigBuilder, usePlotContext, useTheme } from '@grafana/ui'; import { EventsCanvas, UPlotConfigBuilder, usePlotContext, useTheme } from '@grafana/ui';
import React, { useCallback, useEffect, useLayoutEffect, useRef } from 'react'; import React, { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
import { AnnotationMarker } from './AnnotationMarker'; import { AnnotationMarker } from './AnnotationMarker';
@ -65,9 +65,9 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
}, [config, theme]); }, [config, theme]);
const mapAnnotationToXYCoords = useCallback( const mapAnnotationToXYCoords = useCallback(
(frame: DataFrame, index: number) => { (frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame); const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
const annotation = view.get(index); const annotation = view.get(dataFrameFieldIndex.fieldIndex);
const plotInstance = plotCtx.plot; const plotInstance = plotCtx.plot;
if (!annotation.time || !plotInstance) { if (!annotation.time || !plotInstance) {
return undefined; return undefined;
@ -82,9 +82,9 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
); );
const renderMarker = useCallback( const renderMarker = useCallback(
(frame: DataFrame, index: number) => { (frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame); const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
const annotation = view.get(index); const annotation = view.get(dataFrameFieldIndex.fieldIndex);
return <AnnotationMarker annotation={annotation} timeZone={timeZone} />; return <AnnotationMarker annotation={annotation} timeZone={timeZone} />;
}, },
[timeZone] [timeZone]

View File

@ -1,6 +1,7 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { import {
DataFrame, DataFrame,
DataFrameFieldIndex,
dateTimeFormat, dateTimeFormat,
Field, Field,
FieldType, FieldType,
@ -10,18 +11,25 @@ import {
TimeZone, TimeZone,
} from '@grafana/data'; } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { FieldLinkList, Portal, useStyles } from '@grafana/ui'; import { FieldLinkList, Portal, UPlotConfigBuilder, useStyles } from '@grafana/ui';
import React, { useCallback, useRef, useState } from 'react'; import React, { useCallback, useRef, useState } from 'react';
import { usePopper } from 'react-popper'; import { usePopper } from 'react-popper';
interface ExemplarMarkerProps { interface ExemplarMarkerProps {
timeZone: TimeZone; timeZone: TimeZone;
dataFrame: DataFrame; dataFrame: DataFrame;
index: number; dataFrameFieldIndex: DataFrameFieldIndex;
config: UPlotConfigBuilder;
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>; getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
} }
export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({ timeZone, dataFrame, index, getFieldLinks }) => { export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({
timeZone,
dataFrame,
dataFrameFieldIndex,
config,
getFieldLinks,
}) => {
const styles = useStyles(getExemplarMarkerStyles); const styles = useStyles(getExemplarMarkerStyles);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [markerElement, setMarkerElement] = React.useState<HTMLDivElement | null>(null); const [markerElement, setMarkerElement] = React.useState<HTMLDivElement | null>(null);
@ -29,6 +37,24 @@ export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({ timeZone, dataFr
const { styles: popperStyles, attributes } = usePopper(markerElement, popperElement); const { styles: popperStyles, attributes } = usePopper(markerElement, popperElement);
const popoverRenderTimeout = useRef<NodeJS.Timer>(); const popoverRenderTimeout = useRef<NodeJS.Timer>();
const getSymbol = () => {
const symbols = [
<rect key="diamond" x="3.38672" width="4.78985" height="4.78985" transform="rotate(45 3.38672 0)" />,
<path
key="x"
d="M1.94444 3.49988L0 5.44432L1.55552 6.99984L3.49996 5.05539L5.4444 6.99983L6.99992 5.44431L5.05548 3.49988L6.99983 1.55552L5.44431 0L3.49996 1.94436L1.5556 0L8.42584e-05 1.55552L1.94444 3.49988Z"
/>,
<path key="triangle" d="M4 0L7.4641 6H0.535898L4 0Z" />,
<rect key="rectangle" width="5" height="5" />,
<path key="pentagon" d="M3 0.5L5.85317 2.57295L4.76336 5.92705H1.23664L0.146831 2.57295L3 0.5Z" />,
<path
key="plus"
d="m2.35672,4.2425l0,2.357l1.88558,0l0,-2.357l2.3572,0l0,-1.88558l-2.3572,0l0,-2.35692l-1.88558,0l0,2.35692l-2.35672,0l0,1.88558l2.35672,0z"
/>,
];
return symbols[dataFrameFieldIndex.frameIndex % symbols.length];
};
const onMouseEnter = useCallback(() => { const onMouseEnter = useCallback(() => {
if (popoverRenderTimeout.current) { if (popoverRenderTimeout.current) {
clearTimeout(popoverRenderTimeout.current); clearTimeout(popoverRenderTimeout.current);
@ -68,8 +94,10 @@ export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({ timeZone, dataFr
<table className={styles.exemplarsTable}> <table className={styles.exemplarsTable}>
<tbody> <tbody>
{dataFrame.fields.map((field, i) => { {dataFrame.fields.map((field, i) => {
const value = field.values.get(index); const value = field.values.get(dataFrameFieldIndex.fieldIndex);
const links = field.config.links?.length ? getFieldLinks(field, index) : undefined; const links = field.config.links?.length
? getFieldLinks(field, dataFrameFieldIndex.fieldIndex)
: undefined;
return ( return (
<tr key={i}> <tr key={i}>
<td valign="top">{field.name}</td> <td valign="top">{field.name}</td>
@ -93,7 +121,7 @@ export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({ timeZone, dataFr
attributes.popper, attributes.popper,
dataFrame.fields, dataFrame.fields,
getFieldLinks, getFieldLinks,
index, dataFrameFieldIndex,
onMouseEnter, onMouseEnter,
onMouseLeave, onMouseLeave,
popperStyles.popper, popperStyles.popper,
@ -101,6 +129,10 @@ export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({ timeZone, dataFr
timeZone, timeZone,
]); ]);
const seriesColor = config
.getSeries()
.find((s) => s.props.dataFrameFieldIndex?.frameIndex === dataFrameFieldIndex.frameIndex)?.props.lineColor;
return ( return (
<> <>
<div <div
@ -110,8 +142,14 @@ export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({ timeZone, dataFr
className={styles.markerWrapper} className={styles.markerWrapper}
aria-label={selectors.components.DataSource.Prometheus.exemplarMarker} aria-label={selectors.components.DataSource.Prometheus.exemplarMarker}
> >
<svg viewBox="0 0 599 599" width="8" height="8" className={cx(styles.marble, isOpen && styles.activeMarble)}> <svg
<path d="M 300,575 L 575,300 L 300,25 L 25,300 L 300,575 Z" /> viewBox="0 0 7 7"
width="7"
height="7"
style={{ fill: seriesColor }}
className={cx(styles.marble, isOpen && styles.activeMarble)}
>
{getSymbol()}
</svg> </svg>
</div> </div>
{isOpen && <Portal>{renderMarker()}</Portal>} {isOpen && <Portal>{renderMarker()}</Portal>}
@ -123,21 +161,8 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme) => {
const bg = theme.isDark ? theme.palette.dark2 : theme.palette.white; const bg = theme.isDark ? theme.palette.dark2 : theme.palette.white;
const headerBg = theme.isDark ? theme.palette.dark9 : theme.palette.gray5; const headerBg = theme.isDark ? theme.palette.dark9 : theme.palette.gray5;
const shadowColor = theme.isDark ? theme.palette.black : theme.palette.white; const shadowColor = theme.isDark ? theme.palette.black : theme.palette.white;
const marbleFill = theme.isDark ? theme.palette.gray3 : theme.palette.gray1;
const marbleFillHover = theme.isDark ? theme.palette.blue85 : theme.palette.blue77;
const tableBgOdd = theme.isDark ? theme.palette.dark3 : theme.palette.gray6; const tableBgOdd = theme.isDark ? theme.palette.dark3 : theme.palette.gray6;
const marble = css`
display: block;
fill: ${marbleFill};
transition: transform 0.15s ease-out;
`;
const activeMarble = css`
fill: ${marbleFillHover};
transform: scale(1.3);
filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.5));
`;
return { return {
markerWrapper: css` markerWrapper: css`
padding: 0 4px 4px 4px; padding: 0 4px 4px 4px;
@ -147,7 +172,9 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme) => {
&:hover { &:hover {
> svg { > svg {
${activeMarble} transform: scale(1.3);
opacity: 1;
filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.5));
} }
} }
`, `,
@ -218,7 +245,15 @@ const getExemplarMarkerStyles = (theme: GrafanaTheme) => {
padding: ${theme.spacing.sm}; padding: ${theme.spacing.sm};
font-weight: ${theme.typography.weight.semibold}; font-weight: ${theme.typography.weight.semibold};
`, `,
marble, marble: css`
activeMarble, display: block;
opacity: 0.5;
transition: transform 0.15s ease-out;
`,
activeMarble: css`
transform: scale(1.3);
opacity: 1;
filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.5));
`,
}; };
}; };

View File

@ -1,5 +1,6 @@
import { import {
DataFrame, DataFrame,
DataFrameFieldIndex,
Field, Field,
LinkModel, LinkModel,
TimeZone, TimeZone,
@ -21,7 +22,7 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
const plotCtx = usePlotContext(); const plotCtx = usePlotContext();
const mapExemplarToXYCoords = useCallback( const mapExemplarToXYCoords = useCallback(
(dataFrame: DataFrame, index: number) => { (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
const plotInstance = plotCtx.plot; const plotInstance = plotCtx.plot;
const time = dataFrame.fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME); 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); const value = dataFrame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME);
@ -37,7 +38,7 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
const yMin = plotInstance.scales[yScale].min; const yMin = plotInstance.scales[yScale].min;
const yMax = plotInstance.scales[yScale].max; const yMax = plotInstance.scales[yScale].max;
let y = value.values.get(index); let y = value.values.get(dataFrameFieldIndex.fieldIndex);
// 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 // 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) { if (yMin != null && y < yMin) {
y = yMin; y = yMin;
@ -48,7 +49,7 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
} }
return { return {
x: plotInstance.valToPos(time.values.get(index), 'x'), x: plotInstance.valToPos(time.values.get(dataFrameFieldIndex.fieldIndex), 'x'),
y: plotInstance.valToPos(y, yScale), y: plotInstance.valToPos(y, yScale),
}; };
}, },
@ -56,10 +57,18 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
); );
const renderMarker = useCallback( const renderMarker = useCallback(
(dataFrame: DataFrame, index: number) => { (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
return <ExemplarMarker timeZone={timeZone} getFieldLinks={getFieldLinks} dataFrame={dataFrame} index={index} />; return (
<ExemplarMarker
timeZone={timeZone}
getFieldLinks={getFieldLinks}
dataFrame={dataFrame}
dataFrameFieldIndex={dataFrameFieldIndex}
config={config}
/>
);
}, },
[timeZone, getFieldLinks] [config, timeZone, getFieldLinks]
); );
return ( return (