mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* 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:
parent
6d6e875a84
commit
f704fa423d
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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.');
|
||||||
|
@ -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]
|
||||||
|
@ -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));
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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 (
|
||||||
|
Loading…
Reference in New Issue
Block a user