mirror of https://github.com/grafana/grafana.git
				
				
				
			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 <andres.martinez@grafana.com> * Rename migrations for clarity Co-authored-by: Andres Martinez Gotor <andres.martinez@grafana.com>
This commit is contained in:
		
							parent
							
								
									61772a66b6
								
							
						
					
					
						commit
						2bd9e9aca5
					
				| 
						 | 
				
			
			@ -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 ")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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{}{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ import (
 | 
			
		|||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/grafana/grafana-azure-sdk-go/azcredentials"
 | 
			
		||||
| 
						 | 
				
			
			@ -141,15 +142,21 @@ type AzureMonitorJSONQuery struct {
 | 
			
		|||
type AzureMonitorDimensionFilter struct {
 | 
			
		||||
	Dimension string   `json:"dimension"`
 | 
			
		||||
	Operator  string   `json:"operator"`
 | 
			
		||||
	Filter    string `json:"filter"`
 | 
			
		||||
	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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -105,11 +105,11 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
 | 
			
		|||
    const dimensionFilters = (item.dimensionFilters ?? [])
 | 
			
		||||
      .filter((f) => 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 || [],
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(
 | 
			
		||||
      <DimensionFields
 | 
			
		||||
        data={mockPanelData}
 | 
			
		||||
        subscriptionId="123"
 | 
			
		||||
| 
						 | 
				
			
			@ -45,9 +46,12 @@ describe('Azure Monitor QueryEditor', () => {
 | 
			
		|||
    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(
 | 
			
		||||
      <DimensionFields
 | 
			
		||||
        data={mockPanelData}
 | 
			
		||||
        subscriptionId="123"
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +69,7 @@ describe('Azure Monitor QueryEditor', () => {
 | 
			
		|||
      ...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(
 | 
			
		||||
      <DimensionFields
 | 
			
		||||
        data={mockPanelData}
 | 
			
		||||
        subscriptionId="123"
 | 
			
		||||
| 
						 | 
				
			
			@ -98,7 +103,7 @@ describe('Azure Monitor QueryEditor', () => {
 | 
			
		|||
    const addDimension = await screen.findByText('Add new dimension');
 | 
			
		||||
    await user.click(addDimension);
 | 
			
		||||
    mockQuery = appendDimensionFilter(mockQuery);
 | 
			
		||||
    render(
 | 
			
		||||
    rerender(
 | 
			
		||||
      <DimensionFields
 | 
			
		||||
        data={mockPanelData}
 | 
			
		||||
        subscriptionId="123"
 | 
			
		||||
| 
						 | 
				
			
			@ -119,9 +124,10 @@ describe('Azure Monitor QueryEditor', () => {
 | 
			
		|||
 | 
			
		||||
  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(
 | 
			
		||||
      <DimensionFields
 | 
			
		||||
        data={mockPanelData}
 | 
			
		||||
        subscriptionId="123"
 | 
			
		||||
| 
						 | 
				
			
			@ -191,14 +198,14 @@ describe('Azure Monitor QueryEditor', () => {
 | 
			
		|||
      />
 | 
			
		||||
    );
 | 
			
		||||
    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(
 | 
			
		||||
      <DimensionFields
 | 
			
		||||
        data={mockPanelData}
 | 
			
		||||
        subscriptionId="123"
 | 
			
		||||
| 
						 | 
				
			
			@ -226,11 +233,127 @@ describe('Azure Monitor QueryEditor', () => {
 | 
			
		|||
        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(
 | 
			
		||||
      <DimensionFields
 | 
			
		||||
        data={mockPanelData}
 | 
			
		||||
        subscriptionId="123"
 | 
			
		||||
        query={mockQuery}
 | 
			
		||||
        onQueryChange={onQueryChange}
 | 
			
		||||
        datasource={mockDatasource}
 | 
			
		||||
        variableOptionGroup={variableOptionGroup}
 | 
			
		||||
        setError={() => {}}
 | 
			
		||||
        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(
 | 
			
		||||
      <DimensionFields
 | 
			
		||||
        data={mockPanelData}
 | 
			
		||||
        subscriptionId="123"
 | 
			
		||||
        query={mockQuery}
 | 
			
		||||
        onQueryChange={onQueryChange}
 | 
			
		||||
        datasource={mockDatasource}
 | 
			
		||||
        variableOptionGroup={variableOptionGroup}
 | 
			
		||||
        setError={() => {}}
 | 
			
		||||
        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' },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<DimensionFieldsProps> = ({ data, query, dimensio
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  const onFilterInputChange = (index: number, v: SelectableValue<string> | null) => {
 | 
			
		||||
    onFieldChange(index, 'filter', v?.value ?? '');
 | 
			
		||||
    onFieldChange(index, 'filters', [v?.value ?? '']);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getValidDimensionOptions = (selectedDimension: string) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -118,6 +122,18 @@ const DimensionFields: React.FC<DimensionFieldsProps> = ({ 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<DimensionFieldsProps> = ({ data, query, dimensio
 | 
			
		|||
    return [...dimensionOperators, ...(selectedOperator ? [{ label: selectedOperator, value: selectedOperator }] : [])];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onMultiSelectFilterChange = (index: number, v: Array<SelectableValue<string>>) => {
 | 
			
		||||
    onFieldChange(
 | 
			
		||||
      index,
 | 
			
		||||
      'filters',
 | 
			
		||||
      v.map((item) => item.value || '')
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Field label="Dimension">
 | 
			
		||||
      <VerticalGroup spacing="xs">
 | 
			
		||||
| 
						 | 
				
			
			@ -145,16 +169,28 @@ const DimensionFields: React.FC<DimensionFieldsProps> = ({ data, query, dimensio
 | 
			
		|||
              onChange={(v) => onFieldChange(index, 'operator', v.value ?? '')}
 | 
			
		||||
              allowCustomValue
 | 
			
		||||
            />
 | 
			
		||||
            {filter.operator === 'eq' || filter.operator === 'ne' ? (
 | 
			
		||||
              <MultiSelect
 | 
			
		||||
                menuShouldPortal
 | 
			
		||||
                placeholder="Select value(s)"
 | 
			
		||||
                value={filter.filters}
 | 
			
		||||
                options={getValidMultiSelectOptions(filter.filters, filter.dimension)}
 | 
			
		||||
                onChange={(v) => onMultiSelectFilterChange(index, v)}
 | 
			
		||||
                aria-label={'dimension-labels-select'}
 | 
			
		||||
                allowCustomValue
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              // The API does not currently allow for multiple "starts with" clauses to be used.
 | 
			
		||||
              <Select
 | 
			
		||||
                menuShouldPortal
 | 
			
		||||
                placeholder="Select value"
 | 
			
		||||
              value={filter.filter ? filter.filter : ''}
 | 
			
		||||
                value={filter.filters ? filter.filters[0] : ''}
 | 
			
		||||
                allowCustomValue
 | 
			
		||||
              options={getValidFilterOptions(filter.filter, filter.dimension)}
 | 
			
		||||
                options={getValidFilterOptions(filter.filters ? filter.filters[0] : '', filter.dimension)}
 | 
			
		||||
                onChange={(v) => onFilterInputChange(index, v)}
 | 
			
		||||
                isClearable
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
            )}
 | 
			
		||||
            <Button
 | 
			
		||||
              variant="secondary"
 | 
			
		||||
              size="md"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -185,7 +185,7 @@ export function appendDimensionFilter(
 | 
			
		|||
  query: AzureMonitorQuery,
 | 
			
		||||
  dimension = '',
 | 
			
		||||
  operator = 'eq',
 | 
			
		||||
  filter = '*'
 | 
			
		||||
  filters: string[] = []
 | 
			
		||||
): AzureMonitorQuery {
 | 
			
		||||
  const existingFilters = query.azureMonitor?.dimensionFilters ?? [];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -194,7 +194,7 @@ export function appendDimensionFilter(
 | 
			
		|||
    {
 | 
			
		||||
      dimension,
 | 
			
		||||
      operator,
 | 
			
		||||
      filter,
 | 
			
		||||
      filters,
 | 
			
		||||
    },
 | 
			
		||||
  ]);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -216,6 +216,9 @@ export function setDimensionFilterValue<Key extends keyof AzureMetricDimension>(
 | 
			
		|||
  const newFilters = [...existingFilters];
 | 
			
		||||
  const newFilter = newFilters[index];
 | 
			
		||||
  newFilter[fieldName] = value;
 | 
			
		||||
  if (fieldName === 'dimension' || fieldName === 'operator') {
 | 
			
		||||
    newFilter.filters = [];
 | 
			
		||||
  }
 | 
			
		||||
  return setDimensionFilters(query, newFilters);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -81,5 +81,9 @@ export interface AzureResourceGraphQuery {
 | 
			
		|||
export interface AzureMetricDimension {
 | 
			
		||||
  dimension: string;
 | 
			
		||||
  operator: string;
 | 
			
		||||
  filters?: string[];
 | 
			
		||||
  /**
 | 
			
		||||
   * @deprecated filter is deprecated in favour of filters to support multiselect
 | 
			
		||||
   */
 | 
			
		||||
  filter?: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { AzureMonitorQuery, AzureQueryType } from '../types';
 | 
			
		||||
import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType } from '../types';
 | 
			
		||||
 | 
			
		||||
import migrateQuery from './migrateQuery';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +62,7 @@ const modernMetricsQuery: AzureMonitorQuery = {
 | 
			
		|||
    aggregation: 'Average',
 | 
			
		||||
    alias: '{{ dimensionvalue }}',
 | 
			
		||||
    allowedTimeGrainsMs: [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
 | 
			
		||||
    dimensionFilters: [{ dimension: 'dependency/success', filter: '', operator: 'eq' }],
 | 
			
		||||
    dimensionFilters: [{ dimension: 'dependency/success', filters: ['*'], operator: 'eq' }],
 | 
			
		||||
    metricDefinition: 'microsoft.insights/components',
 | 
			
		||||
    metricName: 'dependencies/duration',
 | 
			
		||||
    metricNamespace: 'microsoft.insights/components',
 | 
			
		||||
| 
						 | 
				
			
			@ -115,4 +115,84 @@ describe('AzureMonitor: migrateQuery', () => {
 | 
			
		|||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('migrating from a v9 query to the latest query version', () => {
 | 
			
		||||
    it('will not change valid dimension filters', () => {
 | 
			
		||||
      const dimensionFilters: AzureMetricDimension[] = [
 | 
			
		||||
        { dimension: 'TestDimension', operator: 'eq', filters: ['testFilter'] },
 | 
			
		||||
      ];
 | 
			
		||||
      const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } });
 | 
			
		||||
      expect(result).toMatchObject(
 | 
			
		||||
        expect.objectContaining({
 | 
			
		||||
          azureMonitor: expect.objectContaining({
 | 
			
		||||
            dimensionFilters,
 | 
			
		||||
          }),
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    it('correctly updates old filter containing wildcard', () => {
 | 
			
		||||
      const dimensionFilters: AzureMetricDimension[] = [{ dimension: 'TestDimension', operator: 'eq', filter: '*' }];
 | 
			
		||||
      const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } });
 | 
			
		||||
      expect(result).toMatchObject(
 | 
			
		||||
        expect.objectContaining({
 | 
			
		||||
          azureMonitor: expect.objectContaining({
 | 
			
		||||
            dimensionFilters: [
 | 
			
		||||
              { dimension: dimensionFilters[0].dimension, operator: dimensionFilters[0].operator, filters: ['*'] },
 | 
			
		||||
            ],
 | 
			
		||||
          }),
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    it('correctly updates old filter containing value', () => {
 | 
			
		||||
      const dimensionFilters: AzureMetricDimension[] = [{ dimension: 'TestDimension', operator: 'eq', filter: 'test' }];
 | 
			
		||||
      const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } });
 | 
			
		||||
      expect(result).toMatchObject(
 | 
			
		||||
        expect.objectContaining({
 | 
			
		||||
          azureMonitor: expect.objectContaining({
 | 
			
		||||
            dimensionFilters: [
 | 
			
		||||
              { dimension: dimensionFilters[0].dimension, operator: dimensionFilters[0].operator, filters: ['test'] },
 | 
			
		||||
            ],
 | 
			
		||||
          }),
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    it('correctly ignores wildcard if filters has a value', () => {
 | 
			
		||||
      const dimensionFilters: AzureMetricDimension[] = [
 | 
			
		||||
        { dimension: 'TestDimension', operator: 'eq', filter: '*', filters: ['testFilter'] },
 | 
			
		||||
      ];
 | 
			
		||||
      const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } });
 | 
			
		||||
      expect(result).toMatchObject(
 | 
			
		||||
        expect.objectContaining({
 | 
			
		||||
          azureMonitor: expect.objectContaining({
 | 
			
		||||
            dimensionFilters: [
 | 
			
		||||
              {
 | 
			
		||||
                dimension: dimensionFilters[0].dimension,
 | 
			
		||||
                operator: dimensionFilters[0].operator,
 | 
			
		||||
                filters: ['testFilter'],
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          }),
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    it('correctly ignores duplicates', () => {
 | 
			
		||||
      const dimensionFilters: AzureMetricDimension[] = [
 | 
			
		||||
        { dimension: 'TestDimension', operator: 'eq', filter: 'testFilter', filters: ['testFilter'] },
 | 
			
		||||
      ];
 | 
			
		||||
      const result = migrateQuery({ ...azureMonitorQueryV8, azureMonitor: { dimensionFilters } });
 | 
			
		||||
      expect(result).toMatchObject(
 | 
			
		||||
        expect.objectContaining({
 | 
			
		||||
          azureMonitor: expect.objectContaining({
 | 
			
		||||
            dimensionFilters: [
 | 
			
		||||
              {
 | 
			
		||||
                dimension: dimensionFilters[0].dimension,
 | 
			
		||||
                operator: dimensionFilters[0].operator,
 | 
			
		||||
                filters: ['testFilter'],
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          }),
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ import {
 | 
			
		|||
  setTimeGrain as setMetricsTimeGrain,
 | 
			
		||||
} from '../components/MetricsQueryEditor/setQueryValue';
 | 
			
		||||
import TimegrainConverter from '../time_grain_converter';
 | 
			
		||||
import { AzureMonitorQuery, AzureQueryType } from '../types';
 | 
			
		||||
import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType } from '../types';
 | 
			
		||||
 | 
			
		||||
const OLD_DEFAULT_DROPDOWN_VALUE = 'select';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20,8 +20,9 @@ export default function migrateQuery(query: AzureMonitorQuery): AzureMonitorQuer
 | 
			
		|||
  workingQuery = migrateTimeGrains(workingQuery);
 | 
			
		||||
  workingQuery = migrateLogAnalyticsToFromTimes(workingQuery);
 | 
			
		||||
  workingQuery = migrateToDefaultNamespace(workingQuery);
 | 
			
		||||
  workingQuery = migrateMetricsDimensionFilters(workingQuery);
 | 
			
		||||
  workingQuery = migrateDimensionToDimensionFilter(workingQuery);
 | 
			
		||||
  workingQuery = migrateResourceUri(workingQuery);
 | 
			
		||||
  workingQuery = migrateDimensionFilterToArray(workingQuery);
 | 
			
		||||
 | 
			
		||||
  return workingQuery;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -79,17 +80,14 @@ function migrateToDefaultNamespace(query: AzureMonitorQuery): AzureMonitorQuery
 | 
			
		|||
  return query;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function migrateMetricsDimensionFilters(query: AzureMonitorQuery): AzureMonitorQuery {
 | 
			
		||||
function migrateDimensionToDimensionFilter(query: AzureMonitorQuery): AzureMonitorQuery {
 | 
			
		||||
  let workingQuery = query;
 | 
			
		||||
 | 
			
		||||
  const oldDimension = workingQuery.azureMonitor?.dimension;
 | 
			
		||||
  if (oldDimension && oldDimension !== 'None') {
 | 
			
		||||
    workingQuery = appendDimensionFilter(
 | 
			
		||||
      workingQuery,
 | 
			
		||||
      oldDimension,
 | 
			
		||||
      'eq',
 | 
			
		||||
      workingQuery.azureMonitor?.dimensionFilter || ''
 | 
			
		||||
    );
 | 
			
		||||
    workingQuery = appendDimensionFilter(workingQuery, oldDimension, 'eq', [
 | 
			
		||||
      workingQuery.azureMonitor?.dimensionFilter || '',
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return workingQuery;
 | 
			
		||||
| 
						 | 
				
			
			@ -122,6 +120,43 @@ function migrateResourceUri(query: AzureMonitorQuery): AzureMonitorQuery {
 | 
			
		|||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function migrateDimensionFilterToArray(query: AzureMonitorQuery): AzureMonitorQuery {
 | 
			
		||||
  const azureMonitorQuery = query.azureMonitor;
 | 
			
		||||
 | 
			
		||||
  if (!azureMonitorQuery) {
 | 
			
		||||
    return query;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const newFilters: AzureMetricDimension[] = [];
 | 
			
		||||
  const dimensionFilters = azureMonitorQuery.dimensionFilters;
 | 
			
		||||
  if (dimensionFilters && dimensionFilters.length > 0) {
 | 
			
		||||
    dimensionFilters.forEach((filter) => {
 | 
			
		||||
      const staticProps = { dimension: filter.dimension, operator: filter.operator };
 | 
			
		||||
      if (!filter.filters && filter.filter) {
 | 
			
		||||
        newFilters.push({ ...staticProps, filters: [filter.filter] });
 | 
			
		||||
      } else {
 | 
			
		||||
        let hasFilter = false;
 | 
			
		||||
        if (filter.filters && filter.filter) {
 | 
			
		||||
          for (const oldFilter of filter.filters) {
 | 
			
		||||
            if (filter.filter === oldFilter) {
 | 
			
		||||
              hasFilter = true;
 | 
			
		||||
              break;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          if (!hasFilter && filter.filter !== '*') {
 | 
			
		||||
            filter.filters.push(filter.filter);
 | 
			
		||||
          }
 | 
			
		||||
          newFilters.push({ ...staticProps, filters: filter.filters });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    if (newFilters.length > 0) {
 | 
			
		||||
      return { ...query, azureMonitor: { ...azureMonitorQuery, dimensionFilters: newFilters } };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return query;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// datasource.ts also contains some migrations, which have been moved to here. Unsure whether
 | 
			
		||||
// they should also do all the other migrations...
 | 
			
		||||
export function datasourceMigrations(query: AzureMonitorQuery): AzureMonitorQuery {
 | 
			
		||||
| 
						 | 
				
			
			@ -135,8 +170,9 @@ export function datasourceMigrations(query: AzureMonitorQuery): AzureMonitorQuer
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  if (workingQuery.queryType === AzureQueryType.AzureMonitor && workingQuery.azureMonitor) {
 | 
			
		||||
    workingQuery = migrateMetricsDimensionFilters(workingQuery);
 | 
			
		||||
    workingQuery = migrateDimensionToDimensionFilter(workingQuery);
 | 
			
		||||
    workingQuery = migrateResourceUri(workingQuery);
 | 
			
		||||
    workingQuery = migrateDimensionFilterToArray(workingQuery);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return workingQuery;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue