diff --git a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx index dc01b7262f4..c59b44d8a86 100644 --- a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx +++ b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx @@ -30,7 +30,17 @@ jest.mock('../language_provider', () => { }); }); -const mockQuery = { +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getTemplateSrv: () => ({ + replace: jest.fn(), + containsTemplate: (val: string): boolean => { + return val.includes('$'); + }, + }), +})); + +let mockQuery = { refId: 'A', queryType: 'nativeSearch', key: 'Q-595a9bbc-2a25-49a7-9249-a52a0a475d83-0', @@ -117,7 +127,38 @@ describe('NativeSearch', () => { expect(option).toBeDefined(); await user.type(select, 'a'); - option = await screen.findByText('No options found'); + option = await screen.findByText('Hit enter to add'); expect(option).toBeDefined(); }); + + it('should add variable to select menu options', async () => { + mockQuery = { + ...mockQuery, + refId: '121314', + serviceName: '$service', + spanName: '$span', + }; + + render( + {}} onRunQuery={() => {}} /> + ); + + const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' }); + expect(asyncServiceSelect).toBeInTheDocument(); + await user.click(asyncServiceSelect); + jest.advanceTimersByTime(3000); + + await user.type(asyncServiceSelect, '$'); + var serviceOption = await screen.findByText('$service'); + expect(serviceOption).toBeDefined(); + + const asyncSpanSelect = screen.getByRole('combobox', { name: 'select-span-name' }); + expect(asyncSpanSelect).toBeInTheDocument(); + await user.click(asyncSpanSelect); + jest.advanceTimersByTime(3000); + + await user.type(asyncSpanSelect, '$'); + var operationOption = await screen.findByText('$span'); + expect(operationOption).toBeDefined(); + }); }); diff --git a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx index 0fdb8e983fa..d73eae5d4f9 100644 --- a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx +++ b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx @@ -3,7 +3,7 @@ import Prism from 'prismjs'; import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { Node } from 'slate'; -import { GrafanaTheme2, isValidGoDuration, SelectableValue } from '@grafana/data'; +import { GrafanaTheme2, isValidGoDuration, SelectableValue, toOption } from '@grafana/data'; import { FetchError, getTemplateSrv, isFetchError, TemplateSrv } from '@grafana/runtime'; import { InlineFieldRow, @@ -90,7 +90,13 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props const fetchOptions = async () => { try { const [services, spans] = await Promise.all([loadOptions('serviceName'), loadOptions('spanName')]); + if (query.serviceName && getTemplateSrv().containsTemplate(query.serviceName)) { + services.push(toOption(query.serviceName)); + } setServiceOptions(services); + if (query.spanName && getTemplateSrv().containsTemplate(query.spanName)) { + spans.push(toOption(query.spanName)); + } setSpanOptions(spans); } catch (error) { // Display message if Tempo is connected but search 404's @@ -102,7 +108,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props } }; fetchOptions(); - }, [languageProvider, loadOptions]); + }, [languageProvider, loadOptions, query.serviceName, query.spanName]); useEffect(() => { const fetchTags = async () => { @@ -161,6 +167,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props isClearable onKeyDown={onKeyDown} aria-label={'select-service-name'} + allowCustomValue={true} /> @@ -184,6 +191,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props isClearable onKeyDown={onKeyDown} aria-label={'select-span-name'} + allowCustomValue={true} /> diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index ec834e35a5b..704b408bea4 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -77,9 +77,11 @@ describe('Tempo data source', () => { const queries = ds.interpolateVariablesInQueries([getQuery()], { interpolationVar: { text: text, value: text }, }); - expect(templateSrv.replace).toBeCalledTimes(5); + expect(templateSrv.replace).toBeCalledTimes(7); expect(queries[0].linkedQuery?.expr).toBe(text); expect(queries[0].query).toBe(text); + expect(queries[0].serviceName).toBe(text); + expect(queries[0].spanName).toBe(text); expect(queries[0].search).toBe(text); expect(queries[0].minDuration).toBe(text); expect(queries[0].maxDuration).toBe(text); @@ -94,9 +96,11 @@ describe('Tempo data source', () => { const resp = ds.applyTemplateVariables(getQuery(), { interpolationVar: { text: text, value: text }, }); - expect(templateSrv.replace).toBeCalledTimes(5); + expect(templateSrv.replace).toBeCalledTimes(7); expect(resp.linkedQuery?.expr).toBe(text); expect(resp.query).toBe(text); + expect(resp.serviceName).toBe(text); + expect(resp.spanName).toBe(text); expect(resp.search).toBe(text); expect(resp.minDuration).toBe(text); expect(resp.maxDuration).toBe(text); diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 93308043580..3fa28b7cd99 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -296,6 +296,8 @@ export class TempoDatasource extends DataSourceWithBackend ({ describe('ZipkinDatasource', () => { describe('query', () => { + const templateSrv: TemplateSrv = { + replace: jest.fn(), + getVariables: jest.fn(), + containsTemplate: jest.fn(), + updateTimeRange: jest.fn(), + }; + it('runs query', async () => { setupBackendSrv(zipkinResponse); - const ds = new ZipkinDatasource(defaultSettings); + const ds = new ZipkinDatasource(defaultSettings, templateSrv); await expect(ds.query({ targets: [{ query: '12345' }] } as any)).toEmitValuesWith((val) => { expect(val[0].data[0].fields).toMatchObject(traceFrameFields); }); }); + it('runs query with traceId that includes special characters', async () => { setupBackendSrv(zipkinResponse); - const ds = new ZipkinDatasource(defaultSettings); + const ds = new ZipkinDatasource(defaultSettings, templateSrv); await expect(ds.query({ targets: [{ query: 'a/b' }] } as any)).toEmitValuesWith((val) => { expect(val[0].data[0].fields).toMatchObject(traceFrameFields); }); diff --git a/public/app/plugins/datasource/zipkin/datasource.ts b/public/app/plugins/datasource/zipkin/datasource.ts index 4c088c4548c..cf9ed51c03f 100644 --- a/public/app/plugins/datasource/zipkin/datasource.ts +++ b/public/app/plugins/datasource/zipkin/datasource.ts @@ -9,8 +9,9 @@ import { DataSourceJsonData, FieldType, MutableDataFrame, + ScopedVars, } from '@grafana/data'; -import { BackendSrvRequest, FetchResponse, getBackendSrv } from '@grafana/runtime'; +import { BackendSrvRequest, FetchResponse, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { SpanBarOptions } from '@jaegertracing/jaeger-ui-components'; import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings'; @@ -29,7 +30,10 @@ export class ZipkinDatasource extends DataSourceApi uploadedJson: string | ArrayBuffer | null = null; nodeGraph?: NodeGraphOptions; spanBar?: SpanBarOptions; - constructor(private instanceSettings: DataSourceInstanceSettings) { + constructor( + private instanceSettings: DataSourceInstanceSettings, + private readonly templateSrv: TemplateSrv = getTemplateSrv() + ) { super(instanceSettings); this.nodeGraph = instanceSettings.jsonData.nodeGraph; } @@ -50,7 +54,8 @@ export class ZipkinDatasource extends DataSourceApi } if (target.query) { - return this.request(`${apiPrefix}/trace/${encodeURIComponent(target.query)}`).pipe( + const query = this.applyVariables(target, options.scopedVars); + return this.request(`${apiPrefix}/trace/${encodeURIComponent(query.query)}`).pipe( map((res) => responseToDataQueryResponse(res, this.nodeGraph?.enabled)) ); } @@ -71,6 +76,29 @@ export class ZipkinDatasource extends DataSourceApi return query.query; } + interpolateVariablesInQueries(queries: ZipkinQuery[], scopedVars: ScopedVars): ZipkinQuery[] { + if (!queries || queries.length === 0) { + return []; + } + + return queries.map((query) => { + return { + ...query, + datasource: this.getRef(), + ...this.applyVariables(query, scopedVars), + }; + }); + } + + applyVariables(query: ZipkinQuery, scopedVars: ScopedVars) { + const expandedQuery = { ...query }; + + return { + ...expandedQuery, + query: this.templateSrv.replace(query.query ?? '', scopedVars), + }; + } + private request( apiUrl: string, data?: any,