From bed1bb1a73140e71ff2f5e0d1198f0a9bc7542f1 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Mon, 30 Jan 2023 10:49:20 +0100 Subject: [PATCH] Azure Monitor: Allow to specify a region when listing resources (#62306) * Azure Monitor: Allow to specify a region when listing resources * Add region template variable to e2e tests --- e2e/cloud-plugins-suite/azure-monitor.spec.ts | 12 ++- .../__mocks__/datasource.ts | 2 +- .../azure_monitor_datasource.test.ts | 18 +++- .../azure_monitor/azure_monitor_datasource.ts | 89 +++++++++++-------- .../VariableEditor/VariableEditor.test.tsx | 2 + .../VariableEditor/VariableEditor.tsx | 36 ++++++++ .../datasource.ts | 4 +- .../e2e/selectors.ts | 3 + .../types/query.ts | 3 +- .../types/types.ts | 1 + .../variables.ts | 3 +- 11 files changed, 126 insertions(+), 47 deletions(-) diff --git a/e2e/cloud-plugins-suite/azure-monitor.spec.ts b/e2e/cloud-plugins-suite/azure-monitor.spec.ts index b965fa5880b..178b3a4bed0 100644 --- a/e2e/cloud-plugins-suite/azure-monitor.spec.ts +++ b/e2e/cloud-plugins-suite/azure-monitor.spec.ts @@ -56,7 +56,7 @@ const addAzureMonitorVariable = ( name: string, type: AzureQueryType, isFirst: boolean, - options?: { subscription?: string; resourceGroup?: string; namespace?: string; resource?: string } + options?: { subscription?: string; resourceGroup?: string; namespace?: string; resource?: string; region?: string } ) => { e2e.components.PageToolbar.item('Dashboard settings').click(); e2e.components.Tab.title('Variables').click(); @@ -75,6 +75,9 @@ const addAzureMonitorVariable = ( case AzureQueryType.ResourceGroupsQuery: e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); break; + case AzureQueryType.LocationsQuery: + e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); + break; case AzureQueryType.NamespacesQuery: e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); e2eSelectors.variableEditor.resourceGroup.input().find('input').type(`${options?.resourceGroup}{enter}`); @@ -83,6 +86,7 @@ const addAzureMonitorVariable = ( e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); e2eSelectors.variableEditor.resourceGroup.input().find('input').type(`${options?.resourceGroup}{enter}`); e2eSelectors.variableEditor.namespace.input().find('input').type(`${options?.namespace}{enter}`); + e2eSelectors.variableEditor.region.input().find('input').type(`${options?.region}{enter}`); break; case AzureQueryType.MetricNamesQuery: e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`); @@ -223,10 +227,14 @@ e2e.scenario({ subscription: '$subscription', resourceGroup: '$resourceGroups', }); + addAzureMonitorVariable('region', AzureQueryType.LocationsQuery, false, { + subscription: '$subscription', + }); addAzureMonitorVariable('resource', AzureQueryType.ResourceNamesQuery, false, { subscription: '$subscription', resourceGroup: '$resourceGroups', namespace: '$namespace', + region: '$region', }); e2e.pages.Dashboard.SubMenu.submenuItemLabels('subscription').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('grafanalabs-datasources-dev').click(); @@ -254,6 +262,8 @@ e2e.scenario({ e2eSelectors.queryEditor.resourcePicker.advanced.subscription.input().find('input').type('$subscription'); e2eSelectors.queryEditor.resourcePicker.advanced.resourceGroup.input().find('input').type('$resourceGroups'); e2eSelectors.queryEditor.resourcePicker.advanced.namespace.input().find('input').type('$namespaces'); + // TODO: Enable this input once multiple resources feature flag is removed + // e2eSelectors.queryEditor.resourcePicker.advanced.region.input().find('input').type('$region'); e2eSelectors.queryEditor.resourcePicker.advanced.resource.input().find('input').type('$resource'); e2eSelectors.queryEditor.resourcePicker.apply.button().click(); e2eSelectors.queryEditor.metricsQueryEditor.metricName.input().find('input').type('Transactions{enter}'); 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 e12e778136d..da450501276 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 @@ -32,7 +32,7 @@ export default function createMockDatasource(overrides?: DeepPartial }), getLocations: jest .fn() - .mockResolvedValueOnce( + .mockResolvedValue( new Map([['northeurope', { displayName: 'North Europe', name: 'northeurope', supportsLogs: false }]]) ), }, 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 8d5cdf15074..080078e6fe7 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 @@ -580,6 +580,7 @@ describe('AzureMonitorDatasource', () => { let subscription = 'mock-subscription-id'; let resourceGroup = 'nodeapp'; let metricNamespace = 'microsoft.insights/components'; + let region = ''; beforeEach(() => { subscription = 'mock-subscription-id'; @@ -605,7 +606,9 @@ describe('AzureMonitorDatasource', () => { ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`; expect(path).toBe( - `${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricNamespace}'` + `${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricNamespace}'${ + region ? ` and location eq '${region}'` : '' + }` ); return Promise.resolve(response); }); @@ -632,11 +635,22 @@ describe('AzureMonitorDatasource', () => { }); }); + it('should return include a region', () => { + region = 'eastus'; + return ctx.ds + .getResourceNames(subscription, resourceGroup, metricNamespace, region) + .then((results: Array<{ text: string; value: string }>) => { + expect(results.length).toEqual(1); + expect(results[0].text).toEqual('nodeapp'); + expect(results[0].value).toEqual('nodeapp'); + }); + }); + it('should return multiple resources from a template variable', () => { const tsrv = new TemplateSrv(); tsrv.replace = jest .fn() - .mockImplementation((value: string) => (value === `$${multiVariable.id}` ? 'foo,bar' : value)); + .mockImplementation((value: string) => (value === `$${multiVariable.id}` ? 'foo,bar' : value ?? '')); const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv); ds.azureMonitorDatasource.templateSrv = tsrv; ds.azureMonitorDatasource.getResource = jest 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 84d8b3bebf9..1eb89ab56d3 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 @@ -165,47 +165,56 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { - const validMetricNamespace = startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/') - ? 'microsoft.storage/storageaccounts' - : metricNamespace; - let url = `${this.resourcePath}/subscriptions/${subscriptionId}`; - if (resourceGroup) { - url += `/resourceGroups/${resourceGroup}`; - } - url += `/resources?api-version=${this.listByResourceGroupApiVersion}`; - if (validMetricNamespace) { - url += `&$filter=resourceType eq '${validMetricNamespace}'`; - } - if (skipToken) { - url += `&$skiptoken=${skipToken}`; - } - return this.getResource(url).then(async (result: any) => { - let list: Array<{ text: string; value: string }> = []; - if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) { - list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts'); - for (let i = 0; i < list.length; i++) { - list[i].text += '/default'; - list[i].value += '/default'; - } - } else { - list = ResponseParser.parseResourceNames(result, metricNamespace); + const promises = this.replaceTemplateVariables(query).map( + ({ metricNamespace, subscriptionId, resourceGroup, region }) => { + const validMetricNamespace = startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/') + ? 'microsoft.storage/storageaccounts' + : metricNamespace; + let url = `${this.resourcePath}/subscriptions/${subscriptionId}`; + if (resourceGroup) { + url += `/resourceGroups/${resourceGroup}`; } - - if (result.nextLink) { - // If there is a nextLink, we should request more pages - const nextURL = new URL(result.nextLink); - const nextToken = nextURL.searchParams.get('$skiptoken'); - if (!nextToken) { - throw Error('unable to request the next page of resources'); - } - const nextPage = await this.getResourceNames({ metricNamespace, subscriptionId, resourceGroup }, nextToken); - list = list.concat(nextPage); + url += `/resources?api-version=${this.listByResourceGroupApiVersion}`; + const filters: string[] = []; + if (validMetricNamespace) { + filters.push(`resourceType eq '${validMetricNamespace}'`); } + if (region) { + filters.push(`location eq '${region}'`); + } + if (filters.length > 0) { + url += `&$filter=${filters.join(' and ')}`; + } + if (skipToken) { + url += `&$skiptoken=${skipToken}`; + } + return this.getResource(url).then(async (result: any) => { + let list: Array<{ text: string; value: string }> = []; + if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) { + list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts'); + for (let i = 0; i < list.length; i++) { + list[i].text += '/default'; + list[i].value += '/default'; + } + } else { + list = ResponseParser.parseResourceNames(result, metricNamespace); + } - return list; - }); - }); + if (result.nextLink) { + // If there is a nextLink, we should request more pages + const nextURL = new URL(result.nextLink); + const nextToken = nextURL.searchParams.get('$skiptoken'); + if (!nextToken) { + throw Error('unable to request the next page of resources'); + } + const nextPage = await this.getResourceNames({ metricNamespace, subscriptionId, resourceGroup }, nextToken); + list = list.concat(nextPage); + } + + return list; + }); + } + ); return (await Promise.all(promises)).flat(); } @@ -347,7 +356,9 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend( - `${routeNames.azureMonitor}/subscriptions/${subscription}/locations?api-version=${this.locationsApiVersion}` + `${routeNames.azureMonitor}/subscriptions/${this.templateSrv.replace(subscription)}/locations?api-version=${ + this.locationsApiVersion + }` ) ); for (const location of subLocations) { 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 index 4aab7246106..a0f8bbcbfe3 100644 --- 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 @@ -258,10 +258,12 @@ describe('VariableEditor:', () => { await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); await selectAndRerender('select query type', 'Resource Names', onChange, rerender); await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); + await selectAndRerender('select region', 'North Europe', onChange, rerender); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ queryType: AzureQueryType.ResourceNamesQuery, subscription: 'sub', + region: 'northeurope', refId: 'A', }) ); 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 index 44c77300584..9ada46d6676 100644 --- 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 @@ -51,6 +51,7 @@ const VariableEditor = (props: Props) => { const [requireSubscription, setRequireSubscription] = useState(false); const [hasResourceGroup, setHasResourceGroup] = useState(false); const [hasNamespace, setHasNamespace] = useState(false); + const [hasRegion, setHasRegion] = useState(false); const [requireResourceGroup, setRequireResourceGroup] = useState(false); const [requireNamespace, setRequireNamespace] = useState(false); const [requireResource, setRequireResource] = useState(false); @@ -58,6 +59,7 @@ const VariableEditor = (props: Props) => { const [resourceGroups, setResourceGroups] = useState([]); const [namespaces, setNamespaces] = useState([]); const [resources, setResources] = useState([]); + const [regions, setRegions] = useState([]); const [errorMessage, setError] = useLastError(); const queryType = typeof query === 'string' ? '' : query.queryType; @@ -87,6 +89,7 @@ const VariableEditor = (props: Props) => { setRequireSubscription(true); setHasResourceGroup(true); setHasNamespace(true); + setHasRegion(true); break; case AzureQueryType.MetricNamesQuery: setRequireSubscription(true); @@ -137,6 +140,16 @@ const VariableEditor = (props: Props) => { } }, [datasource, subscription, resourceGroup]); + useEffect(() => { + if (subscription) { + datasource.azureMonitorDatasource.getLocations([subscription]).then((rgs) => { + const regions: SelectableValue[] = []; + rgs.forEach((r) => regions.push({ label: r.displayName, value: r.name })); + setRegions(regions); + }); + } + }, [datasource, subscription, resourceGroup]); + const namespace = (typeof query === 'object' && query.namespace) || ''; useEffect(() => { if (subscription) { @@ -193,6 +206,13 @@ const VariableEditor = (props: Props) => { }); }; + const onChangeRegion = (selectableValue: SelectableValue) => { + onChange({ + ...query, + region: selectableValue.value, + }); + }; + const onChangeResource = (selectableValue: SelectableValue) => { onChange({ ...query, @@ -298,6 +318,22 @@ const VariableEditor = (props: Props) => { /> )} + {hasRegion && ( + +