diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx index f4c729c52b8..0a0f60ebaa2 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx @@ -1,10 +1,20 @@ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; +import { config } from '@grafana/runtime'; + import createMockDatasource from '../../__mocks__/datasource'; +import { createMockInstanceSetttings } from '../../__mocks__/instanceSettings'; import createMockPanelData from '../../__mocks__/panelData'; import createMockQuery from '../../__mocks__/query'; +import { + createMockResourceGroupsBySubscription, + createMockSubscriptions, + mockResourcesByResourceGroup, +} from '../../__mocks__/resourcePickerRows'; +import ResourcePickerData from '../../resourcePicker/resourcePickerData'; import MetricsQueryEditor from './MetricsQueryEditor'; @@ -13,259 +23,238 @@ const variableOptionGroup = { options: [], }; -describe('Azure Monitor QueryEditor', () => { - const mockPanelData = createMockPanelData(); - it('should render', async () => { - const mockDatasource = createMockDatasource(); - render( - {}} - setError={() => {}} - /> - ); - await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument()); - }); +const tests = [ + { + id: 'azure-monitor-metrics-query-editor-with-resource-picker', + }, + { + id: 'azure-monitor-metrics-query-editor-with-experimental-ui', + }, +]; - it('should change the subscription ID when selected', async () => { - const mockDatasource = createMockDatasource(); - const onChange = jest.fn(); - const mockQuery = createMockQuery(); - (mockQuery.azureMonitor ?? {}).metricName = undefined; - mockDatasource.azureMonitorDatasource.getSubscriptions = jest.fn().mockResolvedValueOnce([ - { - value: 'abc-123', - text: 'Primary Subscription', - }, - { - value: 'abc-456', - text: 'Another Subscription', - }, - ]); +export function createMockResourcePickerData() { + const mockDatasource = new ResourcePickerData(createMockInstanceSetttings()); - render( - {}} - /> - ); + mockDatasource.getSubscriptions = jest.fn().mockResolvedValue(createMockSubscriptions()); + mockDatasource.getResourceGroupsBySubscriptionId = jest + .fn() + .mockResolvedValue(createMockResourceGroupsBySubscription()); + mockDatasource.getResourcesForResourceGroup = jest.fn().mockResolvedValue(mockResourcesByResourceGroup()); + mockDatasource.getResourceURIFromWorkspace = jest.fn().mockReturnValue(''); + mockDatasource.getResourceURIDisplayProperties = jest.fn().mockResolvedValue({}); - const subscriptions = await screen.findByLabelText('Subscription'); - await selectOptionInTest(subscriptions, 'Another Subscription'); + return mockDatasource; +} - expect(onChange).toHaveBeenCalledWith({ - ...mockQuery, - subscription: 'abc-456', - azureMonitor: { - ...mockQuery.azureMonitor, - resourceGroup: undefined, - metricDefinition: undefined, - metricNamespace: undefined, - resourceName: undefined, - metricName: undefined, - aggregation: undefined, - timeGrain: '', - dimensionFilters: [], - }, +for (const t of tests) { + describe(`MetricsQueryEditor: ${t.id}`, () => { + const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView; + const mockPanelData = createMockPanelData(); + + beforeEach(() => { + window.HTMLElement.prototype.scrollIntoView = function () {}; + config.featureToggles.azureMonitorExperimentalUI = + t.id === 'azure-monitor-metrics-query-editor-with-experimental-ui'; + }); + afterEach(() => { + window.HTMLElement.prototype.scrollIntoView = originalScrollIntoView; + config.featureToggles.azureMonitorExperimentalUI = false; + }); + + it('should render', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + + render( + {}} + setError={() => {}} + /> + ); + + expect(await screen.findByTestId(t.id)).toBeInTheDocument(); + }); + + it('should change resource when a resource is selected in the ResourcePicker', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery(); + delete query?.azureMonitor?.resourceUri; + const onChange = jest.fn(); + + render( + {}} + /> + ); + + const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' }); + expect(resourcePickerButton).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument(); + resourcePickerButton.click(); + + const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' }); + expect(subscriptionButton).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Expand A Great Resource Group' })).not.toBeInTheDocument(); + subscriptionButton.click(); + + const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' }); + expect(resourceGroupButton).toBeInTheDocument(); + expect(screen.queryByLabelText('web-server')).not.toBeInTheDocument(); + resourceGroupButton.click(); + + const checkbox = await screen.findByLabelText('web-server'); + expect(checkbox).toBeInTheDocument(); + expect(checkbox).not.toBeChecked(); + await userEvent.click(checkbox); + expect(checkbox).toBeChecked(); + await userEvent.click(await screen.findByRole('button', { name: 'Apply' })); + + expect(onChange).toBeCalledTimes(1); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureMonitor: expect.objectContaining({ + resourceUri: + '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/web-server', + }), + }) + ); + }); + + it('should reset metric namespace, metric name, and aggregation fields after selecting a new resource when a valid query has already been set', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery(); + const onChange = jest.fn(); + + render( + {}} + /> + ); + + const resourcePickerButton = await screen.findByRole('button', { name: /grafana/ }); + + expect(screen.getByText('Microsoft.Compute/virtualMachines')).toBeInTheDocument(); + expect(screen.getByText('Metric A')).toBeInTheDocument(); + expect(screen.getByText('Average')).toBeInTheDocument(); + + expect(resourcePickerButton).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument(); + resourcePickerButton.click(); + + const subscriptionButton = await screen.findByRole('button', { name: 'Expand Dev Subscription' }); + expect(subscriptionButton).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Expand Development 3' })).not.toBeInTheDocument(); + subscriptionButton.click(); + + const resourceGroupButton = await screen.findByRole('button', { name: 'Expand Development 3' }); + expect(resourceGroupButton).toBeInTheDocument(); + expect(screen.queryByLabelText('db-server')).not.toBeInTheDocument(); + resourceGroupButton.click(); + + const checkbox = await screen.findByLabelText('db-server'); + expect(checkbox).toBeInTheDocument(); + expect(checkbox).not.toBeChecked(); + await userEvent.click(checkbox); + expect(checkbox).toBeChecked(); + await userEvent.click(await screen.findByRole('button', { name: 'Apply' })); + + expect(onChange).toBeCalledTimes(1); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureMonitor: expect.objectContaining({ + resourceUri: + '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server', + metricNamespace: undefined, + metricName: undefined, + aggregation: undefined, + timeGrain: '', + dimensionFilters: [], + }), + }) + ); + }); + + it('should change the metric name when selected', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const onChange = jest.fn(); + const mockQuery = createMockQuery(); + mockDatasource.azureMonitorDatasource.getMetricNames = jest.fn().mockResolvedValue([ + { + value: 'metric-a', + text: 'Metric A', + }, + { + value: 'metric-b', + text: 'Metric B', + }, + ]); + + render( + {}} + /> + ); + + const metrics = await screen.findByLabelText('Metric'); + expect(metrics).toBeInTheDocument(); + await selectOptionInTest(metrics, 'Metric B'); + + expect(onChange).toHaveBeenLastCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + metricName: 'metric-b', + aggregation: undefined, + timeGrain: '', + }, + }); + }); + + it('should change the aggregation type when selected', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const onChange = jest.fn(); + const mockQuery = createMockQuery(); + + render( + {}} + /> + ); + + const aggregation = await screen.findByLabelText('Aggregation'); + expect(aggregation).toBeInTheDocument(); + await selectOptionInTest(aggregation, 'Maximum'); + + expect(onChange).toHaveBeenLastCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + aggregation: 'Maximum', + }, + }); }); }); - - it('should change the resource group when selected', async () => { - const mockDatasource = createMockDatasource(); - const onChange = jest.fn(); - const mockQuery = createMockQuery(); - mockDatasource.getResourceGroups = jest.fn().mockResolvedValue([ - { text: 'grafanastaging', value: 'grafanastaging' }, - { text: 'Grafana Prod', value: 'grafanaprod' }, - ]); - render( - {}} - /> - ); - await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument()); - - const resourceGroup = await screen.findByLabelText('Resource group'); - await selectOptionInTest(resourceGroup, 'Grafana Prod'); - - expect(onChange).toHaveBeenLastCalledWith({ - ...mockQuery, - azureMonitor: { - ...mockQuery.azureMonitor, - resourceUri: '', - resourceGroup: 'grafanaprod', - metricDefinition: undefined, - metricNamespace: undefined, - resourceName: undefined, - metricName: undefined, - aggregation: undefined, - timeGrain: '', - dimensionFilters: [], - }, - }); - }); - - it('should change the resource type when selected', async () => { - const mockDatasource = createMockDatasource(); - const onChange = jest.fn(); - const mockQuery = createMockQuery(); - mockDatasource.getMetricDefinitions = jest.fn().mockResolvedValue([ - { text: 'Virtual Machine', value: 'azure/vm' }, - { text: 'Database', value: 'azure/db' }, - ]); - render( - {}} - /> - ); - await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument()); - - const resourceGroup = await screen.findByLabelText('Resource type'); - await selectOptionInTest(resourceGroup, 'Virtual Machine'); - - expect(onChange).toHaveBeenLastCalledWith({ - ...mockQuery, - azureMonitor: { - ...mockQuery.azureMonitor, - resourceUri: '', - metricDefinition: 'azure/vm', - resourceName: undefined, - metricNamespace: undefined, - metricName: undefined, - aggregation: undefined, - timeGrain: '', - dimensionFilters: [], - }, - }); - }); - - it('should change the resource name when selected', async () => { - const mockDatasource = createMockDatasource(); - const onChange = jest.fn(); - const mockQuery = createMockQuery(); - mockDatasource.getResourceNames = jest.fn().mockResolvedValue([ - { text: 'ResourceName1', value: 'resource-name-1' }, - { text: 'ResourceName2', value: 'resource-name-2' }, - ]); - render( - {}} - /> - ); - await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument()); - - const resourceGroup = await screen.findByLabelText('Resource name'); - await selectOptionInTest(resourceGroup, 'ResourceName1'); - - expect(onChange).toHaveBeenLastCalledWith({ - ...mockQuery, - azureMonitor: { - ...mockQuery.azureMonitor, - resourceUri: '', - resourceName: 'resource-name-1', - metricNamespace: undefined, - metricName: undefined, - aggregation: undefined, - timeGrain: '', - dimensionFilters: [], - }, - }); - }); - - it('should change the metric name when selected', async () => { - const mockDatasource = createMockDatasource(); - const onChange = jest.fn(); - const mockQuery = createMockQuery(); - mockDatasource.azureMonitorDatasource.getMetricNames = jest.fn().mockResolvedValue([ - { - value: 'metric-a', - text: 'Metric A', - }, - { - value: 'metric-b', - text: 'Metric B', - }, - ]); - render( - {}} - /> - ); - await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument()); - - const metrics = await screen.findByLabelText('Metric'); - await selectOptionInTest(metrics, 'Metric B'); - - expect(onChange).toHaveBeenLastCalledWith({ - ...mockQuery, - azureMonitor: { - ...mockQuery.azureMonitor, - metricName: 'metric-b', - aggregation: undefined, - timeGrain: '', - }, - }); - }); - - it('should change the aggregation type when selected', async () => { - const mockDatasource = createMockDatasource(); - const onChange = jest.fn(); - const mockQuery = createMockQuery(); - render( - {}} - /> - ); - await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument()); - - const aggregation = await screen.findByLabelText('Aggregation'); - await selectOptionInTest(aggregation, 'Maximum'); - - expect(onChange).toHaveBeenLastCalledWith({ - ...mockQuery, - azureMonitor: { - ...mockQuery.azureMonitor, - aggregation: 'Maximum', - }, - }); - }); -}); +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx index 6f082fe4390..c75e59029f7 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -1,37 +1,31 @@ +import { css } from '@emotion/css'; import React from 'react'; import { PanelData } from '@grafana/data/src/types'; -import { InlineFieldRow } from '@grafana/ui'; +import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/experimental'; +import { config } from '@grafana/runtime'; +import { InlineFieldRow, useStyles2 } from '@grafana/ui'; -import Datasource from '../../datasource'; -import { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish } from '../../types'; -import SubscriptionField from '../SubscriptionField'; +import type Datasource from '../../datasource'; +import type { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish } from '../../types'; +import ResourceField from '../ResourceField'; +import { ResourceRowType } from '../ResourcePicker/types'; import AggregationField from './AggregationField'; import DimensionFields from './DimensionFields'; import LegendFormatField from './LegendFormatField'; import MetricNameField from './MetricNameField'; import MetricNamespaceField from './MetricNamespaceField'; -import ResourceGroupsField from './ResourceGroupsField'; -import ResourceNameField from './ResourceNameField'; -import ResourceTypeField from './ResourceTypeField'; +import NewDimensionFields from './NewDimensionFields'; import TimeGrainField from './TimeGrainField'; import TopField from './TopField'; -import { - useMetricNames, - useMetricNamespaces, - useResourceGroups, - useResourceNames, - useResourceTypes, - useSubscriptions, - useMetricMetadata, -} from './dataHooks'; +import { useMetricNames, useMetricNamespaces, useMetricMetadata } from './dataHooks'; +import { setResource } from './setQueryValue'; interface MetricsQueryEditorProps { data: PanelData | undefined; query: AzureMonitorQuery; datasource: Datasource; - subscriptionId?: string; onChange: (newQuery: AzureMonitorQuery) => void; variableOptionGroup: { label: string; options: AzureMonitorOption[] }; setError: (source: string, error: AzureMonitorErrorish | undefined) => void; @@ -41,133 +35,194 @@ const MetricsQueryEditor: React.FC = ({ data, query, datasource, - subscriptionId, variableOptionGroup, onChange, setError, }) => { + const styles = useStyles2(getStyles); + const metricsMetadata = useMetricMetadata(query, datasource, onChange); - const subscriptions = useSubscriptions(query, datasource, onChange, setError); - const resourceGroups = useResourceGroups(query, datasource, onChange, setError); - const resourceTypes = useResourceTypes(query, datasource, onChange, setError); - const resourceNames = useResourceNames(query, datasource, onChange, setError); - const metricNames = useMetricNames(query, datasource, onChange, setError); const metricNamespaces = useMetricNamespaces(query, datasource, onChange, setError); + const metricNames = useMetricNames(query, datasource, onChange, setError); + if (config.featureToggles.azureMonitorExperimentalUI) { + return ( + + + + + + + - return ( -
- - + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } else { + return ( +
+ + + - + + + + + + + + + - - - - - - - - - - - - - - - - - - -
- ); +
+ ); + } }; +const getStyles = () => ({ + row: css({ + rowGap: 0, + }), +}); + export default MetricsQueryEditor; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceGroupsField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceGroupsField.tsx deleted file mode 100644 index fd9aa3fcfb2..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceGroupsField.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; - -import { SelectableValue } from '@grafana/data'; -import { Select } from '@grafana/ui'; - -import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; -import { Field } from '../Field'; - -import { setResourceGroup } from './setQueryValue'; - -interface ResourceGroupsFieldProps extends AzureQueryEditorFieldProps { - resourceGroups: AzureMonitorOption[]; -} - -const ResourceGroupsField: React.FC = ({ - query, - resourceGroups, - variableOptionGroup, - onQueryChange, - setError, -}) => { - const handleChange = useCallback( - (change: SelectableValue) => { - const newQuery = setResourceGroup(query, change.value); - onQueryChange(newQuery); - }, - [onQueryChange, query] - ); - - const options = useMemo(() => [...resourceGroups, variableOptionGroup], [resourceGroups, variableOptionGroup]); - - return ( - - - - ); -}; - -export default ResourceNameField; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceTypeField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceTypeField.tsx deleted file mode 100644 index 3f1b2b5a69e..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceTypeField.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; - -import { SelectableValue } from '@grafana/data'; -import { Select } from '@grafana/ui'; - -import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; -import { Field } from '../Field'; - -import { setResourceType } from './setQueryValue'; - -interface NamespaceFieldProps extends AzureQueryEditorFieldProps { - resourceTypes: AzureMonitorOption[]; -} - -const NamespaceField: React.FC = ({ - resourceTypes, - query, - variableOptionGroup, - onQueryChange, -}) => { - const handleChange = useCallback( - (change: SelectableValue) => { - if (!change.value) { - return; - } - - const newQuery = setResourceType(query, change.value); - onQueryChange(newQuery); - }, - [onQueryChange, query] - ); - - const options = useMemo(() => [...resourceTypes, variableOptionGroup], [resourceTypes, variableOptionGroup]); - - return ( - - {/* It's expected that the label reads Resource type but the property is metricDefinition */} -