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:
co-authored by
Andres Martinez Gotor
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"
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user