From 2538fca53a6cf9cea5f33c7441c676f5fb0fb0f0 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 20 Jul 2022 11:04:01 +0200 Subject: [PATCH] Azure Monitor: Add support for Metric Names variables (#52322) * Azure Monitor: Add support for Metric Names variables * Azure Monitor: Add support for Workspaces variables (#52323) --- .../azure_monitor/url_builder.test.ts | 20 ++++ .../azure_monitor/url_builder.ts | 9 +- .../VariableEditor/VariableEditor.test.tsx | 113 ++++++++++-------- .../VariableEditor/VariableEditor.tsx | 67 +++++++++-- .../datasource.ts | 9 ++ .../types/query.ts | 3 + .../types/types.ts | 4 +- .../variables.test.ts | 49 ++++++++ .../variables.ts | 19 +++ 9 files changed, 230 insertions(+), 63 deletions(-) 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 93f1b68cca3..e59c3a40c3a 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 @@ -321,5 +321,25 @@ describe('AzureMonitorUrlBuilder', () => { ); }); }); + + describe('when metric definition does not contain a metric namespace', () => { + it('should build the getMetricNames url in the longer format', () => { + const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( + '', + '2017-05-01-preview', + { + subscription: 'sub1', + resourceGroup: 'rg', + metricDefinition: 'microsoft.compute/virtualmachines', + resourceName: 'rn1', + }, + templateSrv + ); + expect(url).toBe( + '/subscriptions/sub1/resourceGroups/rg/providers/microsoft.compute/virtualmachines/rn1/' + + 'providers/microsoft.insights/metricdefinitions?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 b2a9b1212bb..25c92753dc5 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 @@ -82,9 +82,10 @@ export default class UrlBuilder { ); } - return ( - `${baseUrl}${resourceUri}/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}` + - `&metricnamespace=${encodeURIComponent(metricNamespace)}` - ); + let url = `${baseUrl}${resourceUri}/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}`; + if (metricNamespace) { + url += `&metricnamespace=${encodeURIComponent(metricNamespace)}`; + } + return url; } } 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 7c4c6cef5a1..6291edb1814 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 @@ -33,6 +33,7 @@ const defaultProps = { 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' }]), + getResourceNames: jest.fn().mockResolvedValue([{ text: 'foobar', value: 'foobar' }]), getVariablesRaw: jest.fn().mockReturnValue([ { label: 'query0', name: 'sub0' }, { label: 'query1', name: 'rg', query: { queryType: AzureQueryType.ResourceGroupsQuery } }, @@ -143,8 +144,24 @@ describe('VariableEditor:', () => { }); describe('predefined queries:', () => { - it('should show the new query types if feature gate is enabled', async () => { + const selectAndRerender = async ( + label: string, + text: string, + onChange: jest.Mock, + rerender: (ui: React.ReactElement) => void + ) => { + openMenu(screen.getByLabelText(label)); + screen.getByText(text).click(); + // Simulate onChange behavior + const newQuery = onChange.mock.calls.at(-1)[0]; + rerender(); + await waitFor(() => expect(screen.getByText(text)).toBeInTheDocument()); + }; + beforeEach(() => { grafanaRuntime.config.featureToggles.azTemplateVars = true; + }); + + it('should show the new query types if feature gate is enabled', async () => { render(); openMenu(screen.getByLabelText('select query type')); await waitFor(() => expect(screen.getByText('Subscriptions')).toBeInTheDocument()); @@ -158,36 +175,21 @@ describe('VariableEditor:', () => { }); it('should run the query if requesting subscriptions', async () => { - grafanaRuntime.config.featureToggles.azTemplateVars = true; const onChange = jest.fn(); const { rerender } = render(); - openMenu(screen.getByLabelText('select query type')); - screen.getByText('Subscriptions').click(); - // Simulate onChange behavior - const newQuery = onChange.mock.calls.at(-1)[0]; - rerender(); - await waitFor(() => expect(screen.getByText('Subscriptions')).toBeInTheDocument()); + await selectAndRerender('select query type', 'Subscriptions', onChange, rerender); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ queryType: AzureQueryType.SubscriptionsQuery, refId: 'A' }) ); }); it('should run the query if requesting resource groups', 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 Groups').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(); + await selectAndRerender('select query type', 'Resource Groups', onChange, rerender); + await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ queryType: AzureQueryType.ResourceGroupsQuery, @@ -199,17 +201,10 @@ describe('VariableEditor:', () => { it('should show template variables as options ', async () => { const onChange = jest.fn(); - grafanaRuntime.config.featureToggles.azTemplateVars = true; 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 Groups').click(); - // Simulate onChange behavior - const newQuery = onChange.mock.calls.at(-1)[0]; - rerender(); - await waitFor(() => expect(screen.getByText('Select subscription')).toBeInTheDocument()); + await selectAndRerender('select query type', 'Resource Groups', onChange, rerender); // Select a subscription openMenu(screen.getByLabelText('select subscription')); await waitFor(() => expect(screen.getByText('Primary Subscription')).toBeInTheDocument()); @@ -223,21 +218,12 @@ describe('VariableEditor:', () => { }); 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(); + await selectAndRerender('select query type', 'Namespaces', onChange, rerender); + await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ queryType: AzureQueryType.NamespacesQuery, @@ -248,21 +234,12 @@ describe('VariableEditor:', () => { }); 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(); + await selectAndRerender('select query type', 'Resource Names', onChange, rerender); + await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ queryType: AzureQueryType.ResourceNamesQuery, @@ -271,5 +248,43 @@ describe('VariableEditor:', () => { }) ); }); + + it('should run the query if requesting metric names', async () => { + const onChange = jest.fn(); + const { rerender } = render(); + // wait for initial load + await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); + await selectAndRerender('select query type', 'Metric Names', onChange, rerender); + await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); + await selectAndRerender('select resource group', 'rg', onChange, rerender); + await selectAndRerender('select namespace', 'foo/bar', onChange, rerender); + await selectAndRerender('select resource', 'foobar', onChange, rerender); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + queryType: AzureQueryType.MetricNamesQuery, + subscription: 'sub', + resourceGroup: 'rg', + namespace: 'foo/bar', + resource: 'foobar', + refId: 'A', + }) + ); + }); + + it('should run the query if requesting workspaces', async () => { + const onChange = jest.fn(); + const { rerender } = render(); + // wait for initial load + await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); + await selectAndRerender('select query type', 'Workspaces', onChange, rerender); + await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + queryType: AzureQueryType.WorkspacesQuery, + 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 c2abb163768..9626f13685f 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 @@ -34,6 +34,8 @@ const VariableEditor = (props: Props) => { 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 }); + AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Metric Names', value: AzureQueryType.MetricNamesQuery }); + AZURE_QUERY_VARIABLE_TYPE_OPTIONS.push({ label: 'Workspaces', value: AzureQueryType.WorkspacesQuery }); } const [variableOptionGroup, setVariableOptionGroup] = useState<{ label: string; options: AzureMonitorOption[] }>({ label: 'Template Variables', @@ -42,9 +44,13 @@ const VariableEditor = (props: Props) => { const [requireSubscription, setRequireSubscription] = useState(false); const [hasResourceGroup, setHasResourceGroup] = useState(false); const [hasNamespace, setHasNamespace] = useState(false); + const [requireResourceGroup, setRequireResourceGroup] = useState(false); + const [requireNamespace, setRequireNamespace] = useState(false); + const [requireResource, setRequireResource] = useState(false); const [subscriptions, setSubscriptions] = useState([]); const [resourceGroups, setResourceGroups] = useState([]); const [namespaces, setNamespaces] = useState([]); + const [resources, setResources] = useState([]); const [errorMessage, setError] = useLastError(); const queryType = typeof query === 'string' ? '' : query.queryType; @@ -58,8 +64,12 @@ const VariableEditor = (props: Props) => { setRequireSubscription(false); setHasResourceGroup(false); setHasNamespace(false); + setRequireResourceGroup(false); + setRequireNamespace(false); + setRequireResource(false); switch (queryType) { case AzureQueryType.ResourceGroupsQuery: + case AzureQueryType.WorkspacesQuery: setRequireSubscription(true); break; case AzureQueryType.NamespacesQuery: @@ -71,6 +81,12 @@ const VariableEditor = (props: Props) => { setHasResourceGroup(true); setHasNamespace(true); break; + case AzureQueryType.MetricNamesQuery: + setRequireSubscription(true); + setRequireResourceGroup(true); + setRequireNamespace(true); + setRequireResource(true); + break; } }, [queryType]); @@ -111,6 +127,15 @@ const VariableEditor = (props: Props) => { } }, [datasource, subscription, resourceGroup]); + const namespace = (typeof query === 'object' && query.namespace) || ''; + useEffect(() => { + if (subscription) { + datasource.getResourceNames(subscription, resourceGroup, namespace).then((rgs) => { + setResources(rgs.map((s) => ({ label: s.text, value: s.value }))); + }); + } + }, [datasource, subscription, resourceGroup, namespace]); + if (typeof query === 'string') { // still migrating the query return null; @@ -148,6 +173,13 @@ const VariableEditor = (props: Props) => { }); }; + const onChangeResource = (selectableValue: SelectableValue) => { + onChange({ + ...query, + resource: selectableValue.value, + }); + }; + const onLogsQueryChange = (queryChange: AzureMonitorQuery) => { onChange(queryChange); }; @@ -198,27 +230,46 @@ const VariableEditor = (props: Props) => { /> )} - {hasResourceGroup && ( - + {(requireResourceGroup || hasResourceGroup) && ( + + + )} + {requireResource && ( + +