mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 06:41:49 +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,
|
MetricDefinition: azJSONModel.MetricDefinition,
|
||||||
ResourceName: azJSONModel.ResourceName,
|
ResourceName: azJSONModel.ResourceName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
azJSONModel.DimensionFilters = MigrateDimensionFilters(azJSONModel.DimensionFilters)
|
||||||
azureURL := ub.BuildMetricsURL()
|
azureURL := ub.BuildMetricsURL()
|
||||||
|
|
||||||
resourceName := azJSONModel.ResourceName
|
resourceName := azJSONModel.ResourceName
|
||||||
@ -129,10 +131,10 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf
|
|||||||
dimSB.WriteString(fmt.Sprintf("%s eq '%s'", dimension, dimensionFilter))
|
dimSB.WriteString(fmt.Sprintf("%s eq '%s'", dimension, dimensionFilter))
|
||||||
} else {
|
} else {
|
||||||
for i, filter := range azJSONModel.DimensionFilters {
|
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))
|
dimSB.WriteString(fmt.Sprintf("%s eq '*'", filter.Dimension))
|
||||||
} else {
|
} else {
|
||||||
dimSB.WriteString(filter.String())
|
dimSB.WriteString(filter.ConstructFiltersString())
|
||||||
}
|
}
|
||||||
if i != len(azJSONModel.DimensionFilters)-1 {
|
if i != len(azJSONModel.DimensionFilters)-1 {
|
||||||
dimSB.WriteString(" and ")
|
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)
|
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
|
||||||
duration, _ := time.ParseDuration("400s")
|
duration, _ := time.ParseDuration("400s")
|
||||||
|
wildcardFilter := "*"
|
||||||
|
testFilter := "test"
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
azureMonitorVariedProperties map[string]interface{}
|
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",
|
name: "legacy query without resourceURI and has dimensionFilter*s* property with one dimension",
|
||||||
azureMonitorVariedProperties: map[string]interface{}{
|
azureMonitorVariedProperties: map[string]interface{}{
|
||||||
"timeGrain": "PT1M",
|
"timeGrain": "PT1M",
|
||||||
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: "*"}},
|
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter}},
|
||||||
"top": "30",
|
"top": "30",
|
||||||
},
|
},
|
||||||
queryInterval: duration,
|
queryInterval: duration,
|
||||||
@ -112,7 +113,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
|
|||||||
name: "legacy query without resourceURI and has dimensionFilter*s* property with two dimensions",
|
name: "legacy query without resourceURI and has dimensionFilter*s* property with two dimensions",
|
||||||
azureMonitorVariedProperties: map[string]interface{}{
|
azureMonitorVariedProperties: map[string]interface{}{
|
||||||
"timeGrain": "PT1M",
|
"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",
|
"top": "30",
|
||||||
},
|
},
|
||||||
queryInterval: duration,
|
queryInterval: duration,
|
||||||
@ -134,7 +135,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
|
|||||||
name: "has dimensionFilter*s* property with not equals operator",
|
name: "has dimensionFilter*s* property with not equals operator",
|
||||||
azureMonitorVariedProperties: map[string]interface{}{
|
azureMonitorVariedProperties: map[string]interface{}{
|
||||||
"timeGrain": "PT1M",
|
"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",
|
"top": "30",
|
||||||
},
|
},
|
||||||
queryInterval: duration,
|
queryInterval: duration,
|
||||||
@ -145,7 +146,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
|
|||||||
name: "has dimensionFilter*s* property with startsWith operator",
|
name: "has dimensionFilter*s* property with startsWith operator",
|
||||||
azureMonitorVariedProperties: map[string]interface{}{
|
azureMonitorVariedProperties: map[string]interface{}{
|
||||||
"timeGrain": "PT1M",
|
"timeGrain": "PT1M",
|
||||||
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: "test"}},
|
"dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: &testFilter}},
|
||||||
"top": "30",
|
"top": "30",
|
||||||
},
|
},
|
||||||
queryInterval: duration,
|
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 '*'",
|
name: "correctly sets dimension operator to eq (irrespective of operator) when filter value is '*'",
|
||||||
azureMonitorVariedProperties: map[string]interface{}{
|
azureMonitorVariedProperties: map[string]interface{}{
|
||||||
"timeGrain": "PT1M",
|
"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",
|
"top": "30",
|
||||||
},
|
},
|
||||||
queryInterval: duration,
|
queryInterval: duration,
|
||||||
expectedInterval: "PT1M",
|
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",
|
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{}{
|
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/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana-azure-sdk-go/azcredentials"
|
"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
|
// AzureMonitorDimensionFilter is the model for the frontend sent for azureMonitor metric
|
||||||
// queries like "BlobType", "eq", "*"
|
// queries like "BlobType", "eq", "*"
|
||||||
type AzureMonitorDimensionFilter struct {
|
type AzureMonitorDimensionFilter struct {
|
||||||
Dimension string `json:"dimension"`
|
Dimension string `json:"dimension"`
|
||||||
Operator string `json:"operator"`
|
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 {
|
func (a AzureMonitorDimensionFilter) ConstructFiltersString() string {
|
||||||
filter := "*"
|
var filterStrings []string
|
||||||
if a.Filter != "" {
|
for _, filter := range a.Filters {
|
||||||
filter = a.Filter
|
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.
|
// 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 ?? [])
|
const dimensionFilters = (item.dimensionFilters ?? [])
|
||||||
.filter((f) => f.dimension && f.dimension !== 'None')
|
.filter((f) => f.dimension && f.dimension !== 'None')
|
||||||
.map((f) => {
|
.map((f) => {
|
||||||
const filter = templateSrv.replace(f.filter ?? '', scopedVars);
|
const filters = f.filters?.map((filter) => templateSrv.replace(filter ?? '', scopedVars));
|
||||||
return {
|
return {
|
||||||
dimension: templateSrv.replace(f.dimension, scopedVars),
|
dimension: templateSrv.replace(f.dimension, scopedVars),
|
||||||
operator: f.operator || 'eq',
|
operator: f.operator || 'eq',
|
||||||
filter: filter || '*', // send * when empty
|
filters: filters || [],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { openMenu } from 'react-select-event';
|
||||||
|
|
||||||
import { selectOptionInTest } from '@grafana/ui';
|
import { selectOptionInTest } from '@grafana/ui';
|
||||||
|
|
||||||
@ -18,17 +19,17 @@ const variableOptionGroup = {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
describe('Azure Monitor QueryEditor', () => {
|
describe('Azure Monitor QueryEditor', () => {
|
||||||
const mockPanelData = createMockPanelData();
|
|
||||||
const mockDatasource = createMockDatasource();
|
const mockDatasource = createMockDatasource();
|
||||||
|
|
||||||
it('should render a dimension filter', async () => {
|
it('should render a dimension filter', async () => {
|
||||||
let mockQuery = createMockQuery();
|
let mockQuery = createMockQuery();
|
||||||
|
const mockPanelData = createMockPanelData();
|
||||||
const onQueryChange = jest.fn();
|
const onQueryChange = jest.fn();
|
||||||
const dimensionOptions = [
|
const dimensionOptions = [
|
||||||
{ label: 'Test Dimension 1', value: 'TestDimension1' },
|
{ label: 'Test Dimension 1', value: 'TestDimension1' },
|
||||||
{ label: 'Test Dimension 2', value: 'TestDimension2' },
|
{ label: 'Test Dimension 2', value: 'TestDimension2' },
|
||||||
];
|
];
|
||||||
render(
|
const { rerender } = render(
|
||||||
<DimensionFields
|
<DimensionFields
|
||||||
data={mockPanelData}
|
data={mockPanelData}
|
||||||
subscriptionId="123"
|
subscriptionId="123"
|
||||||
@ -45,9 +46,12 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
mockQuery = appendDimensionFilter(mockQuery);
|
mockQuery = appendDimensionFilter(mockQuery);
|
||||||
expect(onQueryChange).toHaveBeenCalledWith({
|
expect(onQueryChange).toHaveBeenCalledWith({
|
||||||
...mockQuery,
|
...mockQuery,
|
||||||
azureMonitor: { ...mockQuery.azureMonitor, dimensionFilters: [{ dimension: '', operator: 'eq', filter: '*' }] },
|
azureMonitor: {
|
||||||
|
...mockQuery.azureMonitor,
|
||||||
|
dimensionFilters: [{ dimension: '', operator: 'eq', filters: [] }],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
render(
|
rerender(
|
||||||
<DimensionFields
|
<DimensionFields
|
||||||
data={mockPanelData}
|
data={mockPanelData}
|
||||||
subscriptionId="123"
|
subscriptionId="123"
|
||||||
@ -65,7 +69,7 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
...mockQuery,
|
...mockQuery,
|
||||||
azureMonitor: {
|
azureMonitor: {
|
||||||
...mockQuery.azureMonitor,
|
...mockQuery.azureMonitor,
|
||||||
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: '*' }],
|
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(screen.queryByText('Test Dimension 1')).toBeInTheDocument();
|
expect(screen.queryByText('Test Dimension 1')).toBeInTheDocument();
|
||||||
@ -74,16 +78,17 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
|
|
||||||
it('correctly filters out dimensions when selected', async () => {
|
it('correctly filters out dimensions when selected', async () => {
|
||||||
let mockQuery = createMockQuery();
|
let mockQuery = createMockQuery();
|
||||||
|
const mockPanelData = createMockPanelData();
|
||||||
mockQuery.azureMonitor = {
|
mockQuery.azureMonitor = {
|
||||||
...mockQuery.azureMonitor,
|
...mockQuery.azureMonitor,
|
||||||
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: '*' }],
|
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }],
|
||||||
};
|
};
|
||||||
const onQueryChange = jest.fn();
|
const onQueryChange = jest.fn();
|
||||||
const dimensionOptions = [
|
const dimensionOptions = [
|
||||||
{ label: 'Test Dimension 1', value: 'TestDimension1' },
|
{ label: 'Test Dimension 1', value: 'TestDimension1' },
|
||||||
{ label: 'Test Dimension 2', value: 'TestDimension2' },
|
{ label: 'Test Dimension 2', value: 'TestDimension2' },
|
||||||
];
|
];
|
||||||
render(
|
const { rerender } = render(
|
||||||
<DimensionFields
|
<DimensionFields
|
||||||
data={mockPanelData}
|
data={mockPanelData}
|
||||||
subscriptionId="123"
|
subscriptionId="123"
|
||||||
@ -98,7 +103,7 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
const addDimension = await screen.findByText('Add new dimension');
|
const addDimension = await screen.findByText('Add new dimension');
|
||||||
await user.click(addDimension);
|
await user.click(addDimension);
|
||||||
mockQuery = appendDimensionFilter(mockQuery);
|
mockQuery = appendDimensionFilter(mockQuery);
|
||||||
render(
|
rerender(
|
||||||
<DimensionFields
|
<DimensionFields
|
||||||
data={mockPanelData}
|
data={mockPanelData}
|
||||||
subscriptionId="123"
|
subscriptionId="123"
|
||||||
@ -119,9 +124,10 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
|
|
||||||
it('correctly displays dimension labels', async () => {
|
it('correctly displays dimension labels', async () => {
|
||||||
let mockQuery = createMockQuery();
|
let mockQuery = createMockQuery();
|
||||||
|
const mockPanelData = createMockPanelData();
|
||||||
mockQuery.azureMonitor = {
|
mockQuery.azureMonitor = {
|
||||||
...mockQuery.azureMonitor,
|
...mockQuery.azureMonitor,
|
||||||
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: '*' }],
|
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }],
|
||||||
};
|
};
|
||||||
|
|
||||||
mockPanelData.series = [
|
mockPanelData.series = [
|
||||||
@ -150,7 +156,7 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
dimensionOptions={dimensionOptions}
|
dimensionOptions={dimensionOptions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const labelSelect = await screen.findByText('Select value');
|
const labelSelect = await screen.findByText('Select value(s)');
|
||||||
await user.click(labelSelect);
|
await user.click(labelSelect);
|
||||||
const options = await screen.findAllByLabelText('Select option');
|
const options = await screen.findAllByLabelText('Select option');
|
||||||
expect(options).toHaveLength(1);
|
expect(options).toHaveLength(1);
|
||||||
@ -159,9 +165,10 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
|
|
||||||
it('correctly updates dimension labels', async () => {
|
it('correctly updates dimension labels', async () => {
|
||||||
let mockQuery = createMockQuery();
|
let mockQuery = createMockQuery();
|
||||||
|
const mockPanelData = createMockPanelData();
|
||||||
mockQuery.azureMonitor = {
|
mockQuery.azureMonitor = {
|
||||||
...mockQuery.azureMonitor,
|
...mockQuery.azureMonitor,
|
||||||
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: 'testlabel' }],
|
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel'] }],
|
||||||
};
|
};
|
||||||
|
|
||||||
mockPanelData.series = [
|
mockPanelData.series = [
|
||||||
@ -178,7 +185,7 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
];
|
];
|
||||||
const onQueryChange = jest.fn();
|
const onQueryChange = jest.fn();
|
||||||
const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }];
|
const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }];
|
||||||
render(
|
const { rerender } = render(
|
||||||
<DimensionFields
|
<DimensionFields
|
||||||
data={mockPanelData}
|
data={mockPanelData}
|
||||||
subscriptionId="123"
|
subscriptionId="123"
|
||||||
@ -191,14 +198,14 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
await screen.findByText('testlabel');
|
await screen.findByText('testlabel');
|
||||||
const labelClear = await screen.findByLabelText('select-clear-value');
|
const labelClear = await screen.findByLabelText('Remove testlabel');
|
||||||
await user.click(labelClear);
|
await user.click(labelClear);
|
||||||
mockQuery = setDimensionFilterValue(mockQuery, 0, 'filter', '');
|
mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', []);
|
||||||
expect(onQueryChange).toHaveBeenCalledWith({
|
expect(onQueryChange).toHaveBeenCalledWith({
|
||||||
...mockQuery,
|
...mockQuery,
|
||||||
azureMonitor: {
|
azureMonitor: {
|
||||||
...mockQuery.azureMonitor,
|
...mockQuery.azureMonitor,
|
||||||
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: '' }],
|
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mockPanelData.series = [
|
mockPanelData.series = [
|
||||||
@ -214,7 +221,7 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(
|
rerender(
|
||||||
<DimensionFields
|
<DimensionFields
|
||||||
data={mockPanelData}
|
data={mockPanelData}
|
||||||
subscriptionId="123"
|
subscriptionId="123"
|
||||||
@ -226,11 +233,127 @@ describe('Azure Monitor QueryEditor', () => {
|
|||||||
dimensionOptions={dimensionOptions}
|
dimensionOptions={dimensionOptions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const labelSelect = await screen.findByText('Select value');
|
const labelSelect = await screen.getByLabelText('dimension-labels-select');
|
||||||
await user.click(labelSelect);
|
await openMenu(labelSelect);
|
||||||
const options = await screen.findAllByLabelText('Select option');
|
const options = await screen.findAllByLabelText('Select option');
|
||||||
expect(options).toHaveLength(2);
|
expect(options).toHaveLength(2);
|
||||||
expect(options[0]).toHaveTextContent('testlabel');
|
expect(options[0]).toHaveTextContent('testlabel');
|
||||||
expect(options[1]).toHaveTextContent('testlabel2');
|
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 React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { SelectableValue, DataFrame, PanelData } from '@grafana/data';
|
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 { AzureMetricDimension, AzureMonitorOption, AzureMonitorQuery, AzureQueryEditorFieldProps } from '../../types';
|
||||||
import { Field } from '../Field';
|
import { Field } from '../Field';
|
||||||
@ -44,7 +44,11 @@ const useDimensionLabels = (data: PanelData | undefined, query: AzureMonitorQuer
|
|||||||
}
|
}
|
||||||
setDimensionLabels((prevLabels) => {
|
setDimensionLabels((prevLabels) => {
|
||||||
const newLabels: DimensionLabels = {};
|
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) {
|
if (prevLabels[label] && labelsObj[label].size < prevLabels[label].size) {
|
||||||
newLabels[label] = prevLabels[label];
|
newLabels[label] = prevLabels[label];
|
||||||
} else {
|
} else {
|
||||||
@ -100,7 +104,7 @@ const DimensionFields: React.FC<DimensionFieldsProps> = ({ data, query, dimensio
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFilterInputChange = (index: number, v: SelectableValue<string> | null) => {
|
const onFilterInputChange = (index: number, v: SelectableValue<string> | null) => {
|
||||||
onFieldChange(index, 'filter', v?.value ?? '');
|
onFieldChange(index, 'filters', [v?.value ?? '']);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getValidDimensionOptions = (selectedDimension: string) => {
|
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) => {
|
const getValidOperators = (selectedOperator: string) => {
|
||||||
if (dimensionOperators.find((operator: SelectableValue) => operator.value === selectedOperator)) {
|
if (dimensionOperators.find((operator: SelectableValue) => operator.value === selectedOperator)) {
|
||||||
return dimensionOperators;
|
return dimensionOperators;
|
||||||
@ -125,6 +141,14 @@ const DimensionFields: React.FC<DimensionFieldsProps> = ({ data, query, dimensio
|
|||||||
return [...dimensionOperators, ...(selectedOperator ? [{ label: selectedOperator, value: selectedOperator }] : [])];
|
return [...dimensionOperators, ...(selectedOperator ? [{ label: selectedOperator, value: selectedOperator }] : [])];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onMultiSelectFilterChange = (index: number, v: Array<SelectableValue<string>>) => {
|
||||||
|
onFieldChange(
|
||||||
|
index,
|
||||||
|
'filters',
|
||||||
|
v.map((item) => item.value || '')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field label="Dimension">
|
<Field label="Dimension">
|
||||||
<VerticalGroup spacing="xs">
|
<VerticalGroup spacing="xs">
|
||||||
@ -145,16 +169,28 @@ const DimensionFields: React.FC<DimensionFieldsProps> = ({ data, query, dimensio
|
|||||||
onChange={(v) => onFieldChange(index, 'operator', v.value ?? '')}
|
onChange={(v) => onFieldChange(index, 'operator', v.value ?? '')}
|
||||||
allowCustomValue
|
allowCustomValue
|
||||||
/>
|
/>
|
||||||
<Select
|
{filter.operator === 'eq' || filter.operator === 'ne' ? (
|
||||||
menuShouldPortal
|
<MultiSelect
|
||||||
placeholder="Select value"
|
menuShouldPortal
|
||||||
value={filter.filter ? filter.filter : ''}
|
placeholder="Select value(s)"
|
||||||
allowCustomValue
|
value={filter.filters}
|
||||||
options={getValidFilterOptions(filter.filter, filter.dimension)}
|
options={getValidMultiSelectOptions(filter.filters, filter.dimension)}
|
||||||
onChange={(v) => onFilterInputChange(index, v)}
|
onChange={(v) => onMultiSelectFilterChange(index, v)}
|
||||||
isClearable
|
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
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="md"
|
size="md"
|
||||||
|
@ -185,7 +185,7 @@ export function appendDimensionFilter(
|
|||||||
query: AzureMonitorQuery,
|
query: AzureMonitorQuery,
|
||||||
dimension = '',
|
dimension = '',
|
||||||
operator = 'eq',
|
operator = 'eq',
|
||||||
filter = '*'
|
filters: string[] = []
|
||||||
): AzureMonitorQuery {
|
): AzureMonitorQuery {
|
||||||
const existingFilters = query.azureMonitor?.dimensionFilters ?? [];
|
const existingFilters = query.azureMonitor?.dimensionFilters ?? [];
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ export function appendDimensionFilter(
|
|||||||
{
|
{
|
||||||
dimension,
|
dimension,
|
||||||
operator,
|
operator,
|
||||||
filter,
|
filters,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -216,6 +216,9 @@ export function setDimensionFilterValue<Key extends keyof AzureMetricDimension>(
|
|||||||
const newFilters = [...existingFilters];
|
const newFilters = [...existingFilters];
|
||||||
const newFilter = newFilters[index];
|
const newFilter = newFilters[index];
|
||||||
newFilter[fieldName] = value;
|
newFilter[fieldName] = value;
|
||||||
|
if (fieldName === 'dimension' || fieldName === 'operator') {
|
||||||
|
newFilter.filters = [];
|
||||||
|
}
|
||||||
return setDimensionFilters(query, newFilters);
|
return setDimensionFilters(query, newFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,5 +81,9 @@ export interface AzureResourceGraphQuery {
|
|||||||
export interface AzureMetricDimension {
|
export interface AzureMetricDimension {
|
||||||
dimension: string;
|
dimension: string;
|
||||||
operator: string;
|
operator: string;
|
||||||
|
filters?: string[];
|
||||||
|
/**
|
||||||
|
* @deprecated filter is deprecated in favour of filters to support multiselect
|
||||||
|
*/
|
||||||
filter?: string;
|
filter?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AzureMonitorQuery, AzureQueryType } from '../types';
|
import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType } from '../types';
|
||||||
|
|
||||||
import migrateQuery from './migrateQuery';
|
import migrateQuery from './migrateQuery';
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ const modernMetricsQuery: AzureMonitorQuery = {
|
|||||||
aggregation: 'Average',
|
aggregation: 'Average',
|
||||||
alias: '{{ dimensionvalue }}',
|
alias: '{{ dimensionvalue }}',
|
||||||
allowedTimeGrainsMs: [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
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',
|
metricDefinition: 'microsoft.insights/components',
|
||||||
metricName: 'dependencies/duration',
|
metricName: 'dependencies/duration',
|
||||||
metricNamespace: 'microsoft.insights/components',
|
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,
|
setTimeGrain as setMetricsTimeGrain,
|
||||||
} from '../components/MetricsQueryEditor/setQueryValue';
|
} from '../components/MetricsQueryEditor/setQueryValue';
|
||||||
import TimegrainConverter from '../time_grain_converter';
|
import TimegrainConverter from '../time_grain_converter';
|
||||||
import { AzureMonitorQuery, AzureQueryType } from '../types';
|
import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType } from '../types';
|
||||||
|
|
||||||
const OLD_DEFAULT_DROPDOWN_VALUE = 'select';
|
const OLD_DEFAULT_DROPDOWN_VALUE = 'select';
|
||||||
|
|
||||||
@ -20,8 +20,9 @@ export default function migrateQuery(query: AzureMonitorQuery): AzureMonitorQuer
|
|||||||
workingQuery = migrateTimeGrains(workingQuery);
|
workingQuery = migrateTimeGrains(workingQuery);
|
||||||
workingQuery = migrateLogAnalyticsToFromTimes(workingQuery);
|
workingQuery = migrateLogAnalyticsToFromTimes(workingQuery);
|
||||||
workingQuery = migrateToDefaultNamespace(workingQuery);
|
workingQuery = migrateToDefaultNamespace(workingQuery);
|
||||||
workingQuery = migrateMetricsDimensionFilters(workingQuery);
|
workingQuery = migrateDimensionToDimensionFilter(workingQuery);
|
||||||
workingQuery = migrateResourceUri(workingQuery);
|
workingQuery = migrateResourceUri(workingQuery);
|
||||||
|
workingQuery = migrateDimensionFilterToArray(workingQuery);
|
||||||
|
|
||||||
return workingQuery;
|
return workingQuery;
|
||||||
}
|
}
|
||||||
@ -79,17 +80,14 @@ function migrateToDefaultNamespace(query: AzureMonitorQuery): AzureMonitorQuery
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateMetricsDimensionFilters(query: AzureMonitorQuery): AzureMonitorQuery {
|
function migrateDimensionToDimensionFilter(query: AzureMonitorQuery): AzureMonitorQuery {
|
||||||
let workingQuery = query;
|
let workingQuery = query;
|
||||||
|
|
||||||
const oldDimension = workingQuery.azureMonitor?.dimension;
|
const oldDimension = workingQuery.azureMonitor?.dimension;
|
||||||
if (oldDimension && oldDimension !== 'None') {
|
if (oldDimension && oldDimension !== 'None') {
|
||||||
workingQuery = appendDimensionFilter(
|
workingQuery = appendDimensionFilter(workingQuery, oldDimension, 'eq', [
|
||||||
workingQuery,
|
workingQuery.azureMonitor?.dimensionFilter || '',
|
||||||
oldDimension,
|
]);
|
||||||
'eq',
|
|
||||||
workingQuery.azureMonitor?.dimensionFilter || ''
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return workingQuery;
|
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
|
// datasource.ts also contains some migrations, which have been moved to here. Unsure whether
|
||||||
// they should also do all the other migrations...
|
// they should also do all the other migrations...
|
||||||
export function datasourceMigrations(query: AzureMonitorQuery): AzureMonitorQuery {
|
export function datasourceMigrations(query: AzureMonitorQuery): AzureMonitorQuery {
|
||||||
@ -135,8 +170,9 @@ export function datasourceMigrations(query: AzureMonitorQuery): AzureMonitorQuer
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (workingQuery.queryType === AzureQueryType.AzureMonitor && workingQuery.azureMonitor) {
|
if (workingQuery.queryType === AzureQueryType.AzureMonitor && workingQuery.azureMonitor) {
|
||||||
workingQuery = migrateMetricsDimensionFilters(workingQuery);
|
workingQuery = migrateDimensionToDimensionFilter(workingQuery);
|
||||||
workingQuery = migrateResourceUri(workingQuery);
|
workingQuery = migrateResourceUri(workingQuery);
|
||||||
|
workingQuery = migrateDimensionFilterToArray(workingQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
return workingQuery;
|
return workingQuery;
|
||||||
|
Reference in New Issue
Block a user