From 3da4e1cdcd363b9e47827d2b9f84a10761dc0e12 Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Tue, 2 Nov 2021 15:30:15 -0400 Subject: [PATCH] Azure Monitor: Add Resource Picker to Template Variable Query Editor (#40841) Azure Monitor: Add Resource Picker to Template Variable Query Editor - Should fix bug related broken template variables that relied on a deprecated default workspace. --- .../__mocks__/datasource.ts | 5 +- .../app_insights_datasource.test.ts | 67 --- .../app_insights/app_insights_datasource.ts | 23 +- .../azure_log_analytics_datasource.test.ts | 153 +----- .../azure_log_analytics_datasource.ts | 68 +-- .../azure_monitor_datasource.test.ts | 352 ------------- .../azure_monitor/azure_monitor_datasource.ts | 110 +--- .../LogsQueryEditor/LogsQueryEditor.tsx | 20 +- .../LogsQueryEditor/ResourceField.tsx | 3 +- .../VariableEditor/VariableEditor.test.tsx | 163 ++++++ .../VariableEditor/VariableEditor.tsx | 150 ++++++ .../datasource.ts | 30 +- .../grafanaTemplateVariableFns.ts | 268 ++++++++++ .../types/query.ts | 3 + .../types/templateVariables.ts | 75 +++ .../types/types.ts | 2 + .../variables.test.ts | 478 ++++++++++++++++++ .../variables.ts | 105 ++++ 18 files changed, 1277 insertions(+), 798 deletions(-) create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariableFns.ts create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/types/templateVariables.ts create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/variables.test.ts create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/variables.ts diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts index c9e2349828b..a3b36de8013 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts @@ -5,7 +5,7 @@ type DeepPartial = { [P in keyof T]?: DeepPartial; }; -export default function createMockDatasource() { +export default function createMockDatasource(overrides?: DeepPartial) { // We make this a partial so we get _some_ kind of type safety when making this, rather than // having it be any or casted immediately to Datasource const _mockDatasource: DeepPartial = { @@ -16,6 +16,7 @@ export default function createMockDatasource() { return true; }, getSubscriptions: jest.fn().mockResolvedValueOnce([]), + defaultSubscriptionId: 'subscriptionId', }, getAzureLogAnalyticsWorkspaces: jest.fn().mockResolvedValueOnce([]), @@ -34,12 +35,14 @@ export default function createMockDatasource() { azureLogAnalyticsDatasource: { getKustoSchema: () => Promise.resolve(), + getDeprecatedDefaultWorkSpace: () => 'defaultWorkspaceId', }, resourcePickerData: { getResourcePickerData: () => ({}), getResourcesForResourceGroup: () => ({}), getResourceURIFromWorkspace: () => '', }, + ...overrides, }; const mockDatasource = _mockDatasource as Datasource; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts index 00162450a4c..a1e859073e6 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts @@ -394,73 +394,6 @@ describe('AppInsightsDatasource', () => { }); }); - describe('When performing metricFindQuery', () => { - describe('with a metric names query', () => { - const response = { - metrics: { - 'exceptions/server': {}, - 'requests/count': {}, - }, - }; - - beforeEach(() => { - ctx.ds.getResource = jest.fn().mockImplementation((path) => { - expect(path).toContain('/metrics/metadata'); - return Promise.resolve(response); - }); - }); - - it('should return a list of metric names', () => { - return ctx.ds.metricFindQueryInternal('appInsightsMetricNames()').then((results: any) => { - expect(results.length).toBe(2); - expect(results[0].text).toBe('exceptions/server'); - expect(results[0].value).toBe('exceptions/server'); - expect(results[1].text).toBe('requests/count'); - expect(results[1].value).toBe('requests/count'); - }); - }); - }); - - describe('with metadata group by query', () => { - const response = { - metrics: { - 'exceptions/server': { - supportedAggregations: ['sum'], - supportedGroupBy: { - all: ['client/os', 'client/city', 'client/browser'], - }, - defaultAggregation: 'sum', - }, - 'requests/count': { - supportedAggregations: ['avg', 'sum', 'total'], - supportedGroupBy: { - all: ['client/os', 'client/city', 'client/browser'], - }, - defaultAggregation: 'avg', - }, - }, - }; - - beforeEach(() => { - ctx.ds.getResource = jest.fn().mockImplementation((path) => { - expect(path).toContain('/metrics/metadata'); - return Promise.resolve(response); - }); - }); - - it('should return a list of group bys', () => { - return ctx.ds.metricFindQueryInternal('appInsightsGroupBys(requests/count)').then((results: any) => { - expect(results[0].text).toContain('client/os'); - expect(results[0].value).toContain('client/os'); - expect(results[1].text).toContain('client/city'); - expect(results[1].value).toContain('client/city'); - expect(results[2].text).toContain('client/browser'); - expect(results[2].value).toContain('client/browser'); - }); - }); - }); - }); - describe('When getting Metric Names', () => { const response = { metrics: { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts index dfa016fa19b..c9f7a10a313 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts @@ -1,4 +1,4 @@ -import { DataQueryRequest, DataSourceInstanceSettings, ScopedVars, MetricFindValue } from '@grafana/data'; +import { DataQueryRequest, DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; import { getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime'; import { isString } from 'lodash'; @@ -106,27 +106,6 @@ export default class AppInsightsDatasource extends DataSourceWithBackend | null { - const appInsightsMetricNameQuery = query.match(/^AppInsightsMetricNames\(\)/i); - if (appInsightsMetricNameQuery) { - return this.getMetricNames(); - } - - const appInsightsGroupByQuery = query.match(/^AppInsightsGroupBys\(([^\)]+?)(,\s?([^,]+?))?\)/i); - if (appInsightsGroupByQuery) { - const metricName = appInsightsGroupByQuery[1]; - return this.getGroupBys(getTemplateSrv().replace(metricName)); - } - - return null; - } - testDatasource(): Promise { const path = `${this.resourcePath}/metrics/metadata`; return this.getResource(path) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts index fd575e113a8..a42c57e676b 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -2,7 +2,7 @@ import AzureMonitorDatasource from '../datasource'; import AzureLogAnalyticsDatasource from './azure_log_analytics_datasource'; import FakeSchemaData from './__mocks__/schema'; import { TemplateSrv } from 'app/features/templating/template_srv'; -import { AzureLogsVariable, AzureMonitorQuery, DatasourceValidationResult } from '../types'; +import { AzureMonitorQuery, DatasourceValidationResult } from '../types'; import { toUtc } from '@grafana/data'; const templateSrv = new TemplateSrv(); @@ -111,157 +111,6 @@ describe('AzureLogAnalyticsDatasource', () => { }); }); - describe('When performing metricFindQuery', () => { - let queryResults: AzureLogsVariable[]; - - const workspacesResponse = { - value: [ - { - name: 'workspace1', - id: makeResourceURI('workspace-1'), - properties: { - customerId: 'eeee4fde-1aaa-4d60-9974-eeee562ffaa1', - }, - }, - { - name: 'workspace2', - id: makeResourceURI('workspace-2'), - properties: { - customerId: 'eeee4fde-1aaa-4d60-9974-eeee562ffaa2', - }, - }, - ], - }; - - describe('and is the workspaces() macro', () => { - beforeEach(async () => { - ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => { - expect(path).toContain('xxx'); - return Promise.resolve(workspacesResponse); - }); - - queryResults = await ctx.ds.metricFindQuery('workspaces()'); - }); - - it('should return a list of workspaces', () => { - expect(queryResults).toEqual([ - { text: 'workspace1', value: makeResourceURI('workspace-1') }, - { text: 'workspace2', value: makeResourceURI('workspace-2') }, - ]); - }); - }); - - describe('and is the workspaces() macro with the subscription parameter', () => { - beforeEach(async () => { - ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => { - expect(path).toContain('11112222-eeee-4949-9b2d-9106972f9123'); - return Promise.resolve(workspacesResponse); - }); - - queryResults = await ctx.ds.metricFindQuery('workspaces(11112222-eeee-4949-9b2d-9106972f9123)'); - }); - - it('should return a list of workspaces', () => { - expect(queryResults).toEqual([ - { text: 'workspace1', value: makeResourceURI('workspace-1') }, - { text: 'workspace2', value: makeResourceURI('workspace-2') }, - ]); - }); - }); - - describe('and is the workspaces() macro with the subscription parameter quoted', () => { - beforeEach(async () => { - ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => { - expect(path).toContain('11112222-eeee-4949-9b2d-9106972f9123'); - return Promise.resolve(workspacesResponse); - }); - - queryResults = await ctx.ds.metricFindQuery('workspaces("11112222-eeee-4949-9b2d-9106972f9123")'); - }); - - it('should return a list of workspaces', () => { - expect(queryResults).toEqual([ - { text: 'workspace1', value: makeResourceURI('workspace-1') }, - { text: 'workspace2', value: makeResourceURI('workspace-2') }, - ]); - }); - }); - - describe('and is a custom query', () => { - const tableResponseWithOneColumn = { - tables: [ - { - name: 'PrimaryResult', - columns: [ - { - name: 'Category', - type: 'string', - }, - ], - rows: [['Administrative'], ['Policy']], - }, - ], - }; - - const workspaceResponse = { - value: [ - { - name: 'aworkspace', - id: makeResourceURI('a-workspace'), - properties: { - source: 'Azure', - customerId: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67', - }, - }, - ], - }; - - beforeEach(async () => { - ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => { - if (path.indexOf('OperationalInsights/workspaces?api-version=') > -1) { - return Promise.resolve(workspaceResponse); - } else { - return Promise.resolve(tableResponseWithOneColumn); - } - }); - }); - - it('should return a list of categories in the correct format', async () => { - const results = await ctx.ds.metricFindQuery('workspace("aworkspace").AzureActivity | distinct Category'); - - expect(results.length).toBe(2); - expect(results[0].text).toBe('Administrative'); - expect(results[0].value).toBe('Administrative'); - expect(results[1].text).toBe('Policy'); - expect(results[1].value).toBe('Policy'); - }); - }); - - describe('and contain options', () => { - const queryResponse = { - tables: [], - }; - - it('should substitute macros', async () => { - ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => { - const params = new URLSearchParams(path.split('?')[1]); - const query = params.get('query'); - expect(query).toEqual( - 'Perf| where TimeGenerated >= datetime(2021-01-01T05:01:00.000Z) and TimeGenerated <= datetime(2021-01-01T05:02:00.000Z)' - ); - return Promise.resolve(queryResponse); - }); - ctx.ds.azureLogAnalyticsDatasource.firstWorkspace = 'foo'; - await ctx.ds.metricFindQuery('Perf| where TimeGenerated >= $__timeFrom() and TimeGenerated <= $__timeTo()', { - range: { - from: new Date('2021-01-01 00:01:00'), - to: new Date('2021-01-01 00:02:00'), - }, - }); - }); - }); - }); - describe('When performing annotationQuery', () => { const tableResponse = { tables: [ diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index 97037ea5d3a..5ef8db90319 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -8,13 +8,7 @@ import { AzureQueryType, DatasourceValidationResult, } from '../types'; -import { - DataQueryRequest, - DataQueryResponse, - ScopedVars, - DataSourceInstanceSettings, - MetricFindValue, -} from '@grafana/data'; +import { DataQueryRequest, DataQueryResponse, ScopedVars, DataSourceInstanceSettings } from '@grafana/data'; import { getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime'; import { Observable, from } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; @@ -218,60 +212,12 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< }; } - /** - * This is named differently than DataSourceApi.metricFindQuery - * because it's not exposed to Grafana like the main AzureMonitorDataSource. - * And some of the azure internal data sources return null in this function, which the - * external interface does not support - */ - metricFindQueryInternal(query: string, optionalOptions?: unknown): Promise { - // workspaces() - Get workspaces in the default subscription - const workspacesQuery = query.match(/^workspaces\(\)/i); - if (workspacesQuery) { - if (this.defaultSubscriptionId) { - return this.getWorkspaces(this.defaultSubscriptionId); - } else { - throw new Error( - 'No subscription ID. Specify a default subscription ID in the data source config to use workspaces() without a subscription ID' - ); - } - } - - // workspaces("abc-def-etc") - Get workspaces a specified subscription - const workspacesQueryWithSub = query.match(/^workspaces\(["']?([^\)]+?)["']?\)/i); - if (workspacesQueryWithSub) { - return this.getWorkspaces((workspacesQueryWithSub[1] || '').trim()); - } - - // Execute the query as KQL to the default or first workspace - return this.getFirstWorkspace().then((resourceURI) => { - if (!resourceURI) { - return []; - } - - const queries = this.buildQuery(query, optionalOptions, resourceURI); - const promises = this.doQueries(queries); - - return Promise.all(promises) - .then((results) => { - return new ResponseParser(results).parseToVariables(); - }) - .catch((err) => { - if ( - err.error && - err.error.data && - err.error.data.error && - err.error.data.error.innererror && - err.error.data.error.innererror.innererror - ) { - throw { message: err.error.data.error.innererror.innererror.message }; - } else if (err.error && err.error.data && err.error.data.error) { - throw { message: err.error.data.error.message }; - } - - throw err; - }); - }) as Promise; + /* + In 7.5.x it used to be possible to set a default workspace id in the config on the auth page. + This has been deprecated, however is still used by a few legacy template queries. + */ + getDeprecatedDefaultWorkSpace() { + return this.instanceSettings.jsonData.logAnalyticsDefaultWorkspace; } private buildQuery(query: string, options: any, workspace: string): AdhocQuery[] { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index 64c6421fbe2..063be08146e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -73,358 +73,6 @@ describe('AzureMonitorDatasource', () => { }); }); }); - - describe('When performing metricFindQuery', () => { - describe('with a subscriptions query', () => { - const response = { - value: [ - { displayName: 'Primary', subscriptionId: 'sub1' }, - { displayName: 'Secondary', subscriptionId: 'sub2' }, - ], - }; - - beforeEach(() => { - ctx.instanceSettings.jsonData.azureAuthType = 'msi'; - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response); - }); - - it('should return a list of subscriptions', async () => { - const results = await ctx.ds.metricFindQuery('subscriptions()'); - expect(results.length).toBe(2); - expect(results[0].text).toBe('Primary'); - expect(results[0].value).toBe('sub1'); - expect(results[1].text).toBe('Secondary'); - expect(results[1].value).toBe('sub2'); - }); - }); - - describe('with a resource groups query', () => { - const response = { - value: [{ name: 'grp1' }, { name: 'grp2' }], - }; - - beforeEach(() => { - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response); - }); - - it('should return a list of resource groups', async () => { - const results = await ctx.ds.metricFindQuery('ResourceGroups()'); - expect(results.length).toBe(2); - expect(results[0].text).toBe('grp1'); - expect(results[0].value).toBe('grp1'); - expect(results[1].text).toBe('grp2'); - expect(results[1].value).toBe('grp2'); - }); - }); - - describe('with a resource groups query that specifies a subscription id', () => { - const response = { - value: [{ name: 'grp1' }, { name: 'grp2' }], - }; - - beforeEach(() => { - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { - expect(path).toContain('11112222-eeee-4949-9b2d-9106972f9123'); - return Promise.resolve(response); - }); - }); - - it('should return a list of resource groups', async () => { - const results = await ctx.ds.metricFindQuery('ResourceGroups(11112222-eeee-4949-9b2d-9106972f9123)'); - expect(results.length).toBe(2); - expect(results[0].text).toBe('grp1'); - expect(results[0].value).toBe('grp1'); - expect(results[1].text).toBe('grp2'); - expect(results[1].value).toBe('grp2'); - }); - }); - - describe('with namespaces query', () => { - const response = { - value: [ - { - name: 'test', - type: 'Microsoft.Network/networkInterfaces', - }, - ], - }; - - beforeEach(() => { - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { - const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; - expect(path).toBe(basePath + '/nodesapp/resources?api-version=2018-01-01'); - return Promise.resolve(response); - }); - }); - - it('should return a list of namespaces', async () => { - const results = await ctx.ds.metricFindQuery('Namespaces(nodesapp)'); - expect(results.length).toEqual(1); - expect(results[0].text).toEqual('Network interface'); - expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces'); - }); - }); - - describe('with namespaces query that specifies a subscription id', () => { - const response = { - value: [ - { - name: 'test', - type: 'Microsoft.Network/networkInterfaces', - }, - ], - }; - - beforeEach(() => { - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { - const basePath = 'azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups'; - expect(path).toBe(basePath + '/nodesapp/resources?api-version=2018-01-01'); - return Promise.resolve(response); - }); - }); - - it('should return a list of namespaces', async () => { - const results = await ctx.ds.metricFindQuery('namespaces(11112222-eeee-4949-9b2d-9106972f9123, nodesapp)'); - expect(results.length).toEqual(1); - expect(results[0].text).toEqual('Network interface'); - expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces'); - }); - }); - - describe('with resource names query', () => { - const response = { - value: [ - { - name: 'Failure Anomalies - nodeapp', - type: 'microsoft.insights/alertrules', - }, - { - name: 'nodeapp', - type: 'microsoft.insights/components', - }, - ], - }; - - beforeEach(() => { - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { - const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; - expect(path).toBe(basePath + '/nodeapp/resources?api-version=2018-01-01'); - return Promise.resolve(response); - }); - }); - - it('should return a list of resource names', async () => { - const results = await ctx.ds.metricFindQuery('resourceNames(nodeapp, microsoft.insights/components )'); - expect(results.length).toEqual(1); - expect(results[0].text).toEqual('nodeapp'); - expect(results[0].value).toEqual('nodeapp'); - }); - }); - - describe('with resource names query and that specifies a subscription id', () => { - const response = { - value: [ - { - name: 'Failure Anomalies - nodeapp', - type: 'microsoft.insights/alertrules', - }, - { - name: 'nodeapp', - type: 'microsoft.insights/components', - }, - ], - }; - - beforeEach(() => { - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { - const basePath = 'azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups'; - expect(path).toBe(basePath + '/nodeapp/resources?api-version=2018-01-01'); - return Promise.resolve(response); - }); - }); - - it('should return a list of resource names', () => { - return ctx.ds - .metricFindQuery( - 'resourceNames(11112222-eeee-4949-9b2d-9106972f9123, nodeapp, microsoft.insights/components )' - ) - .then((results: any) => { - expect(results.length).toEqual(1); - expect(results[0].text).toEqual('nodeapp'); - expect(results[0].value).toEqual('nodeapp'); - }); - }); - }); - - describe('with metric names query', () => { - const response = { - value: [ - { - name: { - value: 'Percentage CPU', - localizedValue: 'Percentage CPU', - }, - }, - { - name: { - value: 'UsedCapacity', - localizedValue: 'Used capacity', - }, - }, - ], - }; - - beforeEach(() => { - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { - const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; - expect(path).toBe( - basePath + - '/nodeapp/providers/microsoft.insights/components/rn/providers/microsoft.insights/' + - 'metricdefinitions?api-version=2018-01-01&metricnamespace=default' - ); - return Promise.resolve(response); - }); - }); - - it('should return a list of metric names', async () => { - const results = await ctx.ds.metricFindQuery( - 'Metricnames(nodeapp, microsoft.insights/components, rn, default)' - ); - expect(results.length).toEqual(2); - expect(results[0].text).toEqual('Percentage CPU'); - expect(results[0].value).toEqual('Percentage CPU'); - - expect(results[1].text).toEqual('Used capacity'); - expect(results[1].value).toEqual('UsedCapacity'); - }); - }); - - describe('with metric names query and specifies a subscription id', () => { - const response = { - value: [ - { - name: { - value: 'Percentage CPU', - localizedValue: 'Percentage CPU', - }, - }, - { - name: { - value: 'UsedCapacity', - localizedValue: 'Used capacity', - }, - }, - ], - }; - - beforeEach(() => { - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { - const basePath = 'azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups'; - expect(path).toBe( - basePath + - '/nodeapp/providers/microsoft.insights/components/rn/providers/microsoft.insights/' + - 'metricdefinitions?api-version=2018-01-01&metricnamespace=default' - ); - return Promise.resolve(response); - }); - }); - - it('should return a list of metric names', async () => { - const results = await ctx.ds.metricFindQuery( - 'Metricnames(11112222-eeee-4949-9b2d-9106972f9123, nodeapp, microsoft.insights/components, rn, default)' - ); - expect(results.length).toEqual(2); - expect(results[0].text).toEqual('Percentage CPU'); - expect(results[0].value).toEqual('Percentage CPU'); - - expect(results[1].text).toEqual('Used capacity'); - expect(results[1].value).toEqual('UsedCapacity'); - }); - }); - - describe('with metric namespace query', () => { - const response = { - value: [ - { - name: 'Microsoft.Compute-virtualMachines', - properties: { - metricNamespaceName: 'Microsoft.Compute/virtualMachines', - }, - }, - { - name: 'Telegraf-mem', - properties: { - metricNamespaceName: 'Telegraf/mem', - }, - }, - ], - }; - - beforeEach(() => { - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { - const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; - expect(path).toBe( - basePath + - '/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview' - ); - return Promise.resolve(response); - }); - }); - - it('should return a list of metric names', async () => { - const results = await ctx.ds.metricFindQuery('Metricnamespace(nodeapp, Microsoft.Compute/virtualMachines, rn)'); - expect(results.length).toEqual(2); - expect(results[0].text).toEqual('Microsoft.Compute-virtualMachines'); - expect(results[0].value).toEqual('Microsoft.Compute/virtualMachines'); - - expect(results[1].text).toEqual('Telegraf-mem'); - expect(results[1].value).toEqual('Telegraf/mem'); - }); - }); - - describe('with metric namespace query and specifies a subscription id', () => { - const response = { - value: [ - { - name: 'Microsoft.Compute-virtualMachines', - properties: { - metricNamespaceName: 'Microsoft.Compute/virtualMachines', - }, - }, - { - name: 'Telegraf-mem', - properties: { - metricNamespaceName: 'Telegraf/mem', - }, - }, - ], - }; - - beforeEach(() => { - ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { - const basePath = 'azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups'; - expect(path).toBe( - basePath + - '/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview' - ); - return Promise.resolve(response); - }); - }); - - it('should return a list of metric namespaces', async () => { - const results = await ctx.ds.metricFindQuery( - 'Metricnamespace(11112222-eeee-4949-9b2d-9106972f9123, nodeapp, Microsoft.Compute/virtualMachines, rn)' - ); - expect(results.length).toEqual(2); - expect(results[0].text).toEqual('Microsoft.Compute-virtualMachines'); - expect(results[0].value).toEqual('Microsoft.Compute/virtualMachines'); - - expect(results[1].text).toEqual('Telegraf-mem'); - expect(results[1].value).toEqual('Telegraf/mem'); - }); - }); - }); - describe('When performing getSubscriptions', () => { const response = { value: [ diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index 5ee36e9fab3..f06919c4840 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -11,7 +11,7 @@ import { AzureQueryType, DatasourceValidationResult, } from '../types'; -import { DataSourceInstanceSettings, ScopedVars, MetricFindValue } from '@grafana/data'; +import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; @@ -122,114 +122,6 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend | null { - const subscriptionsQuery = query.match(/^Subscriptions\(\)/i); - if (subscriptionsQuery) { - return this.getSubscriptions(); - } - - const resourceGroupsQuery = query.match(/^ResourceGroups\(\)/i); - if (resourceGroupsQuery && this.defaultSubscriptionId) { - return this.getResourceGroups(this.defaultSubscriptionId); - } - - const resourceGroupsQueryWithSub = query.match(/^ResourceGroups\(([^\)]+?)(,\s?([^,]+?))?\)/i); - if (resourceGroupsQueryWithSub) { - return this.getResourceGroups(this.toVariable(resourceGroupsQueryWithSub[1])); - } - - const metricDefinitionsQuery = query.match(/^Namespaces\(([^\)]+?)(,\s?([^,]+?))?\)/i); - if (metricDefinitionsQuery && this.defaultSubscriptionId) { - if (!metricDefinitionsQuery[3]) { - return this.getMetricDefinitions(this.defaultSubscriptionId, this.toVariable(metricDefinitionsQuery[1])); - } - } - - const metricDefinitionsQueryWithSub = query.match(/^Namespaces\(([^,]+?),\s?([^,]+?)\)/i); - if (metricDefinitionsQueryWithSub) { - return this.getMetricDefinitions( - this.toVariable(metricDefinitionsQueryWithSub[1]), - this.toVariable(metricDefinitionsQueryWithSub[2]) - ); - } - - const resourceNamesQuery = query.match(/^ResourceNames\(([^,]+?),\s?([^,]+?)\)/i); - if (resourceNamesQuery && this.defaultSubscriptionId) { - const resourceGroup = this.toVariable(resourceNamesQuery[1]); - const metricDefinition = this.toVariable(resourceNamesQuery[2]); - return this.getResourceNames(this.defaultSubscriptionId, resourceGroup, metricDefinition); - } - - const resourceNamesQueryWithSub = query.match(/^ResourceNames\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/i); - if (resourceNamesQueryWithSub) { - const subscription = this.toVariable(resourceNamesQueryWithSub[1]); - const resourceGroup = this.toVariable(resourceNamesQueryWithSub[2]); - const metricDefinition = this.toVariable(resourceNamesQueryWithSub[3]); - return this.getResourceNames(subscription, resourceGroup, metricDefinition); - } - - const metricNamespaceQuery = query.match(/^MetricNamespace\(([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/i); - if (metricNamespaceQuery && this.defaultSubscriptionId) { - const resourceGroup = this.toVariable(metricNamespaceQuery[1]); - const metricDefinition = this.toVariable(metricNamespaceQuery[2]); - const resourceName = this.toVariable(metricNamespaceQuery[3]); - return this.getMetricNamespaces(this.defaultSubscriptionId, resourceGroup, metricDefinition, resourceName); - } - - const metricNamespaceQueryWithSub = query.match( - /^metricnamespace\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/i - ); - if (metricNamespaceQueryWithSub) { - const subscription = this.toVariable(metricNamespaceQueryWithSub[1]); - const resourceGroup = this.toVariable(metricNamespaceQueryWithSub[2]); - const metricDefinition = this.toVariable(metricNamespaceQueryWithSub[3]); - const resourceName = this.toVariable(metricNamespaceQueryWithSub[4]); - return this.getMetricNamespaces(subscription, resourceGroup, metricDefinition, resourceName); - } - - const metricNamesQuery = query.match(/^MetricNames\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/i); - if (metricNamesQuery && this.defaultSubscriptionId) { - if (metricNamesQuery[3].indexOf(',') === -1) { - const resourceGroup = this.toVariable(metricNamesQuery[1]); - const metricDefinition = this.toVariable(metricNamesQuery[2]); - const resourceName = this.toVariable(metricNamesQuery[3]); - const metricNamespace = this.toVariable(metricNamesQuery[4]); - return this.getMetricNames( - this.defaultSubscriptionId, - resourceGroup, - metricDefinition, - resourceName, - metricNamespace - ); - } - } - - const metricNamesQueryWithSub = query.match( - /^MetricNames\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?(.+?)\)/i - ); - - if (metricNamesQueryWithSub) { - const subscription = this.toVariable(metricNamesQueryWithSub[1]); - const resourceGroup = this.toVariable(metricNamesQueryWithSub[2]); - const metricDefinition = this.toVariable(metricNamesQueryWithSub[3]); - const resourceName = this.toVariable(metricNamesQueryWithSub[4]); - const metricNamespace = this.toVariable(metricNamesQueryWithSub[5]); - return this.getMetricNames(subscription, resourceGroup, metricDefinition, resourceName, metricNamespace); - } - - return null; - } - - toVariable(metric: string) { - return getTemplateSrv().replace((metric || '').trim()); - } - async getSubscriptions(): Promise> { if (!this.isConfigured()) { return []; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx index 7a57f51ccf1..d6ae263793d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -14,6 +14,7 @@ interface LogsQueryEditorProps { onChange: (newQuery: AzureMonitorQuery) => void; variableOptionGroup: { label: string; options: AzureMonitorOption[] }; setError: (source: string, error: AzureMonitorErrorish | undefined) => void; + hideFormatAs?: boolean; } const LogsQueryEditor: React.FC = ({ @@ -23,6 +24,7 @@ const LogsQueryEditor: React.FC = ({ variableOptionGroup, onChange, setError, + hideFormatAs, }) => { const migrationError = useMigrations(datasource, query, onChange); @@ -48,14 +50,16 @@ const LogsQueryEditor: React.FC = ({ setError={setError} /> - + {!hideFormatAs && ( + + )} {migrationError && {migrationError.message}} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx index a7f00a23b05..16867af23af 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx @@ -27,7 +27,6 @@ function parseResourceDetails(resourceURI: string) { const ResourceField: React.FC = ({ query, datasource, onQueryChange }) => { const styles = useStyles2(getStyles); const { resource } = query.azureLogAnalytics ?? {}; - const [pickerIsOpen, setPickerIsOpen] = useState(false); const handleOpenPicker = useCallback(() => { @@ -61,7 +60,7 @@ const ResourceField: React.FC = ({ query, datasource - diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx new file mode 100644 index 00000000000..b41c65e11a2 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.test.tsx @@ -0,0 +1,163 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import VariableEditor from './VariableEditor'; +import userEvent from '@testing-library/user-event'; +import createMockDatasource from '../../__mocks__/datasource'; +import { AzureMonitorQuery, AzureQueryType } from '../../types'; +import { select } from 'react-select-event'; +import * as ui from '@grafana/ui'; +// eslint-disable-next-line lodash/import-scope +import _ from 'lodash'; + +// Have to mock CodeEditor because it doesnt seem to work in tests??? +jest.mock('@grafana/ui', () => ({ + ...jest.requireActual('@grafana/ui'), + CodeEditor: function CodeEditor({ value, onSave }: { value: string; onSave: (newQuery: string) => void }) { + return onSave(event.target.value)} />; + }, +})); + +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: jest.fn((fn: unknown) => fn), +})); + +describe('VariableEditor:', () => { + it('can select a query type', async () => { + const onChange = jest.fn(); + + const props = { + query: { + refId: 'A', + queryType: AzureQueryType.LogAnalytics, + azureLogAnalytics: { + query: 'test query', + }, + subscription: 'id', + }, + onChange, + datasource: createMockDatasource(), + }; + render(); + await waitFor(() => screen.getByLabelText('select query type')); + expect(screen.getByLabelText('select query type')).toBeInTheDocument(); + screen.getByLabelText('select query type').click(); + await select(screen.getByLabelText('select query type'), 'Grafana Query Function', { + container: document.body, + }); + expect(screen.queryByText('Logs')).not.toBeInTheDocument(); + expect(screen.queryByText('Grafana Query Function')).toBeInTheDocument(); + }); + describe('log queries:', () => { + it('should render', async () => { + const props = { + query: { + refId: 'A', + queryType: AzureQueryType.LogAnalytics, + azureLogAnalytics: { + query: 'test query', + }, + subscription: 'id', + }, + onChange: () => {}, + datasource: createMockDatasource(), + }; + render(); + await waitFor(() => screen.queryByTestId('mockeditor')); + expect(screen.queryByText('Resource')).toBeInTheDocument(); + expect(screen.queryByTestId('mockeditor')).toBeInTheDocument(); + }); + + it('should render with legacy query strings', async () => { + const props = { + query: 'test query', + onChange: () => {}, + datasource: createMockDatasource(), + }; + render(); + await waitFor(() => screen.queryByTestId('mockeditor')); + expect(screen.queryByText('Resource')).toBeInTheDocument(); + expect(screen.queryByTestId('mockeditor')).toBeInTheDocument(); + }); + it('should call on change if the query changes', async () => { + const props = { + query: { + refId: 'A', + queryType: AzureQueryType.LogAnalytics, + azureLogAnalytics: { + query: 'test query', + }, + subscription: 'id', + }, + onChange: jest.fn(), + datasource: createMockDatasource(), + }; + render(); + await waitFor(() => screen.queryByTestId('mockeditor')); + expect(screen.queryByTestId('mockeditor')).toBeInTheDocument(); + await userEvent.type(screen.getByTestId('mockeditor'), '{backspace}'); + expect(props.onChange).toHaveBeenCalledWith({ + azureLogAnalytics: { + query: 'test quer', + }, + queryType: 'Azure Log Analytics', + refId: 'A', + subscription: 'id', + }); + }); + }); + + describe('grafana template variable fn queries:', () => { + it('should render', async () => { + const props = { + query: { + refId: 'A', + queryType: AzureQueryType.GrafanaTemplateVariableFn, + grafanaTemplateVariableFn: { + rawQuery: 'test query', + kind: 'SubscriptionsQuery', + }, + subscription: 'id', + } as AzureMonitorQuery, + onChange: () => {}, + datasource: createMockDatasource(), + }; + render(); + await waitFor(() => screen.queryByText('Grafana template variable function')); + expect(screen.queryByText('Grafana template variable function')).toBeInTheDocument(); + expect(screen.queryByDisplayValue('test query')).toBeInTheDocument(); + }); + + it('should call on change if the query changes', async () => { + const props = { + query: { + refId: 'A', + queryType: AzureQueryType.GrafanaTemplateVariableFn, + grafanaTemplateVariableFn: { + rawQuery: 'Su', + kind: 'UnknownQuery', + }, + subscription: 'subscriptionId', + } as AzureMonitorQuery, + onChange: jest.fn(), + datasource: createMockDatasource(), + debounceTime: 1, + }; + render(); + await waitFor(() => screen.queryByText('Grafana template variable function')); + userEvent.type(screen.getByDisplayValue('Su'), 'bscriptions()'); + expect(screen.getByDisplayValue('Subscriptions()')).toBeInTheDocument(); + screen.getByDisplayValue('Subscriptions()').blur(); + await waitFor(() => screen.queryByText('None')); + expect(props.onChange).toHaveBeenCalledWith({ + refId: 'A', + queryType: AzureQueryType.GrafanaTemplateVariableFn, + grafanaTemplateVariableFn: { + rawQuery: 'Subscriptions()', + kind: 'SubscriptionsQuery', + }, + subscription: 'subscriptionId', + }); + }); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx new file mode 100644 index 00000000000..7ba610a5ee9 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/VariableEditor/VariableEditor.tsx @@ -0,0 +1,150 @@ +import { SelectableValue } from '@grafana/data'; +import { Alert, InlineField, Input, Select } from '@grafana/ui'; +import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; +import { AzureMonitorQuery, AzureQueryType } from '../../types'; +import LogsQueryEditor from '../LogsQueryEditor'; +import DataSource from '../../datasource'; +import useLastError from '../../utils/useLastError'; +import { Space } from '../Space'; +import { migrateStringQueriesToObjectQueries } from '../../grafanaTemplateVariableFns'; +import { debounce } from 'lodash'; + +const AZURE_QUERY_VARIABLE_TYPE_OPTIONS = [ + { label: 'Grafana Query Function', value: AzureQueryType.GrafanaTemplateVariableFn }, + { label: 'Logs', value: AzureQueryType.LogAnalytics }, +]; + +const GrafanaTemplateVariableFnInput = ({ + query, + updateQuery, + datasource, +}: { + query: AzureMonitorQuery; + updateQuery: (val: AzureMonitorQuery) => void; + datasource: DataSource; +}) => { + const [inputVal, setInputVal] = useState(''); + useEffect(() => { + setInputVal(query.grafanaTemplateVariableFn?.rawQuery || ''); + }, [query.grafanaTemplateVariableFn?.rawQuery]); + + const onRunQuery = useCallback( + (newQuery: string) => { + migrateStringQueriesToObjectQueries(newQuery, { datasource }).then((updatedQuery) => { + if (updatedQuery.queryType === AzureQueryType.GrafanaTemplateVariableFn) { + updateQuery(updatedQuery); + } else { + updateQuery({ + ...query, + grafanaTemplateVariableFn: { + kind: 'UnknownQuery', + rawQuery: newQuery, + }, + }); + } + }); + }, + [datasource, query, updateQuery] + ); + const debouncedRunQuery = useMemo(() => debounce(onRunQuery, 500), [onRunQuery]); + + const onChange = (event: ChangeEvent) => { + setInputVal(event.target.value); + debouncedRunQuery(event.target.value); + }; + return ( + + + + ); +}; + +type Props = { + query: AzureMonitorQuery | string; + onChange: (query: AzureMonitorQuery) => void; + datasource: DataSource; +}; + +const VariableEditor = (props: Props) => { + const defaultQuery: AzureMonitorQuery = { + refId: 'A', + queryType: AzureQueryType.GrafanaTemplateVariableFn, + }; + const [query, setQuery] = useState(defaultQuery); + + useEffect(() => { + migrateStringQueriesToObjectQueries(props.query, { datasource: props.datasource }).then((migratedQuery) => { + setQuery(migratedQuery); + }); + }, [props.query, props.datasource]); + + const onQueryTypeChange = (selectableValue: SelectableValue) => { + if (selectableValue.value) { + setQuery({ + ...query, + queryType: selectableValue.value, + }); + } + }; + const onLogsQueryChange = (queryChange: AzureMonitorQuery) => { + setQuery(queryChange); + + // only hit backend if there's something to query (prevents error when selecting the resource before pinging a query) + if (queryChange.azureLogAnalytics?.query) { + props.onChange(queryChange); + } + }; + + const [errorMessage, setError] = useLastError(); + + const variableOptionGroup = { + label: 'Template Variables', + // TODO: figure out a way to filter out the current variable from the variables list + // options: props.datasource.getVariables().map((v) => ({ label: v, value: v })), + options: [], + }; + + return ( + <> + +