{
)}
- {query.queryType === 'serviceMap' && }
+ {query.queryType === 'serviceMap' && (
+
+ )}
>
);
}
}
-function ServiceGraphSection({ graphDatasourceUid }: { graphDatasourceUid?: string }) {
- const dsState = useAsync(() => getDS(graphDatasourceUid), [graphDatasourceUid]);
- if (dsState.loading) {
- return null;
- }
-
- const ds = dsState.value as LokiDatasource;
-
- if (!graphDatasourceUid) {
- return Please set up a service graph datasource in the datasource settings.
;
- }
-
- if (graphDatasourceUid && !ds) {
- return (
-
- Service graph datasource is configured but the data source no longer exists. Please configure existing data
- source to use the service graph functionality.
-
- );
- }
-
- return null;
-}
-
interface SearchSectionProps {
linkedDatasourceUid?: string;
onChange: (value: LokiQuery) => void;
@@ -264,18 +244,4 @@ function SearchSection({ linkedDatasourceUid, onChange, onRunQuery, query }: Sea
return null;
}
-async function getDS(uid?: string): Promise {
- if (!uid) {
- return undefined;
- }
-
- const dsSrv = getDataSourceSrv();
- try {
- return await dsSrv.get(uid);
- } catch (error) {
- console.error('Failed to load data source', error);
- return undefined;
- }
-}
-
export const TempoQueryField = withTheme2(TempoQueryFieldComponent);
diff --git a/public/app/plugins/datasource/tempo/QueryEditor/ServiceGraphSection.tsx b/public/app/plugins/datasource/tempo/QueryEditor/ServiceGraphSection.tsx
new file mode 100644
index 00000000000..6b9884e15ab
--- /dev/null
+++ b/public/app/plugins/datasource/tempo/QueryEditor/ServiceGraphSection.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import useAsync from 'react-use/lib/useAsync';
+import { getDS } from './utils';
+import { InlineField, InlineFieldRow } from '@grafana/ui';
+import { AdHocVariableFilter } from '../../../../features/variables/types';
+import { TempoQuery } from '../datasource';
+import { AdHocFilter } from '../../../../features/variables/adhoc/picker/AdHocFilter';
+import { PrometheusDatasource } from '../../prometheus/datasource';
+
+export function ServiceGraphSection({
+ graphDatasourceUid,
+ query,
+ onChange,
+}: {
+ graphDatasourceUid?: string;
+ query: TempoQuery;
+ onChange: (value: TempoQuery) => void;
+}) {
+ const dsState = useAsync(() => getDS(graphDatasourceUid), [graphDatasourceUid]);
+ if (dsState.loading) {
+ return null;
+ }
+
+ const ds = dsState.value as PrometheusDatasource;
+
+ if (!graphDatasourceUid) {
+ return Please set up a service graph datasource in the datasource settings.
;
+ }
+
+ if (graphDatasourceUid && !ds) {
+ return (
+
+ Service graph datasource is configured but the data source no longer exists. Please configure existing data
+ source to use the service graph functionality.
+
+ );
+ }
+ const filters = queryToFilter(query.serviceMapQuery || '');
+
+ return (
+
+
+
+ {
+ onChange({
+ ...query,
+ serviceMapQuery: filtersToQuery([...filters, filter]),
+ });
+ }}
+ removeFilter={(index: number) => {
+ const newFilters = [...filters];
+ newFilters.splice(index, 1);
+ onChange({ ...query, serviceMapQuery: filtersToQuery(newFilters) });
+ }}
+ changeFilter={(index: number, filter: AdHocVariableFilter) => {
+ const newFilters = [...filters];
+ newFilters.splice(index, 1, filter);
+ onChange({ ...query, serviceMapQuery: filtersToQuery(newFilters) });
+ }}
+ />
+
+
+
+ );
+}
+
+function queryToFilter(query: string): AdHocVariableFilter[] {
+ let match;
+ let filters: AdHocVariableFilter[] = [];
+ const re = /([\w_]+)(=|!=|<|>|=~|!~)"(.*?)"/g;
+ while ((match = re.exec(query)) !== null) {
+ filters.push({
+ key: match[1],
+ operator: match[2],
+ value: match[3],
+ condition: '',
+ });
+ }
+ return filters;
+}
+
+function filtersToQuery(filters: AdHocVariableFilter[]): string {
+ return `{${filters.map((f) => `${f.key}${f.operator}"${f.value}"`).join(',')}}`;
+}
diff --git a/public/app/plugins/datasource/tempo/QueryEditor/utils.ts b/public/app/plugins/datasource/tempo/QueryEditor/utils.ts
new file mode 100644
index 00000000000..13e35446404
--- /dev/null
+++ b/public/app/plugins/datasource/tempo/QueryEditor/utils.ts
@@ -0,0 +1,16 @@
+import { DataSourceApi } from '@grafana/data';
+import { getDataSourceSrv } from '@grafana/runtime';
+
+export async function getDS(uid?: string): Promise {
+ if (!uid) {
+ return undefined;
+ }
+
+ const dsSrv = getDataSourceSrv();
+ try {
+ return await dsSrv.get(uid);
+ } catch (error) {
+ console.error('Failed to load data source', error);
+ return undefined;
+ }
+}
diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts
index 998007b45b1..11cb5b4698f 100644
--- a/public/app/plugins/datasource/tempo/datasource.ts
+++ b/public/app/plugins/datasource/tempo/datasource.ts
@@ -54,6 +54,7 @@ export type TempoQuery = {
minDuration?: string;
maxDuration?: string;
limit?: number;
+ serviceMapQuery?: string;
} & DataQuery;
export const DEFAULT_LIMIT = 20;
@@ -268,6 +269,16 @@ export class TempoDatasource extends DataSourceWithBackend ({ ...tagQuery, ...item }), {});
return { ...tagsQueryObject, ...tempoQuery };
}
+
+ async getServiceGraphLabels() {
+ const ds = await getDatasourceSrv().get(this.serviceMap!.datasourceUid);
+ return ds.getTagKeys!();
+ }
+
+ async getServiceGraphLabelValues(key: string) {
+ const ds = await getDatasourceSrv().get(this.serviceMap!.datasourceUid);
+ return ds.getTagValues!({ key });
+ }
}
function queryServiceMapPrometheus(request: DataQueryRequest, datasourceUid: string) {
@@ -324,7 +335,9 @@ function makePromServiceMapRequest(options: DataQueryRequest): DataQ
targets: serviceMapMetrics.map((metric) => {
return {
refId: metric,
- expr: `delta(${metric}[$__range])`,
+ // options.targets[0] is not correct here, but not sure what should happen if you have multiple queries for
+ // service map at the same time anyway
+ expr: `delta(${metric}${options.targets[0].serviceMapQuery || ''}[$__range])`,
instant: true,
};
}),
diff --git a/public/app/plugins/datasource/tempo/module.ts b/public/app/plugins/datasource/tempo/module.ts
index 7a2f3ff1d78..5d301c3855e 100644
--- a/public/app/plugins/datasource/tempo/module.ts
+++ b/public/app/plugins/datasource/tempo/module.ts
@@ -2,7 +2,7 @@ import { DataSourcePlugin } from '@grafana/data';
import CheatSheet from './CheatSheet';
import { ConfigEditor } from './configuration/ConfigEditor';
import { TempoDatasource } from './datasource';
-import { TempoQueryField } from './QueryField';
+import { TempoQueryField } from './QueryEditor/QueryField';
export const plugin = new DataSourcePlugin(TempoDatasource)
.setConfigEditor(ConfigEditor)
diff --git a/public/app/plugins/panel/nodeGraph/__mocks__/layout.worker.js b/public/app/plugins/panel/nodeGraph/__mocks__/layout.worker.js
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/public/app/plugins/panel/nodeGraph/layout.test.ts b/public/app/plugins/panel/nodeGraph/layout.test.ts
new file mode 100644
index 00000000000..4783832bfad
--- /dev/null
+++ b/public/app/plugins/panel/nodeGraph/layout.test.ts
@@ -0,0 +1,88 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { useLayout } from './layout';
+import { EdgeDatum, NodeDatum } from './types';
+
+let onmessage: jest.MockedFunction;
+let postMessage: jest.MockedFunction;
+let terminate: jest.MockedFunction;
+
+jest.mock('./createLayoutWorker', () => {
+ return {
+ __esModule: true,
+ createWorker: () => {
+ onmessage = jest.fn();
+ postMessage = jest.fn();
+ terminate = jest.fn();
+ return {
+ onmessage: onmessage,
+ postMessage: postMessage,
+ terminate: terminate,
+ };
+ },
+ };
+});
+
+describe('layout', () => {
+ it('doesnt fail without any data', async () => {
+ const nodes: NodeDatum[] = [];
+ const edges: EdgeDatum[] = [];
+
+ const { result } = renderHook(() => {
+ return useLayout(nodes, edges, undefined, 100, 1000);
+ });
+ expect(result.current.nodes).toEqual([]);
+ expect(result.current.edges).toEqual([]);
+ expect(postMessage).toBeUndefined();
+ });
+
+ it('cancels worker', async () => {
+ const { result, rerender } = renderHook(
+ ({ nodes, edges }) => {
+ return useLayout(nodes, edges, undefined, 100, 1000);
+ },
+ {
+ initialProps: {
+ nodes: [makeNode(0, 0), makeNode(1, 1)],
+ edges: [makeEdge(0, 1)],
+ },
+ }
+ );
+ expect(postMessage).toBeCalledTimes(1);
+ // Bit convoluted but we cannot easily access the worker instance as we only export constructor so the default
+ // export is class and we only store latest instance of the methods as jest.fn here as module local variables.
+ // So we capture the terminate function from current worker so that when we call rerender and new worker is created
+ // we can still access and check the method from the old one that we assume should be canceled.
+ const localTerminate = terminate;
+
+ rerender({
+ nodes: [],
+ edges: [],
+ });
+
+ expect(result.current.nodes).toEqual([]);
+ expect(result.current.edges).toEqual([]);
+ expect(localTerminate).toBeCalledTimes(1);
+ });
+});
+
+function makeNode(index: number, incoming: number): NodeDatum {
+ return {
+ id: `n${index}`,
+ title: `n${index}`,
+ subTitle: '',
+ dataFrameRowIndex: 0,
+ incoming,
+ arcSections: [],
+ };
+}
+
+function makeEdge(source: number, target: number): EdgeDatum {
+ return {
+ id: `${source}-${target}`,
+ source: 'n' + source,
+ target: 'n' + target,
+ mainStat: '',
+ secondaryStat: '',
+ dataFrameRowIndex: 0,
+ };
+}
diff --git a/public/app/plugins/panel/nodeGraph/layout.ts b/public/app/plugins/panel/nodeGraph/layout.ts
index 4814de573da..2745e686fcb 100644
--- a/public/app/plugins/panel/nodeGraph/layout.ts
+++ b/public/app/plugins/panel/nodeGraph/layout.ts
@@ -1,10 +1,11 @@
-import { useEffect, useMemo, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
import { EdgeDatum, EdgeDatumLayout, NodeDatum } from './types';
import { Field } from '@grafana/data';
import { useNodeLimit } from './useNodeLimit';
import useMountedState from 'react-use/lib/useMountedState';
import { graphBounds } from './utils';
import { createWorker } from './createLayoutWorker';
+import { useUnmount } from 'react-use';
export interface Config {
linkDistance: number;
@@ -52,6 +53,13 @@ export function useLayout(
const [loading, setLoading] = useState(false);
const isMounted = useMountedState();
+ const layoutWorkerCancelRef = useRef<(() => void) | undefined>();
+
+ useUnmount(() => {
+ if (layoutWorkerCancelRef.current) {
+ layoutWorkerCancelRef.current();
+ }
+ });
// Also we compute both layouts here. Grid layout should not add much time and we can more easily just cache both
// so this should happen only once for a given response data.
@@ -69,6 +77,7 @@ export function useLayout(
if (rawNodes.length === 0) {
setNodesGraph([]);
setEdgesGraph([]);
+ setLoading(false);
return;
}
@@ -76,14 +85,15 @@ export function useLayout(
// This is async but as I wanted to still run the sync grid layout and you cannot return promise from effect so
// having callback seems ok here.
- defaultLayout(rawNodes, rawEdges, ({ nodes, edges }) => {
- // TODO: it would be better to cancel the worker somehow but probably not super important right now.
+ const cancel = defaultLayout(rawNodes, rawEdges, ({ nodes, edges }) => {
if (isMounted()) {
setNodesGraph(nodes);
setEdgesGraph(edges as EdgeDatumLayout[]);
setLoading(false);
}
});
+ layoutWorkerCancelRef.current = cancel;
+ return cancel;
}, [rawNodes, rawEdges, isMounted]);
// Compute grid separately as it is sync and do not need to be inside effect. Also it is dependant on width while
@@ -128,6 +138,7 @@ export function useLayout(
/**
* Wraps the layout code in a worker as it can take long and we don't want to block the main thread.
+ * Returns a cancel function to terminate the worker.
*/
function defaultLayout(
nodes: NodeDatum[],
@@ -154,6 +165,10 @@ function defaultLayout(
edges,
config: defaultConfig,
});
+
+ return () => {
+ worker.terminate();
+ };
}
/**
diff --git a/public/test/mocks/workers.ts b/public/test/mocks/workers.ts
index c21cee96220..20aa32146bc 100644
--- a/public/test/mocks/workers.ts
+++ b/public/test/mocks/workers.ts
@@ -1,15 +1,21 @@
const { layout } = jest.requireActual('../../app/plugins/panel/nodeGraph/layout.worker.js');
class LayoutMockWorker {
+ timeout: number | undefined;
constructor() {}
-
postMessage(data: any) {
const { nodes, edges, config } = data;
- setTimeout(() => {
+ this.timeout = setTimeout(() => {
+ this.timeout = undefined;
layout(nodes, edges, config);
// @ts-ignore
this.onmessage({ data: { nodes, edges } });
- }, 1);
+ }, 1) as any;
+ }
+ terminate() {
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
}
}