From 2bd9e9aca5ce057884abe4bf20795d61fe94079c Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Tue, 10 May 2022 15:05:48 +0100 Subject: [PATCH] AzureMonitor: Add support for selecting multiple options when using the equals and not equals dimension filters (#48650) * Add support for multiselect - Add filters param to Dimensions - Update existing tests - Add MultiSelect component - Add helper function to determine valid options - Update labels hook to account for custom values - Update go type - Add function to build valid filters string * Additional go tests - Ensure query targets are built correctly * Update DimensionFields frontend test - Corrently rerender components - Additional test for multiple labels selection - Better selection of options in react-select components * Fix lint issue * Reset filters when operator or dimension changes * Terminology * Update test * Add backend migration - Update types (deprecate Filter field) - Add migration logic - Update tests - Update dimension filters buliding * Add migration test code * Simplify some logic * Add frontend deprecation notice * Add frontend migration logic and migration tests * Update setting of filter values * Update DimensionFields test * Fix linting issues * PR comment updates - Remove unnecessary if/else condition - Don't set filter default value as queries should be migrated - Add comment explaining why sw operator only accepts one value - Remove unnecessary test for merging of old and new filters * Nit on terminology Co-authored-by: Andres Martinez Gotor * Rename migrations for clarity Co-authored-by: Andres Martinez Gotor --- .../metrics/azuremonitor-datasource.go | 6 +- .../metrics/azuremonitor-datasource_test.go | 35 +++- pkg/tsdb/azuremonitor/metrics/migrations.go | 43 +++++ .../azuremonitor/metrics/migrations_test.go | 62 +++++++ pkg/tsdb/azuremonitor/types/types.go | 23 ++- .../azure_monitor/azure_monitor_datasource.ts | 4 +- .../DimensionFields.test.tsx | 159 ++++++++++++++++-- .../MetricsQueryEditor/DimensionFields.tsx | 62 +++++-- .../MetricsQueryEditor/setQueryValue.ts | 7 +- .../types/query.ts | 4 + .../utils/migrateQuery.test.ts | 84 ++++++++- .../utils/migrateQuery.ts | 56 ++++-- 12 files changed, 482 insertions(+), 63 deletions(-) create mode 100644 pkg/tsdb/azuremonitor/metrics/migrations.go create mode 100644 pkg/tsdb/azuremonitor/metrics/migrations_test.go diff --git a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go index 3a5ba27f2b2..e8ccd0458b4 100644 --- a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go @@ -85,6 +85,8 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf MetricDefinition: azJSONModel.MetricDefinition, ResourceName: azJSONModel.ResourceName, } + + azJSONModel.DimensionFilters = MigrateDimensionFilters(azJSONModel.DimensionFilters) azureURL := ub.BuildMetricsURL() resourceName := azJSONModel.ResourceName @@ -129,10 +131,10 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf dimSB.WriteString(fmt.Sprintf("%s eq '%s'", dimension, dimensionFilter)) } else { for i, filter := range azJSONModel.DimensionFilters { - if filter.Operator != "eq" && filter.Filter == "*" { + if len(filter.Filters) == 0 { dimSB.WriteString(fmt.Sprintf("%s eq '*'", filter.Dimension)) } else { - dimSB.WriteString(filter.String()) + dimSB.WriteString(filter.ConstructFiltersString()) } if i != len(azJSONModel.DimensionFilters)-1 { dimSB.WriteString(" and ") diff --git a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go index 98a021aa429..33be35745c4 100644 --- a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go +++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go @@ -32,7 +32,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) { fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) duration, _ := time.ParseDuration("400s") - + wildcardFilter := "*" + testFilter := "test" tests := []struct { name string azureMonitorVariedProperties map[string]interface{} @@ -101,7 +102,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { name: "legacy query without resourceURI and has dimensionFilter*s* property with one dimension", azureMonitorVariedProperties: map[string]interface{}{ "timeGrain": "PT1M", - "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: "*"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter}}, "top": "30", }, queryInterval: duration, @@ -112,7 +113,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { name: "legacy query without resourceURI and has dimensionFilter*s* property with two dimensions", azureMonitorVariedProperties: map[string]interface{}{ "timeGrain": "PT1M", - "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: "*"}, {Dimension: "tier", Operator: "eq", Filter: "*"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter}, {Dimension: "tier", Operator: "eq", Filter: &wildcardFilter}}, "top": "30", }, queryInterval: duration, @@ -134,7 +135,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { name: "has dimensionFilter*s* property with not equals operator", azureMonitorVariedProperties: map[string]interface{}{ "timeGrain": "PT1M", - "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: "test"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: &wildcardFilter, Filters: []string{"test"}}}, "top": "30", }, queryInterval: duration, @@ -145,7 +146,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { name: "has dimensionFilter*s* property with startsWith operator", azureMonitorVariedProperties: map[string]interface{}{ "timeGrain": "PT1M", - "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: "test"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: &testFilter}}, "top": "30", }, queryInterval: duration, @@ -156,13 +157,35 @@ func TestAzureMonitorBuildQueries(t *testing.T) { name: "correctly sets dimension operator to eq (irrespective of operator) when filter value is '*'", azureMonitorVariedProperties: map[string]interface{}{ "timeGrain": "PT1M", - "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: "*"}, {Dimension: "tier", Operator: "ne", Filter: "*"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: &wildcardFilter}, {Dimension: "tier", Operator: "ne", Filter: &wildcardFilter}}, "top": "30", }, queryInterval: duration, expectedInterval: "PT1M", azureMonitorQueryTarget: "%24filter=blob+eq+%27%2A%27+and+tier+eq+%27%2A%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", }, + { + name: "correctly constructs target when multiple filter values are provided for the 'eq' operator", + azureMonitorVariedProperties: map[string]interface{}{ + "timeGrain": "PT1M", + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter, Filters: []string{"test", "test2"}}}, + "top": "30", + }, + queryInterval: duration, + expectedInterval: "PT1M", + azureMonitorQueryTarget: "%24filter=blob+eq+%27test%27+or+blob+eq+%27test2%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + }, + { + name: "correctly constructs target when multiple filter values are provided for ne 'eq' operator", + azureMonitorVariedProperties: map[string]interface{}{ + "timeGrain": "PT1M", + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: &wildcardFilter, Filters: []string{"test", "test2"}}}, + "top": "30", + }, + queryInterval: duration, + expectedInterval: "PT1M", + azureMonitorQueryTarget: "%24filter=blob+ne+%27test%27+and+blob+ne+%27test2%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + }, } commonAzureModelProps := map[string]interface{}{ diff --git a/pkg/tsdb/azuremonitor/metrics/migrations.go b/pkg/tsdb/azuremonitor/metrics/migrations.go new file mode 100644 index 00000000000..a144a892862 --- /dev/null +++ b/pkg/tsdb/azuremonitor/metrics/migrations.go @@ -0,0 +1,43 @@ +package metrics + +import ( + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" +) + +func MigrateDimensionFilters(filters []types.AzureMonitorDimensionFilter) []types.AzureMonitorDimensionFilter { + var newFilters []types.AzureMonitorDimensionFilter + for _, filter := range filters { + newFilter := filter + // Ignore the deprecation check as this is a migration + // nolint:staticcheck + newFilter.Filter = nil + // If there is no old field and the new field is specified - append as this is valid + // nolint:staticcheck + if filter.Filter == nil && filter.Filters != nil { + newFilters = append(newFilters, newFilter) + } else { + // nolint:staticcheck + oldFilter := *filter.Filter + // If there is an old filter and no new ones then construct the new array and append + if filter.Filters == nil && oldFilter != "*" { + newFilter.Filters = []string{oldFilter} + // If both the new and old fields are specified (edge case) then construct the appropriate values + } else { + hasFilter := false + oldFilters := filter.Filters + for _, filterValue := range oldFilters { + if filterValue == oldFilter { + hasFilter = true + break + } + } + if !hasFilter && oldFilter != "*" { + oldFilters = append(oldFilters, oldFilter) + newFilter.Filters = oldFilters + } + } + newFilters = append(newFilters, newFilter) + } + } + return newFilters +} diff --git a/pkg/tsdb/azuremonitor/metrics/migrations_test.go b/pkg/tsdb/azuremonitor/metrics/migrations_test.go new file mode 100644 index 00000000000..56b7508dbbe --- /dev/null +++ b/pkg/tsdb/azuremonitor/metrics/migrations_test.go @@ -0,0 +1,62 @@ +package metrics + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" +) + +func TestDimensionFiltersMigration(t *testing.T) { + wildcard := "*" + testFilter := "testFilter" + additionalTestFilter := "testFilter2" + tests := []struct { + name string + dimensionFilters []types.AzureMonitorDimensionFilter + expectedDimensionFilters []types.AzureMonitorDimensionFilter + }{ + { + name: "will return new format unchanged", + dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{"testFilter"}}}, + expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{"testFilter"}}}, + }, + { + name: "correctly updates old format with wildcard", + dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &wildcard}}, + expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq"}}, + }, + { + name: "correctly updates old format with a value", + dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &testFilter}}, + expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter}}}, + }, + { + name: "correctly ignores wildcard if filters has a value", + dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &wildcard, Filters: []string{testFilter}}}, + expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter}}}, + }, + { + name: "correctly merges values if filters has a value (ignores duplicates)", + dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &testFilter, Filters: []string{testFilter}}}, + expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter}}}, + }, + { + name: "correctly merges values if filters has a value", + dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &additionalTestFilter, Filters: []string{testFilter}}}, + expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter, additionalTestFilter}}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filters := MigrateDimensionFilters(tt.dimensionFilters) + + if diff := cmp.Diff(tt.expectedDimensionFilters, filters, cmpopts.IgnoreUnexported(simplejson.Json{})); diff != "" { + t.Errorf("Result mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/tsdb/azuremonitor/types/types.go b/pkg/tsdb/azuremonitor/types/types.go index 1a9b4d60271..5213bf5399c 100644 --- a/pkg/tsdb/azuremonitor/types/types.go +++ b/pkg/tsdb/azuremonitor/types/types.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" "regexp" + "strings" "time" "github.com/grafana/grafana-azure-sdk-go/azcredentials" @@ -139,17 +140,23 @@ type AzureMonitorJSONQuery struct { // AzureMonitorDimensionFilter is the model for the frontend sent for azureMonitor metric // queries like "BlobType", "eq", "*" type AzureMonitorDimensionFilter struct { - Dimension string `json:"dimension"` - Operator string `json:"operator"` - Filter string `json:"filter"` + Dimension string `json:"dimension"` + Operator string `json:"operator"` + Filters []string `json:"filters,omitempty"` + // Deprecated: To support multiselection, filters are passed in a slice now. Also migrated in frontend. + Filter *string `json:"filter,omitempty"` } -func (a AzureMonitorDimensionFilter) String() string { - filter := "*" - if a.Filter != "" { - filter = a.Filter +func (a AzureMonitorDimensionFilter) ConstructFiltersString() string { + var filterStrings []string + for _, filter := range a.Filters { + filterStrings = append(filterStrings, fmt.Sprintf("%v %v '%v'", a.Dimension, a.Operator, filter)) + } + if a.Operator == "eq" { + return strings.Join(filterStrings, " or ") + } else { + return strings.Join(filterStrings, " and ") } - return fmt.Sprintf("%v %v '%v'", a.Dimension, a.Operator, filter) } // LogJSONQuery is the frontend JSON query model for an Azure Log Analytics query. 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 fbc474e63ac..6dd613faaef 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 @@ -105,11 +105,11 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend f.dimension && f.dimension !== 'None') .map((f) => { - const filter = templateSrv.replace(f.filter ?? '', scopedVars); + const filters = f.filters?.map((filter) => templateSrv.replace(filter ?? '', scopedVars)); return { dimension: templateSrv.replace(f.dimension, scopedVars), operator: f.operator || 'eq', - filter: filter || '*', // send * when empty + filters: filters || [], }; }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.test.tsx index b26f3cde2b9..b4c6e16cf31 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.test.tsx @@ -1,6 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { openMenu } from 'react-select-event'; import { selectOptionInTest } from '@grafana/ui'; @@ -18,17 +19,17 @@ const variableOptionGroup = { const user = userEvent.setup(); describe('Azure Monitor QueryEditor', () => { - const mockPanelData = createMockPanelData(); const mockDatasource = createMockDatasource(); it('should render a dimension filter', async () => { let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); const onQueryChange = jest.fn(); const dimensionOptions = [ { label: 'Test Dimension 1', value: 'TestDimension1' }, { label: 'Test Dimension 2', value: 'TestDimension2' }, ]; - render( + const { rerender } = render( { mockQuery = appendDimensionFilter(mockQuery); expect(onQueryChange).toHaveBeenCalledWith({ ...mockQuery, - azureMonitor: { ...mockQuery.azureMonitor, dimensionFilters: [{ dimension: '', operator: 'eq', filter: '*' }] }, + azureMonitor: { + ...mockQuery.azureMonitor, + dimensionFilters: [{ dimension: '', operator: 'eq', filters: [] }], + }, }); - render( + rerender( { ...mockQuery, azureMonitor: { ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: '*' }], + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], }, }); expect(screen.queryByText('Test Dimension 1')).toBeInTheDocument(); @@ -74,16 +78,17 @@ describe('Azure Monitor QueryEditor', () => { it('correctly filters out dimensions when selected', async () => { let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); mockQuery.azureMonitor = { ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: '*' }], + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], }; const onQueryChange = jest.fn(); const dimensionOptions = [ { label: 'Test Dimension 1', value: 'TestDimension1' }, { label: 'Test Dimension 2', value: 'TestDimension2' }, ]; - render( + const { rerender } = render( { const addDimension = await screen.findByText('Add new dimension'); await user.click(addDimension); mockQuery = appendDimensionFilter(mockQuery); - render( + rerender( { it('correctly displays dimension labels', async () => { let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); mockQuery.azureMonitor = { ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: '*' }], + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], }; mockPanelData.series = [ @@ -150,7 +156,7 @@ describe('Azure Monitor QueryEditor', () => { dimensionOptions={dimensionOptions} /> ); - const labelSelect = await screen.findByText('Select value'); + const labelSelect = await screen.findByText('Select value(s)'); await user.click(labelSelect); const options = await screen.findAllByLabelText('Select option'); expect(options).toHaveLength(1); @@ -159,9 +165,10 @@ describe('Azure Monitor QueryEditor', () => { it('correctly updates dimension labels', async () => { let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); mockQuery.azureMonitor = { ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: 'testlabel' }], + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel'] }], }; mockPanelData.series = [ @@ -178,7 +185,7 @@ describe('Azure Monitor QueryEditor', () => { ]; const onQueryChange = jest.fn(); const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }]; - render( + const { rerender } = render( { /> ); await screen.findByText('testlabel'); - const labelClear = await screen.findByLabelText('select-clear-value'); + const labelClear = await screen.findByLabelText('Remove testlabel'); await user.click(labelClear); - mockQuery = setDimensionFilterValue(mockQuery, 0, 'filter', ''); + mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', []); expect(onQueryChange).toHaveBeenCalledWith({ ...mockQuery, azureMonitor: { ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: '' }], + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], }, }); mockPanelData.series = [ @@ -214,7 +221,7 @@ describe('Azure Monitor QueryEditor', () => { ], }, ]; - render( + rerender( { dimensionOptions={dimensionOptions} /> ); - const labelSelect = await screen.findByText('Select value'); - await user.click(labelSelect); + const labelSelect = await screen.getByLabelText('dimension-labels-select'); + await openMenu(labelSelect); const options = await screen.findAllByLabelText('Select option'); expect(options).toHaveLength(2); expect(options[0]).toHaveTextContent('testlabel'); expect(options[1]).toHaveTextContent('testlabel2'); }); + + it('correctly selects multiple dimension labels', async () => { + let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); + mockPanelData.series = [ + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel' }, + }, + ], + }, + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel2' }, + }, + ], + }, + ]; + const onQueryChange = jest.fn(); + const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }]; + mockQuery = appendDimensionFilter(mockQuery, 'TestDimension1'); + const { rerender } = render( + {}} + dimensionOptions={dimensionOptions} + /> + ); + const labelSelect = await screen.getByLabelText('dimension-labels-select'); + await user.click(labelSelect); + await openMenu(labelSelect); + await screen.getByText('testlabel'); + await screen.getByText('testlabel2'); + await selectOptionInTest(labelSelect, 'testlabel'); + mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', ['testlabel']); + expect(onQueryChange).toHaveBeenCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel'] }], + }, + }); + mockPanelData.series = [ + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel' }, + }, + ], + }, + ]; + rerender( + {}} + dimensionOptions={dimensionOptions} + /> + ); + const labelSelect2 = await screen.getByLabelText('dimension-labels-select'); + await openMenu(labelSelect2); + const refreshedOptions = await screen.findAllByLabelText('Select options menu'); + expect(refreshedOptions).toHaveLength(1); + expect(refreshedOptions[0]).toHaveTextContent('testlabel2'); + await selectOptionInTest(labelSelect2, 'testlabel2'); + mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', ['testlabel', 'testlabel2']); + expect(onQueryChange).toHaveBeenCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel', 'testlabel2'] }], + }, + }); + mockPanelData.series = [ + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel' }, + }, + ], + }, + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel2' }, + }, + ], + }, + ]; + }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx index e7027918ddc..110fdcc2837 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { SelectableValue, DataFrame, PanelData } from '@grafana/data'; -import { Button, Select, HorizontalGroup, VerticalGroup } from '@grafana/ui'; +import { Button, Select, HorizontalGroup, VerticalGroup, MultiSelect } from '@grafana/ui'; import { AzureMetricDimension, AzureMonitorOption, AzureMonitorQuery, AzureQueryEditorFieldProps } from '../../types'; import { Field } from '../Field'; @@ -44,7 +44,11 @@ const useDimensionLabels = (data: PanelData | undefined, query: AzureMonitorQuer } setDimensionLabels((prevLabels) => { const newLabels: DimensionLabels = {}; - for (const label of Object.keys(labelsObj)) { + const currentLabels = Object.keys(labelsObj); + if (currentLabels.length === 0) { + return prevLabels; + } + for (const label of currentLabels) { if (prevLabels[label] && labelsObj[label].size < prevLabels[label].size) { newLabels[label] = prevLabels[label]; } else { @@ -100,7 +104,7 @@ const DimensionFields: React.FC = ({ data, query, dimensio }; const onFilterInputChange = (index: number, v: SelectableValue | null) => { - onFieldChange(index, 'filter', v?.value ?? ''); + onFieldChange(index, 'filters', [v?.value ?? '']); }; const getValidDimensionOptions = (selectedDimension: string) => { @@ -118,6 +122,18 @@ const DimensionFields: React.FC = ({ data, query, dimensio })); }; + const getValidMultiSelectOptions = (selectedFilters: string[] | undefined, dimension: string) => { + const labelOptions = getValidFilterOptions(undefined, dimension); + if (selectedFilters) { + for (const filter of selectedFilters) { + if (!labelOptions.find((label) => label.value === filter)) { + labelOptions.push({ value: filter, label: filter }); + } + } + } + return labelOptions; + }; + const getValidOperators = (selectedOperator: string) => { if (dimensionOperators.find((operator: SelectableValue) => operator.value === selectedOperator)) { return dimensionOperators; @@ -125,6 +141,14 @@ const DimensionFields: React.FC = ({ data, query, dimensio return [...dimensionOperators, ...(selectedOperator ? [{ label: selectedOperator, value: selectedOperator }] : [])]; }; + const onMultiSelectFilterChange = (index: number, v: Array>) => { + onFieldChange( + index, + 'filters', + v.map((item) => item.value || '') + ); + }; + return ( @@ -145,16 +169,28 @@ const DimensionFields: React.FC = ({ data, query, dimensio onChange={(v) => onFieldChange(index, 'operator', v.value ?? '')} allowCustomValue /> - onFilterInputChange(index, v)} + isClearable + /> + )}