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 979e3112d79..3f638ba2ef9 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 @@ -64,7 +64,7 @@ describe('AzureMonitorDatasource', () => { const expected = basePath + '/providers/microsoft.insights/components/resource1' + - '/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview'; + '/providers/microsoft.insights/metricNamespaces?region=global&api-version=2017-12-01-preview'; expect(path).toBe(expected); return Promise.resolve(response); }); @@ -80,7 +80,7 @@ describe('AzureMonitorDatasource', () => { expect(results.length).toEqual(2); expect(results[0].text).toEqual('Azure.ApplicationInsights'); expect(results[0].value).toEqual('Azure.ApplicationInsights'); - expect(results[1].text).toEqual('microsoft.insights-components'); + expect(results[1].text).toEqual('microsoft.insights/components'); expect(results[1].value).toEqual('microsoft.insights/components'); }); }); @@ -405,7 +405,7 @@ describe('AzureMonitorDatasource', () => { ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`; expect(path).toBe( - `${basePath}/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01` + `${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricDefinition}'` ); return Promise.resolve(response); }); @@ -456,7 +456,7 @@ describe('AzureMonitorDatasource', () => { const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`; expect(path).toBe( basePath + - `/${resourceGroup}/resources?$filter=resourceType eq '${validMetricDefinition}'&api-version=2021-04-01` + `/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${validMetricDefinition}'` ); return Promise.resolve(response); }); @@ -467,7 +467,7 @@ describe('AzureMonitorDatasource', () => { expect(results[0].text).toEqual('storagetest/default'); expect(results[0].value).toEqual('storagetest/default'); expect(ctx.ds.azureMonitorDatasource.getResource).toHaveBeenCalledWith( - `azuremonitor/subscriptions/${subscription}/resourceGroups/${resourceGroup}/resources?$filter=resourceType eq '${validMetricDefinition}'&api-version=2021-04-01` + `azuremonitor/subscriptions/${subscription}/resourceGroups/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${validMetricDefinition}'` ); }); }); @@ -497,7 +497,7 @@ describe('AzureMonitorDatasource', () => { const fn = jest.fn(); ctx.ds.azureMonitorDatasource.getResource = fn; const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`; - const expectedPath = `${basePath}/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`; + const expectedPath = `${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricDefinition}'`; // first page fn.mockImplementationOnce((path: string) => { expect(path).toBe(expectedPath); @@ -520,6 +520,35 @@ describe('AzureMonitorDatasource', () => { }); }); }); + + describe('without a resource group or a metric definition', () => { + const response = { + value: [ + { + name: 'Failure Anomalies - nodeapp', + type: 'microsoft.insights/alertrules', + }, + { + name: resourceGroup, + type: metricDefinition, + }, + ], + }; + + beforeEach(() => { + ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { + const basePath = `azuremonitor/subscriptions/${subscription}/resources?api-version=2021-04-01`; + expect(path).toBe(basePath); + return Promise.resolve(response); + }); + }); + + it('should return list of Resource Names', () => { + return ctx.ds.getResourceNames(subscription).then((results: Array<{ text: string; value: string }>) => { + expect(results.length).toEqual(2); + }); + }); + }); }); describe('When performing getMetricNames', () => { 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 f1c06ce1a70..125a83a74b9 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 @@ -204,14 +204,18 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { - return ResponseParser.parseResponseValues(result, 'name', 'properties.metricNamespaceName'); + return ResponseParser.parseResponseValues( + result, + 'properties.metricNamespaceName', + 'properties.metricNamespaceName' + ); }) .then((result) => { if (url.includes('Microsoft.Storage/storageAccounts')) { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts index 640a127591e..121cf8f54fa 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts @@ -33,7 +33,7 @@ export default class ResponseParser { return list; } - static parseResourceNames(result: any, metricDefinition: string): Array<{ text: string; value: string }> { + static parseResourceNames(result: any, metricDefinition?: string): Array<{ text: string; value: string }> { const list: Array<{ text: string; value: string }> = []; if (!result) { @@ -43,7 +43,7 @@ export default class ResponseParser { for (let i = 0; i < result.value.length; i++) { if ( typeof result.value[i].type === 'string' && - result.value[i].type.toLocaleLowerCase() === metricDefinition.toLocaleLowerCase() + (!metricDefinition || result.value[i].type.toLocaleLowerCase() === metricDefinition.toLocaleLowerCase()) ) { list.push({ text: result.value[i].name, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts index a7103ef1046..93f1b68cca3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts @@ -92,7 +92,7 @@ describe('AzureMonitorUrlBuilder', () => { templateSrv ); expect(url).toBe( - '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview' + '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?region=global&api-version=2017-05-01-preview' ); }); }); @@ -130,7 +130,7 @@ describe('AzureMonitorUrlBuilder', () => { ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' + - 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview' + 'providers/microsoft.insights/metricNamespaces?region=global&api-version=2017-05-01-preview' ); }); }); @@ -150,7 +150,7 @@ describe('AzureMonitorUrlBuilder', () => { ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' + - 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview' + 'providers/microsoft.insights/metricNamespaces?region=global&api-version=2017-05-01-preview' ); }); }); @@ -170,7 +170,7 @@ describe('AzureMonitorUrlBuilder', () => { ); expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' + - 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview' + 'providers/microsoft.insights/metricNamespaces?region=global&api-version=2017-05-01-preview' ); }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts index d52df58b96e..b2a9b1212bb 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts @@ -57,7 +57,7 @@ export default class UrlBuilder { ); } - return `${baseUrl}${resourceUri}/providers/microsoft.insights/metricNamespaces?api-version=${apiVersion}`; + return `${baseUrl}${resourceUri}/providers/microsoft.insights/metricNamespaces?region=global&api-version=${apiVersion}`; } static buildAzureMonitorGetMetricNamesUrl( 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 63a649aae0b..7c4c6cef5a1 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 @@ -29,7 +29,15 @@ const defaultProps = { subscription: 'id', }, onChange: jest.fn(), - datasource: createMockDatasource(), + datasource: createMockDatasource({ + getSubscriptions: jest.fn().mockResolvedValue([{ text: 'Primary Subscription', value: 'sub' }]), + getResourceGroups: jest.fn().mockResolvedValue([{ text: 'rg', value: 'rg' }]), + getMetricNamespaces: jest.fn().mockResolvedValue([{ text: 'foo/bar', value: 'foo/bar' }]), + getVariablesRaw: jest.fn().mockReturnValue([ + { label: 'query0', name: 'sub0' }, + { label: 'query1', name: 'rg', query: { queryType: AzureQueryType.ResourceGroupsQuery } }, + ]), + }), }; const originalConfigValue = grafanaRuntime.config.featureToggles.azTemplateVars; @@ -166,11 +174,8 @@ describe('VariableEditor:', () => { it('should run the query if requesting resource groups', async () => { grafanaRuntime.config.featureToggles.azTemplateVars = true; - const ds = createMockDatasource({ - getSubscriptions: jest.fn().mockResolvedValue([{ text: 'Primary Subscription', value: 'sub' }]), - }); const onChange = jest.fn(); - const { rerender } = render(); + const { rerender } = render(); // wait for initial load await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); // Select RGs variable @@ -195,14 +200,7 @@ describe('VariableEditor:', () => { it('should show template variables as options ', async () => { const onChange = jest.fn(); grafanaRuntime.config.featureToggles.azTemplateVars = true; - const ds = createMockDatasource({ - getSubscriptions: jest.fn().mockResolvedValue([{ text: 'Primary Subscription', value: 'sub' }]), - getVariablesRaw: jest.fn().mockReturnValue([ - { label: 'query0', name: 'sub0' }, - { label: 'query1', name: 'rg', query: { queryType: AzureQueryType.ResourceGroupsQuery } }, - ]), - }); - const { rerender } = render(); + const { rerender } = render(); // wait for initial load await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); // Select RGs variable @@ -210,7 +208,7 @@ describe('VariableEditor:', () => { screen.getByText('Resource Groups').click(); // Simulate onChange behavior const newQuery = onChange.mock.calls.at(-1)[0]; - rerender(); + rerender(); await waitFor(() => expect(screen.getByText('Select subscription')).toBeInTheDocument()); // Select a subscription openMenu(screen.getByLabelText('select subscription')); @@ -218,10 +216,60 @@ describe('VariableEditor:', () => { screen.getByText('Template Variables').click(); // Simulate onChange behavior const lastQuery = onChange.mock.calls.at(-1)[0]; - rerender(); + rerender(); await waitFor(() => expect(screen.getByText('query0')).toBeInTheDocument()); // Template variables of the same type than the current one should not appear expect(screen.queryByText('query1')).not.toBeInTheDocument(); }); + + it('should run the query if requesting namespaces', async () => { + grafanaRuntime.config.featureToggles.azTemplateVars = true; + const onChange = jest.fn(); + const { rerender } = render(); + // wait for initial load + await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); + // Select RGs variable + openMenu(screen.getByLabelText('select query type')); + screen.getByText('Namespaces').click(); + // Simulate onChange behavior + const newQuery = onChange.mock.calls.at(-1)[0]; + rerender(); + await waitFor(() => expect(screen.getByText('Select subscription')).toBeInTheDocument()); + // Select a subscription + openMenu(screen.getByLabelText('select subscription')); + screen.getByText('Primary Subscription').click(); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + queryType: AzureQueryType.NamespacesQuery, + subscription: 'sub', + refId: 'A', + }) + ); + }); + + it('should run the query if requesting resource names', async () => { + grafanaRuntime.config.featureToggles.azTemplateVars = true; + const onChange = jest.fn(); + const { rerender } = render(); + // wait for initial load + await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); + // Select RGs variable + openMenu(screen.getByLabelText('select query type')); + screen.getByText('Resource Names').click(); + // Simulate onChange behavior + const newQuery = onChange.mock.calls.at(-1)[0]; + rerender(); + await waitFor(() => expect(screen.getByText('Select subscription')).toBeInTheDocument()); + // Select a subscription + openMenu(screen.getByLabelText('select subscription')); + screen.getByText('Primary Subscription').click(); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + queryType: AzureQueryType.ResourceNamesQuery, + subscription: 'sub', + 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 08cc4aae721..c2abb163768 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 @@ -21,6 +21,8 @@ type Props = { datasource: DataSource; }; +const removeOption: SelectableValue = { label: '-', value: '' }; + const VariableEditor = (props: Props) => { const { query, onChange, datasource } = props; const AZURE_QUERY_VARIABLE_TYPE_OPTIONS = [ @@ -30,13 +32,19 @@ const VariableEditor = (props: Props) => { if (config.featureToggles.azTemplateVars) { AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Subscriptions', value: AzureQueryType.SubscriptionsQuery }); AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Resource Groups', value: AzureQueryType.ResourceGroupsQuery }); + AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Namespaces', value: AzureQueryType.NamespacesQuery }); + AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Resource Names', value: AzureQueryType.ResourceNamesQuery }); } const [variableOptionGroup, setVariableOptionGroup] = useState<{ label: string; options: AzureMonitorOption[] }>({ label: 'Template Variables', options: [], }); const [requireSubscription, setRequireSubscription] = useState(false); + const [hasResourceGroup, setHasResourceGroup] = useState(false); + const [hasNamespace, setHasNamespace] = useState(false); const [subscriptions, setSubscriptions] = useState([]); + const [resourceGroups, setResourceGroups] = useState([]); + const [namespaces, setNamespaces] = useState([]); const [errorMessage, setError] = useLastError(); const queryType = typeof query === 'string' ? '' : query.queryType; @@ -47,12 +55,22 @@ const VariableEditor = (props: Props) => { }, [query, datasource, onChange]); useEffect(() => { + setRequireSubscription(false); + setHasResourceGroup(false); + setHasNamespace(false); switch (queryType) { case AzureQueryType.ResourceGroupsQuery: setRequireSubscription(true); break; - default: - setRequireSubscription(false); + case AzureQueryType.NamespacesQuery: + setRequireSubscription(true); + setHasResourceGroup(true); + break; + case AzureQueryType.ResourceNamesQuery: + setRequireSubscription(true); + setHasResourceGroup(true); + setHasNamespace(true); + break; } }, [queryType]); @@ -75,6 +93,24 @@ const VariableEditor = (props: Props) => { }); }); + const subscription = typeof query === 'object' && query.subscription; + useEffect(() => { + if (subscription) { + datasource.getResourceGroups(subscription).then((rgs) => { + setResourceGroups(rgs.map((s) => ({ label: s.text, value: s.value }))); + }); + } + }, [datasource, subscription]); + + const resourceGroup = (typeof query === 'object' && query.resourceGroup) || ''; + useEffect(() => { + if (subscription) { + datasource.getMetricNamespaces(subscription, resourceGroup).then((rgs) => { + setNamespaces(rgs.map((s) => ({ label: s.text, value: s.value }))); + }); + } + }, [datasource, subscription, resourceGroup]); + if (typeof query === 'string') { // still migrating the query return null; @@ -98,6 +134,20 @@ const VariableEditor = (props: Props) => { } }; + const onChangeResourceGroup = (selectableValue: SelectableValue) => { + onChange({ + ...query, + resourceGroup: selectableValue.value, + }); + }; + + const onChangeNamespace = (selectableValue: SelectableValue) => { + onChange({ + ...query, + namespace: selectableValue.value, + }); + }; + const onLogsQueryChange = (queryChange: AzureMonitorQuery) => { onChange(queryChange); }; @@ -113,7 +163,7 @@ const VariableEditor = (props: Props) => { value={queryType} /> - {typeof query === 'object' && query.queryType === AzureQueryType.LogAnalytics && ( + {query.queryType === AzureQueryType.LogAnalytics && ( <> { )} )} - {typeof query === 'object' && query.queryType === AzureQueryType.GrafanaTemplateVariableFn && ( + {query.queryType === AzureQueryType.GrafanaTemplateVariableFn && ( )} - {typeof query === 'object' && requireSubscription && ( + {requireSubscription && ( + + )} + {hasNamespace && ( + +