mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 16:43:07 +08:00
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:
@ -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{}{
|
||||
|
43
pkg/tsdb/azuremonitor/metrics/migrations.go
Normal file
43
pkg/tsdb/azuremonitor/metrics/migrations.go
Normal file
@ -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
|
||||
}
|
62
pkg/tsdb/azuremonitor/metrics/migrations_test.go
Normal file
62
pkg/tsdb/azuremonitor/metrics/migrations_test.go
Normal file
@ -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"
|
||||
@ -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.
|
||||
|
@ -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
|
||||
/>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
placeholder="Select value"
|
||||
value={filter.filter ? filter.filter : ''}
|
||||
allowCustomValue
|
||||
options={getValidFilterOptions(filter.filter, filter.dimension)}
|
||||
onChange={(v) => onFilterInputChange(index, v)}
|
||||
isClearable
|
||||
/>
|
||||
|
||||
{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.filters ? filter.filters[0] : ''}
|
||||
allowCustomValue
|
||||
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;
|
||||
|
Reference in New Issue
Block a user