diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 9518bc940e8..00cab45ae7c 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -122,6 +122,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams | | `exploreMetricsUseExternalAppPlugin` | Use the externalized Grafana Metrics Drilldown (formerly known as Explore Metrics) app plugin | | `alertRuleRestore` | Enables the alert rule restore feature | +| `azureMonitorLogsBuilderEditor` | Enables the logs builder mode for the Azure Monitor data source | ## Experimental feature toggles diff --git a/e2e/cloud-plugins-suite/azure-monitor.spec.ts b/e2e/cloud-plugins-suite/azure-monitor.spec.ts index 23de1596f79..fa3e85eae85 100644 --- a/e2e/cloud-plugins-suite/azure-monitor.spec.ts +++ b/e2e/cloud-plugins-suite/azure-monitor.spec.ts @@ -6,8 +6,8 @@ import { selectors as rawSelectors } from '@grafana/e2e-selectors'; import { selectors } from '../../public/app/plugins/datasource/azuremonitor/e2e/selectors'; import { - AzureDataSourceJsonData, - AzureDataSourceSecureJsonData, + AzureMonitorDataSourceJsonData, + AzureMonitorDataSourceSecureJsonData, AzureQueryType, } from '../../public/app/plugins/datasource/azuremonitor/types'; import { e2e } from '../utils'; @@ -16,8 +16,8 @@ const provisioningPath = `provisioning/datasources/azmonitor-ds.yaml`; const e2eSelectors = e2e.getSelectors(selectors.components); type AzureMonitorConfig = { - secureJsonData: AzureDataSourceSecureJsonData; - jsonData: AzureDataSourceJsonData; + secureJsonData: AzureMonitorDataSourceSecureJsonData; + jsonData: AzureMonitorDataSourceJsonData; }; type AzureMonitorProvision = { datasources: AzureMonitorConfig[] }; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 2dbc0e19f47..aa21d730c15 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1058,6 +1058,11 @@ export interface FeatureToggles { */ unifiedStorageHistoryPruner?: boolean; /** + * Enables the logs builder mode for the Azure Monitor data source + * @default false + */ + azureMonitorLogsBuilderEditor?: boolean; + /** * Specify the locale so we can show the correct format for numbers and dates */ localeFormatPreference?: boolean; diff --git a/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts index d90d2ba7b14..f6cf395b900 100644 --- a/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts @@ -184,6 +184,10 @@ export interface AzureLogsQuery { * If set to true the query will be run as a basic logs query */ basicLogsQuery?: boolean; + /** + * Builder query to be executed. + */ + builderQuery?: BuilderQueryExpression; /** * If set to true the dashboard time range will be used as a filter for the query. Otherwise the query time ranges will be used. Defaults to false. */ @@ -192,6 +196,10 @@ export interface AzureLogsQuery { * @deprecated Use dashboardTime instead */ intersectTime?: boolean; + /** + * Denotes if logs query editor is in builder mode + */ + mode?: LogsEditorMode; /** * KQL query to be executed. */ @@ -284,6 +292,162 @@ export enum ResultFormat { Trace = 'trace', } +export enum LogsEditorMode { + Builder = 'builder', + Raw = 'raw', +} + +export enum BuilderQueryEditorExpressionType { + And = 'and', + Function_parameter = 'function_parameter', + Group_by = 'group_by', + Operator = 'operator', + Or = 'or', + Order_by = 'order_by', + Property = 'property', + Reduce = 'reduce', +} + +export enum BuilderQueryEditorPropertyType { + Boolean = 'boolean', + Datetime = 'datetime', + Function = 'function', + Interval = 'interval', + Number = 'number', + String = 'string', + Time_span = 'time_span', +} + +export enum BuilderQueryEditorOrderByOptions { + Asc = 'asc', + Desc = 'desc', +} + +export interface BuilderQueryEditorProperty { + name: string; + type: BuilderQueryEditorPropertyType; +} + +export interface BuilderQueryEditorPropertyExpression { + property: BuilderQueryEditorProperty; + type: BuilderQueryEditorExpressionType; +} + +export interface BuilderQueryEditorColumnsExpression { + columns?: Array; + type: BuilderQueryEditorExpressionType; +} + +export const defaultBuilderQueryEditorColumnsExpression: Partial = { + columns: [], +}; + +export interface SelectableValue { + label: string; + value: string; +} + +export type BuilderQueryEditorOperatorType = (string | boolean | number | SelectableValue); + +export interface BuilderQueryEditorOperator { + labelValue?: string; + name: string; + value: string; +} + +export interface BuilderQueryEditorWhereExpressionItems { + operator: BuilderQueryEditorOperator; + property: BuilderQueryEditorProperty; + type: BuilderQueryEditorExpressionType; +} + +export interface BuilderQueryEditorWhereExpression { + expressions: Array; + type: BuilderQueryEditorExpressionType; +} + +export const defaultBuilderQueryEditorWhereExpression: Partial = { + expressions: [], +}; + +export interface BuilderQueryEditorWhereExpressionArray { + expressions: Array; + type: BuilderQueryEditorExpressionType; +} + +export const defaultBuilderQueryEditorWhereExpressionArray: Partial = { + expressions: [], +}; + +export interface BuilderQueryEditorFunctionParameterExpression { + fieldType: BuilderQueryEditorPropertyType; + type: BuilderQueryEditorExpressionType; + value: string; +} + +export interface BuilderQueryEditorReduceExpression { + focus?: boolean; + parameters?: Array; + property?: BuilderQueryEditorProperty; + reduce?: BuilderQueryEditorProperty; +} + +export const defaultBuilderQueryEditorReduceExpression: Partial = { + parameters: [], +}; + +export interface BuilderQueryEditorReduceExpressionArray { + expressions: Array; + type: BuilderQueryEditorExpressionType; +} + +export const defaultBuilderQueryEditorReduceExpressionArray: Partial = { + expressions: [], +}; + +export interface BuilderQueryEditorGroupByExpression { + focus?: boolean; + interval?: BuilderQueryEditorProperty; + property?: BuilderQueryEditorProperty; + type?: BuilderQueryEditorExpressionType; +} + +export interface BuilderQueryEditorGroupByExpressionArray { + expressions: Array; + type: BuilderQueryEditorExpressionType; +} + +export const defaultBuilderQueryEditorGroupByExpressionArray: Partial = { + expressions: [], +}; + +export interface BuilderQueryEditorOrderByExpression { + order: BuilderQueryEditorOrderByOptions; + property: BuilderQueryEditorProperty; + type: BuilderQueryEditorExpressionType; +} + +export interface BuilderQueryEditorOrderByExpressionArray { + expressions: Array; + type: BuilderQueryEditorExpressionType; +} + +export const defaultBuilderQueryEditorOrderByExpressionArray: Partial = { + expressions: [], +}; + +export interface BuilderQueryExpression { + columns?: BuilderQueryEditorColumnsExpression; + from?: BuilderQueryEditorPropertyExpression; + fuzzySearch?: BuilderQueryEditorWhereExpressionArray; + groupBy?: BuilderQueryEditorGroupByExpressionArray; + limit?: number; + orderBy?: BuilderQueryEditorOrderByExpressionArray; + reduce?: BuilderQueryEditorReduceExpressionArray; + timeFilter?: BuilderQueryEditorWhereExpressionArray; + where?: BuilderQueryEditorWhereExpressionArray; +} + export interface AzureResourceGraphQuery { /** * Azure Resource Graph KQL query to be executed. diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 7ed86e26451..33b38be4788 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1818,6 +1818,13 @@ var ( HideFromAdminPage: true, HideFromDocs: true, }, + { + Name: "azureMonitorLogsBuilderEditor", + Description: "Enables the logs builder mode for the Azure Monitor data source", + Stage: FeatureStagePublicPreview, + Owner: grafanaPartnerPluginsSquad, + Expression: "false", + }, { Name: "localeFormatPreference", Description: "Specify the locale so we can show the correct format for numbers and dates", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 51116246f25..a4bedb0661d 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -239,6 +239,7 @@ inviteUserExperimental,experimental,@grafana/sharing-squad,false,false,true noBackdropBlur,experimental,@grafana/grafana-frontend-platform,false,false,true alertingMigrationUI,experimental,@grafana/alerting-squad,false,false,true unifiedStorageHistoryPruner,experimental,@grafana/search-and-storage,false,false,false +azureMonitorLogsBuilderEditor,preview,@grafana/partner-datasources,false,false,false localeFormatPreference,experimental,@grafana/grafana-frontend-platform,false,false,false unifiedStorageGrpcConnectionPool,experimental,@grafana/search-and-storage,false,false,false alertingRuleRecoverDeleted,GA,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index da3ba572ea8..ee63c67fe37 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -967,6 +967,10 @@ const ( // Enables the unified storage history pruner FlagUnifiedStorageHistoryPruner = "unifiedStorageHistoryPruner" + // FlagAzureMonitorLogsBuilderEditor + // Enables the logs builder mode for the Azure Monitor data source + FlagAzureMonitorLogsBuilderEditor = "azureMonitorLogsBuilderEditor" + // FlagLocaleFormatPreference // Specify the locale so we can show the correct format for numbers and dates FlagLocaleFormatPreference = "localeFormatPreference" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 3173729f724..348dec9f4e7 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -901,6 +901,22 @@ "codeowner": "@grafana/partner-datasources" } }, + { + "metadata": { + "name": "azureMonitorLogsBuilderEditor", + "resourceVersion": "1743001017970", + "creationTimestamp": "2025-03-19T22:51:49Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-03-26 14:56:57.970732 +0000 UTC" + } + }, + "spec": { + "description": "Enables the logs builder mode for the Azure Monitor data source", + "stage": "preview", + "codeowner": "@grafana/partner-datasources", + "expression": "false" + } + }, { "metadata": { "name": "azureMonitorPrometheusExemplars", diff --git a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go index 2736e023506..174199c4fd7 100644 --- a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go @@ -157,6 +157,10 @@ type AzureLogsQuery struct { BasicLogsQuery *bool `json:"basicLogsQuery,omitempty"` // Workspace ID. This was removed in Grafana 8, but remains for backwards compat. Workspace *string `json:"workspace,omitempty"` + // Denotes if logs query editor is in builder mode + Mode *LogsEditorMode `json:"mode,omitempty"` + // Builder query to be executed. + BuilderQuery *BuilderQueryExpression `json:"builderQuery,omitempty"` // @deprecated Use resources instead Resource *string `json:"resource,omitempty"` // @deprecated Use dashboardTime instead @@ -177,6 +181,217 @@ const ( ResultFormatLogs ResultFormat = "logs" ) +type LogsEditorMode string + +const ( + LogsEditorModeBuilder LogsEditorMode = "builder" + LogsEditorModeRaw LogsEditorMode = "raw" +) + +type BuilderQueryExpression struct { + From *BuilderQueryEditorPropertyExpression `json:"from,omitempty"` + Columns *BuilderQueryEditorColumnsExpression `json:"columns,omitempty"` + Where *BuilderQueryEditorWhereExpressionArray `json:"where,omitempty"` + Reduce *BuilderQueryEditorReduceExpressionArray `json:"reduce,omitempty"` + GroupBy *BuilderQueryEditorGroupByExpressionArray `json:"groupBy,omitempty"` + Limit *int64 `json:"limit,omitempty"` + OrderBy *BuilderQueryEditorOrderByExpressionArray `json:"orderBy,omitempty"` + FuzzySearch *BuilderQueryEditorWhereExpressionArray `json:"fuzzySearch,omitempty"` + TimeFilter *BuilderQueryEditorWhereExpressionArray `json:"timeFilter,omitempty"` +} + +// NewBuilderQueryExpression creates a new BuilderQueryExpression object. +func NewBuilderQueryExpression() *BuilderQueryExpression { + return &BuilderQueryExpression{} +} + +type BuilderQueryEditorPropertyExpression struct { + Property BuilderQueryEditorProperty `json:"property"` + Type BuilderQueryEditorExpressionType `json:"type"` +} + +// NewBuilderQueryEditorPropertyExpression creates a new BuilderQueryEditorPropertyExpression object. +func NewBuilderQueryEditorPropertyExpression() *BuilderQueryEditorPropertyExpression { + return &BuilderQueryEditorPropertyExpression{ + Property: *NewBuilderQueryEditorProperty(), + } +} + +type BuilderQueryEditorProperty struct { + Type BuilderQueryEditorPropertyType `json:"type"` + Name string `json:"name"` +} + +// NewBuilderQueryEditorProperty creates a new BuilderQueryEditorProperty object. +func NewBuilderQueryEditorProperty() *BuilderQueryEditorProperty { + return &BuilderQueryEditorProperty{} +} + +type BuilderQueryEditorPropertyType string + +const ( + BuilderQueryEditorPropertyTypeNumber BuilderQueryEditorPropertyType = "number" + BuilderQueryEditorPropertyTypeString BuilderQueryEditorPropertyType = "string" + BuilderQueryEditorPropertyTypeBoolean BuilderQueryEditorPropertyType = "boolean" + BuilderQueryEditorPropertyTypeDatetime BuilderQueryEditorPropertyType = "datetime" + BuilderQueryEditorPropertyTypeTimeSpan BuilderQueryEditorPropertyType = "time_span" + BuilderQueryEditorPropertyTypeFunction BuilderQueryEditorPropertyType = "function" + BuilderQueryEditorPropertyTypeInterval BuilderQueryEditorPropertyType = "interval" +) + +type BuilderQueryEditorExpressionType string + +const ( + BuilderQueryEditorExpressionTypeProperty BuilderQueryEditorExpressionType = "property" + BuilderQueryEditorExpressionTypeOperator BuilderQueryEditorExpressionType = "operator" + BuilderQueryEditorExpressionTypeReduce BuilderQueryEditorExpressionType = "reduce" + BuilderQueryEditorExpressionTypeFunctionParameter BuilderQueryEditorExpressionType = "function_parameter" + BuilderQueryEditorExpressionTypeGroupBy BuilderQueryEditorExpressionType = "group_by" + BuilderQueryEditorExpressionTypeOr BuilderQueryEditorExpressionType = "or" + BuilderQueryEditorExpressionTypeAnd BuilderQueryEditorExpressionType = "and" + BuilderQueryEditorExpressionTypeOrderBy BuilderQueryEditorExpressionType = "order_by" +) + +type BuilderQueryEditorColumnsExpression struct { + Columns []string `json:"columns,omitempty"` + Type BuilderQueryEditorExpressionType `json:"type"` +} + +// NewBuilderQueryEditorColumnsExpression creates a new BuilderQueryEditorColumnsExpression object. +func NewBuilderQueryEditorColumnsExpression() *BuilderQueryEditorColumnsExpression { + return &BuilderQueryEditorColumnsExpression{} +} + +type BuilderQueryEditorWhereExpressionArray struct { + Expressions []BuilderQueryEditorWhereExpression `json:"expressions"` + Type BuilderQueryEditorExpressionType `json:"type"` +} + +// NewBuilderQueryEditorWhereExpressionArray creates a new BuilderQueryEditorWhereExpressionArray object. +func NewBuilderQueryEditorWhereExpressionArray() *BuilderQueryEditorWhereExpressionArray { + return &BuilderQueryEditorWhereExpressionArray{} +} + +type BuilderQueryEditorWhereExpression struct { + Type BuilderQueryEditorExpressionType `json:"type"` + Expressions []BuilderQueryEditorWhereExpressionItems `json:"expressions"` +} + +// NewBuilderQueryEditorWhereExpression creates a new BuilderQueryEditorWhereExpression object. +func NewBuilderQueryEditorWhereExpression() *BuilderQueryEditorWhereExpression { + return &BuilderQueryEditorWhereExpression{} +} + +type BuilderQueryEditorWhereExpressionItems struct { + Property BuilderQueryEditorProperty `json:"property"` + Operator BuilderQueryEditorOperator `json:"operator"` + Type BuilderQueryEditorExpressionType `json:"type"` +} + +// NewBuilderQueryEditorWhereExpressionItems creates a new BuilderQueryEditorWhereExpressionItems object. +func NewBuilderQueryEditorWhereExpressionItems() *BuilderQueryEditorWhereExpressionItems { + return &BuilderQueryEditorWhereExpressionItems{ + Property: *NewBuilderQueryEditorProperty(), + Operator: *NewBuilderQueryEditorOperator(), + } +} + +type BuilderQueryEditorOperator struct { + Name string `json:"name"` + Value string `json:"value"` + LabelValue *string `json:"labelValue,omitempty"` +} + +// NewBuilderQueryEditorOperator creates a new BuilderQueryEditorOperator object. +func NewBuilderQueryEditorOperator() *BuilderQueryEditorOperator { + return &BuilderQueryEditorOperator{} +} + +type BuilderQueryEditorReduceExpressionArray struct { + Expressions []BuilderQueryEditorReduceExpression `json:"expressions"` + Type BuilderQueryEditorExpressionType `json:"type"` +} + +// NewBuilderQueryEditorReduceExpressionArray creates a new BuilderQueryEditorReduceExpressionArray object. +func NewBuilderQueryEditorReduceExpressionArray() *BuilderQueryEditorReduceExpressionArray { + return &BuilderQueryEditorReduceExpressionArray{} +} + +type BuilderQueryEditorReduceExpression struct { + Property *BuilderQueryEditorProperty `json:"property,omitempty"` + Reduce *BuilderQueryEditorProperty `json:"reduce,omitempty"` + Parameters []BuilderQueryEditorFunctionParameterExpression `json:"parameters,omitempty"` + Focus *bool `json:"focus,omitempty"` +} + +// NewBuilderQueryEditorReduceExpression creates a new BuilderQueryEditorReduceExpression object. +func NewBuilderQueryEditorReduceExpression() *BuilderQueryEditorReduceExpression { + return &BuilderQueryEditorReduceExpression{} +} + +type BuilderQueryEditorFunctionParameterExpression struct { + Value string `json:"value"` + FieldType BuilderQueryEditorPropertyType `json:"fieldType"` + Type BuilderQueryEditorExpressionType `json:"type"` +} + +// NewBuilderQueryEditorFunctionParameterExpression creates a new BuilderQueryEditorFunctionParameterExpression object. +func NewBuilderQueryEditorFunctionParameterExpression() *BuilderQueryEditorFunctionParameterExpression { + return &BuilderQueryEditorFunctionParameterExpression{} +} + +type BuilderQueryEditorGroupByExpressionArray struct { + Expressions []BuilderQueryEditorGroupByExpression `json:"expressions"` + Type BuilderQueryEditorExpressionType `json:"type"` +} + +// NewBuilderQueryEditorGroupByExpressionArray creates a new BuilderQueryEditorGroupByExpressionArray object. +func NewBuilderQueryEditorGroupByExpressionArray() *BuilderQueryEditorGroupByExpressionArray { + return &BuilderQueryEditorGroupByExpressionArray{} +} + +type BuilderQueryEditorGroupByExpression struct { + Property *BuilderQueryEditorProperty `json:"property,omitempty"` + Interval *BuilderQueryEditorProperty `json:"interval,omitempty"` + Focus *bool `json:"focus,omitempty"` + Type *BuilderQueryEditorExpressionType `json:"type,omitempty"` +} + +// NewBuilderQueryEditorGroupByExpression creates a new BuilderQueryEditorGroupByExpression object. +func NewBuilderQueryEditorGroupByExpression() *BuilderQueryEditorGroupByExpression { + return &BuilderQueryEditorGroupByExpression{} +} + +type BuilderQueryEditorOrderByExpressionArray struct { + Expressions []BuilderQueryEditorOrderByExpression `json:"expressions"` + Type BuilderQueryEditorExpressionType `json:"type"` +} + +// NewBuilderQueryEditorOrderByExpressionArray creates a new BuilderQueryEditorOrderByExpressionArray object. +func NewBuilderQueryEditorOrderByExpressionArray() *BuilderQueryEditorOrderByExpressionArray { + return &BuilderQueryEditorOrderByExpressionArray{} +} + +type BuilderQueryEditorOrderByExpression struct { + Property BuilderQueryEditorProperty `json:"property"` + Order BuilderQueryEditorOrderByOptions `json:"order"` + Type BuilderQueryEditorExpressionType `json:"type"` +} + +// NewBuilderQueryEditorOrderByExpression creates a new BuilderQueryEditorOrderByExpression object. +func NewBuilderQueryEditorOrderByExpression() *BuilderQueryEditorOrderByExpression { + return &BuilderQueryEditorOrderByExpression{ + Property: *NewBuilderQueryEditorProperty(), + } +} + +type BuilderQueryEditorOrderByOptions string + +const ( + BuilderQueryEditorOrderByOptionsAsc BuilderQueryEditorOrderByOptions = "asc" + BuilderQueryEditorOrderByOptionsDesc BuilderQueryEditorOrderByOptions = "desc" +) + type AzureResourceGraphQuery struct { // Azure Resource Graph KQL query to be executed. Query *string `json:"query,omitempty"` @@ -391,6 +606,23 @@ const ( AzureQueryTypeCustomMetricNamesQuery AzureQueryType = "Azure Custom Metric Names" ) +type SelectableValue struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// NewSelectableValue creates a new SelectableValue object. +func NewSelectableValue() *SelectableValue { + return &SelectableValue{} +} + +type BuilderQueryEditorOperatorType = StringOrBoolOrFloat64OrSelectableValue + +// NewBuilderQueryEditorOperatorType creates a new BuilderQueryEditorOperatorType object. +func NewBuilderQueryEditorOperatorType() *BuilderQueryEditorOperatorType { + return NewStringOrBoolOrFloat64OrSelectableValue() +} + type GrafanaTemplateVariableQueryType string const ( @@ -569,3 +801,15 @@ func (resource *AppInsightsMetricNameQueryOrAppInsightsGroupByQueryOrSubscriptio return fmt.Errorf("could not unmarshal resource with `kind = %v`", discriminator) } + +type StringOrBoolOrFloat64OrSelectableValue struct { + String *string `json:"String,omitempty"` + Bool *bool `json:"Bool,omitempty"` + Float64 *float64 `json:"Float64,omitempty"` + SelectableValue *SelectableValue `json:"SelectableValue,omitempty"` +} + +// NewStringOrBoolOrFloat64OrSelectableValue creates a new StringOrBoolOrFloat64OrSelectableValue object. +func NewStringOrBoolOrFloat64OrSelectableValue() *StringOrBoolOrFloat64OrSelectableValue { + return &StringOrBoolOrFloat64OrSelectableValue{} +} diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go index fa8e18809b7..885463e69d3 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go @@ -28,6 +28,18 @@ import ( "github.com/grafana/grafana/pkg/tsdb/azuremonitor/utils" ) +// Returns tables with the `HasData` field set to true +func filterTablesWithData(tables []types.MetadataTable) []types.MetadataTable { + filtered := []types.MetadataTable{} + for _, table := range tables { + if table.HasData { + filtered = append(filtered, table) + } + } + + return filtered +} + func (e *AzureLogAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) (http.ResponseWriter, error) { if req.URL.Path == "/usage/basiclogs" { newUrl := &url.URL{ @@ -36,7 +48,57 @@ func (e *AzureLogAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, re Path: "/v1/query", } return e.GetBasicLogsUsage(req.Context(), newUrl.String(), cli, rw, req.Body) + } else if strings.Contains(req.URL.Path, "/metadata") { + // Add necessary headers + req.Header.Set("Prefer", "metadata-format-v4,exclude-resourcetypes,exclude-customfunctions") + queryParams := req.URL.Query() + // Add necessary query params + queryParams.Add("select", "categories,solutions,tables,workspaces") + req.URL.RawQuery = queryParams.Encode() + resp, err := cli.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch metadata: %w", err) + } + + defer func() { + if err := resp.Body.Close(); err != nil { + e.Logger.Warn("Failed to close response body for metadata request", "err", err) + } + }() + + encoding := resp.Header.Get("Content-Encoding") + body, err := decode(encoding, resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read metadata response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("metadata API error: %s", string(body)) + } + + var metadata types.AzureLogAnalyticsMetadata + err = json.Unmarshal(body, &metadata) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal metadata response: %w", err) + } + metadata.Tables = filterTablesWithData(metadata.Tables) + + responseBody, err := json.Marshal(metadata) + if err != nil { + return nil, fmt.Errorf("failed to marshal metadata response: %w", err) + } + + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(http.StatusOK) + _, err = rw.Write(responseBody) + if err != nil { + return nil, fmt.Errorf("failed to write metadata response: %w", err) + } + + return rw, nil } + + // Default behavior for other requests return e.Proxy.Do(rw, req, cli) } diff --git a/pkg/tsdb/azuremonitor/loganalytics/utils.go b/pkg/tsdb/azuremonitor/loganalytics/utils.go index 435f683fe40..a22ced2c24b 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/utils.go +++ b/pkg/tsdb/azuremonitor/loganalytics/utils.go @@ -1,12 +1,16 @@ package loganalytics import ( + "compress/flate" + "compress/gzip" "fmt" + "io" "regexp" "strconv" "strings" "time" + "github.com/andybalholm/brotli" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery" @@ -136,3 +140,40 @@ func ConvertTime(timeStamp string) (time.Time, error) { func GetDataVolumeRawQuery(table string) string { return fmt.Sprintf("Usage \n| where DataType == \"%s\"\n| where IsBillable == true\n| summarize BillableDataGB = round(sum(Quantity) / 1000, 3)", table) } + +// This function handles various compression mechanisms that may have been used on a response body +func decode(encoding string, original io.ReadCloser) ([]byte, error) { + var reader io.Reader + var err error + switch encoding { + case "gzip": + reader, err = gzip.NewReader(original) + if err != nil { + return nil, err + } + defer func() { + if err := reader.(io.ReadCloser).Close(); err != nil { + backend.Logger.Warn("Failed to close reader body", "err", err) + } + }() + case "deflate": + reader = flate.NewReader(original) + defer func() { + if err := reader.(io.ReadCloser).Close(); err != nil { + backend.Logger.Warn("Failed to close reader body", "err", err) + } + }() + case "br": + reader = brotli.NewReader(original) + case "": + reader = original + default: + return nil, fmt.Errorf("unexpected encoding type %v", err) + } + + body, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + return body, nil +} diff --git a/pkg/tsdb/azuremonitor/types/types.go b/pkg/tsdb/azuremonitor/types/types.go index c0fd5b99095..2b037aad764 100644 --- a/pkg/tsdb/azuremonitor/types/types.go +++ b/pkg/tsdb/azuremonitor/types/types.go @@ -210,3 +210,119 @@ type SubscriptionsResponse struct { } var ErrorAzureHealthCheck = errors.New("health check failed") + +// AzureLogAnalyticsMetadata represents the metadata response from the Azure Log Analytics API. +// Types are taken from https://learn.microsoft.com/en-us/rest/api/loganalytics/metadata/get +type AzureLogAnalyticsMetadata struct { + Applications []MetadataApplications `json:"applications"` + Categories []MetadataCategories `json:"categories"` + Functions []MetadataFunctions `json:"functions"` + Permissions []MetadataPermissions `json:"permissions"` + Queries []MetadataQueries `json:"queries"` + ResourceTypes []MetadataResourceTypes `json:"resourceTypes"` + Resources []Resources `json:"resources"` + Solutions []MetadataSolutions `json:"solutions"` + Tables []MetadataTable `json:"tables"` + Workspaces []MetadataWorkspaces `json:"workspaces"` +} + +// MetadataTable represents a table entry in the metadata response. +type MetadataTable struct { + Columns []Columns `json:"columns"` + Description string `json:"description"` + ID string `json:"id"` + Labels []string `json:"labels"` + Name string `json:"name"` + Properties map[string]any `json:"properties"` + Tags map[string]any `json:"tags"` + TimespanColumn string `json:"timespanColumn"` + TableType string `json:"tableType"` + HasData bool `json:"hasData"` +} + +type Columns struct { + Description string `json:"description"` + IsPreferredFacet bool `json:"isPreferredFacet"` + Name string `json:"name"` + Source map[string]any `json:"source"` + Type string `json:"type"` +} + +type MetadataApplications struct { + ID string `json:"id"` + Name string `json:"name"` + Region string `json:"region"` + ResourceId string `json:"resourceId"` +} + +type MetadataWorkspaces struct { + ID string `json:"id"` + Name string `json:"name"` + Region string `json:"region"` + ResourceId string `json:"resourceId"` +} + +type MetadataCategories struct { + Description string `json:"description"` + DisplayName string `json:"displayName"` + ID string `json:"id"` +} + +type MetadataFunctions struct { + Body string `json:"string"` + Description string `json:"description"` + DisplayName string `json:"displayName"` + ID string `json:"id"` + Name string `json:"name"` + Parameters string `json:"parameters"` + Properties map[string]any `json:"properties"` + Tags map[string]any `json:"tags"` +} + +type MetadataPermissions struct { + Applications []Applications `json:"applications"` + Resources []Resources `json:"resources"` + Workspaces []Workspaces `json:"workspaces"` +} + +type MetadataQueries struct { + Body string `json:"string"` + Description string `json:"description"` + DisplayName string `json:"displayName"` + ID string `json:"id"` + Labels []string `json:"labels"` + Properties map[string]any `json:"properties"` + Tags map[string]any `json:"tags"` +} + +type MetadataResourceTypes struct { + Description string `json:"description"` + DisplayName string `json:"displayName"` + ID string `json:"id"` + Labels []string `json:"labels"` + Properties map[string]any `json:"properties"` + Tags map[string]any `json:"tags"` + Type string `json:"type"` +} + +type MetadataSolutions struct { + Description string `json:"description"` + DisplayName string `json:"displayName"` + ID string `json:"id"` + Name string `json:"name"` + Properties map[string]any `json:"properties"` + Tags map[string]any `json:"tags"` +} + +type Applications struct { + ResourceId string `json:"resourceId"` +} + +type Resources struct { + DenyTables []string `json:"denyTables"` + ResourceId string `json:"resourceId"` +} +type Workspaces struct { + DenyTables []string `json:"denyTables"` + ResourceId string `json:"resourceId"` +} diff --git a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts index f8dfad2b8e5..cb08deeafcc 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts @@ -8,9 +8,9 @@ import ResponseParser from '../azure_monitor/response_parser'; import { getCredentials } from '../credentials'; import { AzureAPIResponse, + AzureLogsVariable, AzureMonitorDataSourceInstanceSettings, AzureMonitorDataSourceJsonData, - AzureLogsVariable, AzureMonitorQuery, AzureQueryType, DatasourceValidationResult, @@ -125,6 +125,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< queryType: target.queryType || AzureQueryType.LogAnalytics, azureLogAnalytics: { + builderQuery: item.builderQuery, resultFormat: item.resultFormat, query, resources, diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/AggregateItem.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/AggregateItem.tsx new file mode 100644 index 00000000000..9f59b4439c0 --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/AggregateItem.tsx @@ -0,0 +1,152 @@ +import React, { useState } from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { InputGroup, AccessoryButton } from '@grafana/plugin-ui'; +import { Select, Label, Input } from '@grafana/ui'; + +import { + BuilderQueryEditorExpressionType, + BuilderQueryEditorPropertyType, + BuilderQueryEditorReduceExpression, +} from '../../dataquery.gen'; + +import { aggregateOptions, inputFieldSize } from './utils'; + +interface AggregateItemProps { + aggregate: BuilderQueryEditorReduceExpression; + columns: Array>; + onChange: (item: BuilderQueryEditorReduceExpression) => void; + onDelete: () => void; + templateVariableOptions: SelectableValue; +} + +const AggregateItem: React.FC = ({ + aggregate, + onChange, + onDelete, + columns, + templateVariableOptions, +}) => { + const isPercentile = aggregate.reduce?.name === 'percentile'; + const isCountAggregate = aggregate.reduce?.name?.includes('count'); + + const [percentileValue, setPercentileValue] = useState(aggregate.parameters?.[0]?.value || ''); + const [columnValue, setColumnValue] = useState( + isPercentile ? aggregate.parameters?.[1]?.value || '' : aggregate.property?.name || '' + ); + + const safeTemplateVariables = Array.isArray(templateVariableOptions) + ? templateVariableOptions + : [templateVariableOptions]; + + const selectableOptions = columns.concat(safeTemplateVariables); + + const buildPercentileParams = (percentile: string, column: string) => [ + { + type: BuilderQueryEditorExpressionType.Function_parameter, + fieldType: BuilderQueryEditorPropertyType.Number, + value: percentile, + }, + { + type: BuilderQueryEditorExpressionType.Function_parameter, + fieldType: BuilderQueryEditorPropertyType.String, + value: column, + }, + ]; + + const updateAggregate = (updates: Partial) => { + const base: BuilderQueryEditorReduceExpression = { + ...aggregate, + ...updates, + }; + + onChange(base); + }; + + const handleAggregateChange = (funcName?: string) => { + updateAggregate({ + reduce: { name: funcName || '', type: BuilderQueryEditorPropertyType.Function }, + }); + }; + + const handlePercentileChange = (value?: string) => { + const newValue = value || ''; + setPercentileValue(newValue); + + const percentileParams = buildPercentileParams(newValue, columnValue); + updateAggregate({ parameters: percentileParams }); + }; + + const handleColumnChange = (value?: string) => { + const newCol = value || ''; + setColumnValue(newCol); + + if (isPercentile) { + const percentileParams = buildPercentileParams(percentileValue, newCol); + updateAggregate({ + parameters: percentileParams, + property: { + name: newCol, + type: BuilderQueryEditorPropertyType.String, + }, + }); + } else { + updateAggregate({ + property: { + name: newCol, + type: BuilderQueryEditorPropertyType.String, + }, + }); + } + }; + + return ( + + { + const val = Number(e.currentTarget.value); + if (!isNaN(val) && val >= 0 && val <= 100) { + handlePercentileChange(val.toString()); + } + }} + /> + + + ) : ( + <> + )} + + {!isCountAggregate ? ( + !usedColumns.includes(opt.value!))} + onChange={(e) => e.value && onChange(groupIndex, 'property', e.value, filterIndex)} + /> + updateFuzzySearch(selectedColumn, e.currentTarget.value)} + /> + + + + ); +}; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/GroupBySection.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/GroupBySection.tsx new file mode 100644 index 00000000000..11bf30f6354 --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/GroupBySection.tsx @@ -0,0 +1,150 @@ +import React, { useEffect, useState, useRef } from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { EditorField, EditorFieldGroup, EditorList, EditorRow, InputGroup } from '@grafana/plugin-ui'; +import { Button } from '@grafana/ui'; + +import { + BuilderQueryEditorExpressionType, + BuilderQueryEditorGroupByExpression, + BuilderQueryEditorPropertyType, +} from '../../dataquery.gen'; +import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; + +import { GroupByItem } from './GroupByItem'; +import { BuildAndUpdateOptions } from './utils'; + +interface GroupBySectionProps { + query: AzureMonitorQuery; + allColumns: AzureLogAnalyticsMetadataColumn[]; + buildAndUpdateQuery: (options: Partial) => void; + templateVariableOptions: SelectableValue; +} + +export const GroupBySection: React.FC = ({ + query, + buildAndUpdateQuery, + allColumns, + templateVariableOptions, +}) => { + const builderQuery = query.azureLogAnalytics?.builderQuery; + const prevTable = useRef(builderQuery?.from?.property.name || null); + const [groupBys, setGroupBys] = useState( + builderQuery?.groupBy?.expressions || [] + ); + + const hasLoadedGroupBy = useRef(false); + + useEffect(() => { + const currentTable = builderQuery?.from?.property.name || null; + + if (prevTable.current !== currentTable || builderQuery?.groupBy?.expressions.length === 0) { + setGroupBys([]); + hasLoadedGroupBy.current = false; + prevTable.current = currentTable; + } + }, [builderQuery]); + + const availableColumns: Array> = []; + const columns = builderQuery?.columns?.columns ?? []; + + if (columns.length > 0) { + availableColumns.push( + ...columns.map((col) => ({ + label: col, + value: col, + })) + ); + } else { + availableColumns.push( + ...allColumns.map((col) => ({ + label: col.name, + value: col.name, + })) + ); + } + + const handleGroupByChange = (newItems: Array>) => { + setGroupBys(newItems); + + buildAndUpdateQuery({ + groupBy: newItems, + }); + }; + + const onDeleteGroupBy = (propertyName: string) => { + setGroupBys((prevGroupBys) => { + const updatedGroupBys = prevGroupBys.filter((gb) => gb.property?.name !== propertyName); + + buildAndUpdateQuery({ + groupBy: updatedGroupBys, + }); + + return updatedGroupBys; + }); + }; + + return ( + + + + + {groupBys.length > 0 ? ( + + ) : ( + + + + + + + + ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + codeBlock: css({ + width: '100%', + display: 'table', + tableLayout: 'fixed', + }), + code: css({ + marginBottom: '4px', + }), + }; +}; + +export default KQLPreview; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LimitSection.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LimitSection.tsx new file mode 100644 index 00000000000..deea6a23396 --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LimitSection.tsx @@ -0,0 +1,41 @@ +import { useState } from 'react'; + +import { EditorRow, EditorFieldGroup, EditorField } from '@grafana/plugin-ui'; +import { Input } from '@grafana/ui'; + +import { BuildAndUpdateOptions } from './utils'; + +interface LimitSectionProps { + buildAndUpdateQuery: (options: Partial) => void; +} + +export const LimitSection: React.FC = (props) => { + const { buildAndUpdateQuery } = props; + const [limit, setLimit] = useState(1000); + + const handleQueryLimitUpdate = (newLimit: number) => { + buildAndUpdateQuery({ + limit: newLimit, + }); + }; + + return ( + + + + ) => { + const newValue = e.target.value.replace(/[^0-9]/g, ''); + setLimit(Number(newValue)); + handleQueryLimitUpdate(Number(newValue)); + }} + /> + + + + ); +}; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LogsQueryBuilder.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LogsQueryBuilder.tsx new file mode 100644 index 00000000000..fe9624ed5ef --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LogsQueryBuilder.tsx @@ -0,0 +1,158 @@ +import React, { useMemo, useState, useCallback } from 'react'; + +import { SelectableValue, TimeRange } from '@grafana/data'; +import { EditorRows } from '@grafana/plugin-ui'; +import { Alert } from '@grafana/ui'; + +import { + BuilderQueryEditorExpressionType, + BuilderQueryEditorPropertyType, + BuilderQueryEditorReduceExpression, + BuilderQueryEditorWhereExpression, + BuilderQueryEditorGroupByExpression, + BuilderQueryEditorOrderByExpression, + BuilderQueryEditorPropertyExpression, + BuilderQueryExpression, +} from '../../dataquery.gen'; +import Datasource from '../../datasource'; +import { selectors } from '../../e2e/selectors'; +import { + AzureLogAnalyticsMetadataTable, + AzureLogAnalyticsMetadataColumn, + AzureMonitorQuery, + EngineSchema, +} from '../../types'; + +import { AggregateSection } from './AggregationSection'; +import { AzureMonitorKustoQueryBuilder } from './AzureMonitorKustoQueryBuilder'; +import { FilterSection } from './FilterSection'; +import { FuzzySearch } from './FuzzySearch'; +import { GroupBySection } from './GroupBySection'; +import KQLPreview from './KQLPreview'; +import { LimitSection } from './LimitSection'; +import { OrderBySection } from './OrderBySection'; +import { TableSection } from './TableSection'; +import { DEFAULT_LOGS_BUILDER_QUERY } from './utils'; + +interface LogsQueryBuilderProps { + query: AzureMonitorQuery; + basicLogsEnabled: boolean; + onQueryChange: (newQuery: AzureMonitorQuery) => void; + schema: EngineSchema; + templateVariableOptions: SelectableValue; + datasource: Datasource; + timeRange?: TimeRange; +} + +export const LogsQueryBuilder: React.FC = (props) => { + const { query, onQueryChange, schema, datasource, timeRange } = props; + const [isKQLPreviewHidden, setIsKQLPreviewHidden] = useState(true); + + const tables: AzureLogAnalyticsMetadataTable[] = useMemo(() => { + return schema?.database?.tables || []; + }, [schema?.database]); + + const builderQuery: BuilderQueryExpression = query.azureLogAnalytics?.builderQuery || DEFAULT_LOGS_BUILDER_QUERY; + + const allColumns: AzureLogAnalyticsMetadataColumn[] = useMemo(() => { + const tableName = builderQuery.from?.property.name; + const selectedTable = tables.find((table) => table.name === tableName); + return selectedTable?.columns || []; + }, [builderQuery, tables]); + + const buildAndUpdateQuery = useCallback( + ({ + limit, + reduce, + where, + fuzzySearch, + groupBy, + orderBy, + columns, + from, + }: { + limit?: number; + reduce?: BuilderQueryEditorReduceExpression[]; + where?: BuilderQueryEditorWhereExpression[]; + fuzzySearch?: BuilderQueryEditorWhereExpression[]; + groupBy?: BuilderQueryEditorGroupByExpression[]; + orderBy?: BuilderQueryEditorOrderByExpression[]; + columns?: string[]; + from?: BuilderQueryEditorPropertyExpression; + }) => { + const datetimeColumn = allColumns.find((col) => col.type === 'datetime')?.name || 'TimeGenerated'; + + const timeFilterExpression: BuilderQueryEditorWhereExpression = { + type: BuilderQueryEditorExpressionType.Or, + expressions: [ + { + type: BuilderQueryEditorExpressionType.Operator, + operator: { name: '$__timeFilter', value: datetimeColumn }, + property: { name: datetimeColumn, type: BuilderQueryEditorPropertyType.Datetime }, + }, + ], + }; + + const updatedBuilderQuery: BuilderQueryExpression = { + ...builderQuery, + ...(limit !== undefined ? { limit } : {}), + ...(reduce !== undefined + ? { reduce: { expressions: reduce, type: BuilderQueryEditorExpressionType.Reduce } } + : {}), + ...(where !== undefined ? { where: { expressions: where, type: BuilderQueryEditorExpressionType.And } } : {}), + ...(fuzzySearch !== undefined + ? { fuzzySearch: { expressions: fuzzySearch, type: BuilderQueryEditorExpressionType.And } } + : {}), + ...(groupBy !== undefined + ? { groupBy: { expressions: groupBy, type: BuilderQueryEditorExpressionType.Group_by } } + : {}), + ...(orderBy !== undefined + ? { orderBy: { expressions: orderBy, type: BuilderQueryEditorExpressionType.Order_by } } + : {}), + ...(columns !== undefined ? { columns: { columns, type: BuilderQueryEditorExpressionType.Property } } : {}), + ...(from !== undefined ? { from } : {}), + timeFilter: { expressions: [timeFilterExpression], type: BuilderQueryEditorExpressionType.And }, + }; + + const updatedQueryString = AzureMonitorKustoQueryBuilder.toQuery(updatedBuilderQuery); + + onQueryChange({ + ...query, + azureLogAnalytics: { + ...query.azureLogAnalytics, + builderQuery: updatedBuilderQuery, + query: updatedQueryString, + }, + }); + }, + [query, builderQuery, onQueryChange, allColumns] + ); + + return ( + + + {schema && tables.length === 0 && ( + + )} + + + + + + + + + + ); +}; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.tsx new file mode 100644 index 00000000000..1bdb1db43b1 --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.tsx @@ -0,0 +1,158 @@ +import React, { useEffect, useRef, useState } from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { EditorField, EditorFieldGroup, EditorRow, InputGroup } from '@grafana/plugin-ui'; +import { Button, Select, Label } from '@grafana/ui'; + +import { + BuilderQueryEditorExpressionType, + BuilderQueryEditorOrderByExpression, + BuilderQueryEditorOrderByOptions, + BuilderQueryEditorPropertyType, +} from '../../dataquery.gen'; +import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; + +import { BuildAndUpdateOptions, inputFieldSize } from './utils'; + +interface OrderBySectionProps { + query: AzureMonitorQuery; + allColumns: AzureLogAnalyticsMetadataColumn[]; + buildAndUpdateQuery: (options: Partial) => void; +} + +export const OrderBySection: React.FC = ({ query, allColumns, buildAndUpdateQuery }) => { + const builderQuery = query.azureLogAnalytics?.builderQuery; + const prevTable = useRef(builderQuery?.from?.property.name || null); + const hasLoadedOrderBy = useRef(false); + + const [orderBy, setOrderBy] = useState( + builderQuery?.orderBy?.expressions || [] + ); + + useEffect(() => { + const currentTable = builderQuery?.from?.property.name || null; + + if (prevTable.current !== currentTable || builderQuery?.orderBy?.expressions.length === 0) { + setOrderBy([]); + hasLoadedOrderBy.current = false; + prevTable.current = currentTable; + } + }, [builderQuery]); + + const groupByColumns = builderQuery?.groupBy?.expressions?.map((g) => g.property?.name) || []; + const aggregateColumns = builderQuery?.reduce?.expressions?.map((r) => r.property?.name) || []; + const selectedColumns = builderQuery?.columns?.columns || []; + + const allAvailableColumns = + groupByColumns.length > 0 + ? groupByColumns + : aggregateColumns.length > 0 + ? aggregateColumns + : selectedColumns.length > 0 + ? selectedColumns + : allColumns.map((col) => col.name); + + const columnOptions = allAvailableColumns.map((col) => ({ + label: col, + value: col, + })); + + const orderOptions: Array> = [ + { label: 'Ascending', value: 'asc' }, + { label: 'Descending', value: 'desc' }, + ]; + + const handleOrderByChange = (index: number, key: 'column' | 'order', value: string) => { + setOrderBy((prev) => { + const updated = [...prev]; + + if (index === -1) { + updated.push({ + property: { name: value, type: BuilderQueryEditorPropertyType.String }, + order: BuilderQueryEditorOrderByOptions.Asc, + type: BuilderQueryEditorExpressionType.Order_by, + }); + } else { + updated[index] = { + ...updated[index], + property: + key === 'column' ? { name: value, type: BuilderQueryEditorPropertyType.String } : updated[index].property, + order: + key === 'order' && + (value === BuilderQueryEditorOrderByOptions.Asc || value === BuilderQueryEditorOrderByOptions.Desc) + ? value + : updated[index].order, + }; + } + + buildAndUpdateQuery({ + orderBy: updated, + }); + + return updated; + }); + }; + + const onDeleteOrderBy = (index: number) => { + setOrderBy((prev) => { + const updated = prev.filter((_, i) => i !== index); + + buildAndUpdateQuery({ + orderBy: updated, + }); + + return updated; + }); + }; + + return ( + + + + <> + {orderBy.length > 0 ? ( + orderBy.map((entry, index) => ( + + o.value === entry.order) || null} + options={orderOptions} + onChange={(e) => e.value && handleOrderByChange(index, 'order', e.value)} + /> + - )} - + - {errorMessage && ( <> @@ -146,6 +133,8 @@ interface EditorForQueryTypeProps extends Omit void; + // Used to update the query without running it + onQueryChange: (newQuery: AzureMonitorQuery) => void; } const EditorForQueryType = ({ @@ -157,6 +146,7 @@ const EditorForQueryType = ({ variableOptionGroup, onChange, setError, + onQueryChange, range, }: EditorForQueryTypeProps) => { switch (query.queryType) { @@ -184,6 +174,7 @@ const EditorForQueryType = ({ variableOptionGroup={variableOptionGroup} setError={setError} timeRange={range} + onQueryChange={onQueryChange} /> ); diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx index 7fddd79ce0c..60c7471dd45 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx @@ -1,17 +1,43 @@ -import { useCallback } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; -import { SelectableValue } from '@grafana/data'; -import { EditorHeader, InlineSelect } from '@grafana/plugin-ui'; +import { CoreApp, LoadingState, PanelData, SelectableValue } from '@grafana/data'; +import { EditorHeader, FlexItem, InlineSelect } from '@grafana/plugin-ui'; +import { config, reportInteraction } from '@grafana/runtime'; +import { Button, ConfirmModal, RadioButtonGroup } from '@grafana/ui'; +import { LogsEditorMode } from '../../dataquery.gen'; import { selectors } from '../../e2e/selectors'; import { AzureMonitorQuery, AzureQueryType } from '../../types'; interface QueryTypeFieldProps { query: AzureMonitorQuery; onQueryChange: (newQuery: AzureMonitorQuery) => void; + setAzureLogsCheatSheetModalOpen: React.Dispatch>; + data: PanelData | undefined; + onRunQuery: () => void; + app: CoreApp | undefined; } -export const QueryHeader = ({ query, onQueryChange }: QueryTypeFieldProps) => { +const EDITOR_MODES = [ + { label: 'Builder', value: LogsEditorMode.Builder }, + { label: 'KQL', value: LogsEditorMode.Raw }, +]; + +export const QueryHeader = ({ + query, + onQueryChange, + setAzureLogsCheatSheetModalOpen, + data, + app, + onRunQuery, +}: QueryTypeFieldProps) => { + const isLoading = useMemo(() => data?.state === LoadingState.Loading, [data?.state]); + + const [showModeSwitchWarning, setShowModeSwitchWarning] = useState(false); + const [pendingModeChange, setPendingModeChange] = useState(null); + + const currentMode = query.azureLogAnalytics?.mode; + const queryTypes: Array<{ value: AzureQueryType; label: string }> = [ { value: AzureQueryType.AzureMonitor, label: 'Metrics' }, { value: AzureQueryType.LogAnalytics, label: 'Logs' }, @@ -23,8 +49,7 @@ export const QueryHeader = ({ query, onQueryChange }: QueryTypeFieldProps) => { (change: SelectableValue) => { if (change.value && change.value !== query.queryType) { onQueryChange({ - refId: query.refId, - datasource: query.datasource, + ...query, queryType: change.value, }); } @@ -32,9 +57,76 @@ export const QueryHeader = ({ query, onQueryChange }: QueryTypeFieldProps) => { [onQueryChange, query] ); + useEffect(() => { + if (query.azureLogAnalytics && query.azureLogAnalytics.mode === undefined) { + const updatedQuery = { + ...query, + azureLogAnalytics: { + ...query.azureLogAnalytics, + mode: LogsEditorMode.Builder, + }, + }; + onQueryChange(updatedQuery); + } + }, [query, onQueryChange]); + + const onLogsModeChange = (newMode: LogsEditorMode) => { + if (newMode === currentMode) { + return; + } + + const goingToBuilder = newMode === LogsEditorMode.Builder; + const goingToRaw = newMode === LogsEditorMode.Raw; + + const hasRawKql = !!query.azureLogAnalytics?.query; + const hasBuilderQuery = !!query.azureLogAnalytics?.builderQuery; + + if ((goingToBuilder && hasRawKql) || (goingToRaw && hasBuilderQuery)) { + setPendingModeChange(newMode); + setShowModeSwitchWarning(true); + } else { + applyModeChange(newMode); + } + }; + + const applyModeChange = (mode: LogsEditorMode) => { + const updatedQuery = { + ...query, + azureLogAnalytics: { + ...query.azureLogAnalytics, + mode, + query: '', + builderQuery: mode === LogsEditorMode.Raw ? undefined : query.azureLogAnalytics?.builderQuery, + }, + }; + onQueryChange(updatedQuery); + }; + return ( + { + if (pendingModeChange) { + applyModeChange(pendingModeChange); + } + setShowModeSwitchWarning(false); + setPendingModeChange(null); + }} + onDismiss={() => { + setShowModeSwitchWarning(false); + setPendingModeChange(null); + }} + /> + { options={queryTypes} onChange={handleChange} /> + {query.queryType === AzureQueryType.LogAnalytics && query.azureLogAnalytics?.mode === LogsEditorMode.Raw && ( + + )} + + {query.queryType === AzureQueryType.LogAnalytics && !!config.featureToggles.azureMonitorLogsBuilderEditor && ( + + )} + {query.azureLogAnalytics?.mode === LogsEditorMode.Builder && + !!config.featureToggles.azureMonitorLogsBuilderEditor && + app !== CoreApp.Explore && ( + + )} ); diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx index 8f5b632c3b4..6042f2389d2 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx @@ -22,10 +22,18 @@ jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getTemplateSrv: () => ({ replace: (val: string) => { + if (val === '$ws') { + return '/subscriptions/def-456/resourceGroups/dev-3/providers/microsoft.operationalinsights/workspaces/la-workspace'; + } return val; }, + getVariables: () => [ + { name: 'var1', current: { value: 'value1' } }, + { name: 'var2', current: { value: 'value2' } }, + ], }), })); + const getResourceGroups = jest.fn().mockResolvedValue([{ resourceGroupURI: 'rg', resourceGroupName: 'rg', count: 1 }]); const getResourceNames = jest.fn().mockResolvedValue([ { @@ -103,10 +111,10 @@ describe('VariableEditor:', () => { render(); await waitFor(() => screen.queryByTestId('mockeditor')); expect(screen.queryByTestId('mockeditor')).toBeInTheDocument(); - await userEvent.type(screen.getByTestId('mockeditor'), '{backspace}'); + await userEvent.type(screen.getByTestId('mockeditor'), '2'); expect(onChange).toHaveBeenCalledWith({ azureLogAnalytics: { - query: 'test quer', + query: 'test query2', }, queryType: 'Azure Log Analytics', refId: 'A', diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx index d14fbbe0f83..f9a37a05b4b 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx @@ -291,6 +291,8 @@ const VariableEditor = (props: Props) => { query={query} datasource={datasource} onChange={onQueryChange} + // Not applicable as the builder isn't available in the variable editor yet + onQueryChange={onQueryChange} variableOptionGroup={variableOptionGroup} setError={setError} hideFormatAs={true} diff --git a/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx index fffcb6ba38f..cd4afbc168c 100644 --- a/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx @@ -50,7 +50,7 @@ const FormatAsField = ({ value={resultFormat} onChange={handleChange} options={options} - width={38} + width={20} /> ); diff --git a/public/app/plugins/datasource/azuremonitor/dataquery.cue b/public/app/plugins/datasource/azuremonitor/dataquery.cue index 32447a67a04..e2ba009fd3b 100644 --- a/public/app/plugins/datasource/azuremonitor/dataquery.cue +++ b/public/app/plugins/datasource/azuremonitor/dataquery.cue @@ -128,6 +128,10 @@ composableKinds: DataQuery: { basicLogsQuery?: bool // Workspace ID. This was removed in Grafana 8, but remains for backwards compat. workspace?: string + // Denotes if logs query editor is in builder mode + mode?: #LogsEditorMode + // Builder query to be executed. + builderQuery?: #BuilderQueryExpression // @deprecated Use resources instead resource?: string @@ -161,6 +165,108 @@ composableKinds: DataQuery: { } @cuetsy(kind="interface") #ResultFormat: "table" | "time_series" | "trace" | "logs" @cuetsy(kind="enum", memberNames="Table|TimeSeries|Trace|Logs") + #LogsEditorMode: "builder" | "raw" @cuetsy(kind="enum", memberNames="Builder|Raw") + + #BuilderQueryEditorExpressionType: "property" | "operator" | "reduce" | "function_parameter" | "group_by" | "or" | "and" | "order_by" @cuetsy(kind="enum", memberNames:"Property|Operator|Reduce|FunctionParameter|GroupBy|Or|And|OrderBy") + #BuilderQueryEditorPropertyType: "number" | "string" | "boolean" | "datetime" | "time_span" | "function" | "interval" @cuetsy(kind="enum", memberNames:"Number|String|Boolean|Datetime|TimeSpan|Function|Interval") + #BuilderQueryEditorOrderByOptions: "asc" | "desc" @cuetsy(kind="enum", memberNames:"Asc|Desc") + + #BuilderQueryEditorProperty: { + type: #BuilderQueryEditorPropertyType + name: string + } @cuetsy(kind="interface") + + #BuilderQueryEditorPropertyExpression: { + property: #BuilderQueryEditorProperty + type: #BuilderQueryEditorExpressionType + } @cuetsy(kind="interface") + + #BuilderQueryEditorColumnsExpression: { + columns?: [...string] + type: #BuilderQueryEditorExpressionType + } @cuetsy(kind="interface") + + #SelectableValue: { + label: string + value: string + } @cuetsy(kind="interface") + + #BuilderQueryEditorOperatorType: string | bool | number | #SelectableValue @cuetsy(kind="type") + + #BuilderQueryEditorOperator: { + name: string + value: string + labelValue?: string + } @cuetsy(kind="interface") + + #BuilderQueryEditorWhereExpressionItems: { + property: #BuilderQueryEditorProperty + operator: #BuilderQueryEditorOperator + type: #BuilderQueryEditorExpressionType + } @cuetsy(kind="interface") + + #BuilderQueryEditorWhereExpression: { + type: #BuilderQueryEditorExpressionType + expressions: [...#BuilderQueryEditorWhereExpressionItems] + } @cuetsy(kind="interface") + + #BuilderQueryEditorWhereExpressionArray: { + expressions: [...#BuilderQueryEditorWhereExpression] + type: #BuilderQueryEditorExpressionType + } @cuetsy(kind="interface") + + #BuilderQueryEditorFunctionParameterExpression: { + value: string + fieldType: #BuilderQueryEditorPropertyType + type: #BuilderQueryEditorExpressionType + } @cuetsy(kind="interface") + + #BuilderQueryEditorReduceExpression: { + property?: #BuilderQueryEditorProperty + reduce?: #BuilderQueryEditorProperty + parameters?: [...#BuilderQueryEditorFunctionParameterExpression] + focus?: bool + } @cuetsy(kind="interface") + + #BuilderQueryEditorReduceExpressionArray: { + expressions: [...#BuilderQueryEditorReduceExpression] + type: #BuilderQueryEditorExpressionType + } @cuetsy(kind="interface") + + #BuilderQueryEditorGroupByExpression: { + property?: #BuilderQueryEditorProperty + interval?: #BuilderQueryEditorProperty + focus?: bool + type?: #BuilderQueryEditorExpressionType + } @cuetsy(kind="interface") + + #BuilderQueryEditorGroupByExpressionArray: { + expressions: [...#BuilderQueryEditorGroupByExpression] + type: #BuilderQueryEditorExpressionType + } @cuetsy(kind="interface") + + #BuilderQueryEditorOrderByExpression: { + property: #BuilderQueryEditorProperty + order: #BuilderQueryEditorOrderByOptions + type: #BuilderQueryEditorExpressionType + } @cuetsy(kind="interface") + + #BuilderQueryEditorOrderByExpressionArray: { + expressions: [...#BuilderQueryEditorOrderByExpression] + type: #BuilderQueryEditorExpressionType + } @cuetsy(kind="interface") + + #BuilderQueryExpression: { + from?: #BuilderQueryEditorPropertyExpression + columns?: #BuilderQueryEditorColumnsExpression + where?: #BuilderQueryEditorWhereExpressionArray + reduce?: #BuilderQueryEditorReduceExpressionArray + groupBy?: #BuilderQueryEditorGroupByExpressionArray + limit?: int + orderBy?: #BuilderQueryEditorOrderByExpressionArray + fuzzySearch?: #BuilderQueryEditorWhereExpressionArray + timeFilter?: #BuilderQueryEditorWhereExpressionArray + } @cuetsy(kind="interface") #AzureResourceGraphQuery: { // Azure Resource Graph KQL query to be executed. diff --git a/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts b/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts index 06447a340b6..6a78a43d056 100644 --- a/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts +++ b/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts @@ -182,6 +182,10 @@ export interface AzureLogsQuery { * If set to true the query will be run as a basic logs query */ basicLogsQuery?: boolean; + /** + * Builder query to be executed. + */ + builderQuery?: BuilderQueryExpression; /** * If set to true the dashboard time range will be used as a filter for the query. Otherwise the query time ranges will be used. Defaults to false. */ @@ -190,6 +194,10 @@ export interface AzureLogsQuery { * @deprecated Use dashboardTime instead */ intersectTime?: boolean; + /** + * Denotes if logs query editor is in builder mode + */ + mode?: LogsEditorMode; /** * KQL query to be executed. */ @@ -282,6 +290,162 @@ export enum ResultFormat { Trace = 'trace', } +export enum LogsEditorMode { + Builder = 'builder', + Raw = 'raw', +} + +export enum BuilderQueryEditorExpressionType { + And = 'and', + Function_parameter = 'function_parameter', + Group_by = 'group_by', + Operator = 'operator', + Or = 'or', + Order_by = 'order_by', + Property = 'property', + Reduce = 'reduce', +} + +export enum BuilderQueryEditorPropertyType { + Boolean = 'boolean', + Datetime = 'datetime', + Function = 'function', + Interval = 'interval', + Number = 'number', + String = 'string', + Time_span = 'time_span', +} + +export enum BuilderQueryEditorOrderByOptions { + Asc = 'asc', + Desc = 'desc', +} + +export interface BuilderQueryEditorProperty { + name: string; + type: BuilderQueryEditorPropertyType; +} + +export interface BuilderQueryEditorPropertyExpression { + property: BuilderQueryEditorProperty; + type: BuilderQueryEditorExpressionType; +} + +export interface BuilderQueryEditorColumnsExpression { + columns?: Array; + type: BuilderQueryEditorExpressionType; +} + +export const defaultBuilderQueryEditorColumnsExpression: Partial = { + columns: [], +}; + +export interface SelectableValue { + label: string; + value: string; +} + +export type BuilderQueryEditorOperatorType = (string | boolean | number | SelectableValue); + +export interface BuilderQueryEditorOperator { + labelValue?: string; + name: string; + value: string; +} + +export interface BuilderQueryEditorWhereExpressionItems { + operator: BuilderQueryEditorOperator; + property: BuilderQueryEditorProperty; + type: BuilderQueryEditorExpressionType; +} + +export interface BuilderQueryEditorWhereExpression { + expressions: Array; + type: BuilderQueryEditorExpressionType; +} + +export const defaultBuilderQueryEditorWhereExpression: Partial = { + expressions: [], +}; + +export interface BuilderQueryEditorWhereExpressionArray { + expressions: Array; + type: BuilderQueryEditorExpressionType; +} + +export const defaultBuilderQueryEditorWhereExpressionArray: Partial = { + expressions: [], +}; + +export interface BuilderQueryEditorFunctionParameterExpression { + fieldType: BuilderQueryEditorPropertyType; + type: BuilderQueryEditorExpressionType; + value: string; +} + +export interface BuilderQueryEditorReduceExpression { + focus?: boolean; + parameters?: Array; + property?: BuilderQueryEditorProperty; + reduce?: BuilderQueryEditorProperty; +} + +export const defaultBuilderQueryEditorReduceExpression: Partial = { + parameters: [], +}; + +export interface BuilderQueryEditorReduceExpressionArray { + expressions: Array; + type: BuilderQueryEditorExpressionType; +} + +export const defaultBuilderQueryEditorReduceExpressionArray: Partial = { + expressions: [], +}; + +export interface BuilderQueryEditorGroupByExpression { + focus?: boolean; + interval?: BuilderQueryEditorProperty; + property?: BuilderQueryEditorProperty; + type?: BuilderQueryEditorExpressionType; +} + +export interface BuilderQueryEditorGroupByExpressionArray { + expressions: Array; + type: BuilderQueryEditorExpressionType; +} + +export const defaultBuilderQueryEditorGroupByExpressionArray: Partial = { + expressions: [], +}; + +export interface BuilderQueryEditorOrderByExpression { + order: BuilderQueryEditorOrderByOptions; + property: BuilderQueryEditorProperty; + type: BuilderQueryEditorExpressionType; +} + +export interface BuilderQueryEditorOrderByExpressionArray { + expressions: Array; + type: BuilderQueryEditorExpressionType; +} + +export const defaultBuilderQueryEditorOrderByExpressionArray: Partial = { + expressions: [], +}; + +export interface BuilderQueryExpression { + columns?: BuilderQueryEditorColumnsExpression; + from?: BuilderQueryEditorPropertyExpression; + fuzzySearch?: BuilderQueryEditorWhereExpressionArray; + groupBy?: BuilderQueryEditorGroupByExpressionArray; + limit?: number; + orderBy?: BuilderQueryEditorOrderByExpressionArray; + reduce?: BuilderQueryEditorReduceExpressionArray; + timeFilter?: BuilderQueryEditorWhereExpressionArray; + where?: BuilderQueryEditorWhereExpressionArray; +} + export interface AzureResourceGraphQuery { /** * Azure Resource Graph KQL query to be executed. diff --git a/public/app/plugins/datasource/azuremonitor/e2e/selectors.ts b/public/app/plugins/datasource/azuremonitor/e2e/selectors.ts index 4b131e7f219..4756af42064 100644 --- a/public/app/plugins/datasource/azuremonitor/e2e/selectors.ts +++ b/public/app/plugins/datasource/azuremonitor/e2e/selectors.ts @@ -76,6 +76,9 @@ export const components = { formatSelection: { input: 'data-testid format-selection', }, + runQuery: { + button: 'data-testid run-query', + }, }, argsQueryEditor: { container: { diff --git a/public/app/plugins/datasource/azuremonitor/types/types.ts b/public/app/plugins/datasource/azuremonitor/types/types.ts index a1551965bda..b31e0d97d75 100644 --- a/public/app/plugins/datasource/azuremonitor/types/types.ts +++ b/public/app/plugins/datasource/azuremonitor/types/types.ts @@ -264,6 +264,15 @@ export interface AzureAPIResponse { statusText?: string; } +export interface AzureLogAnalyticsTable { + name: string; + description: string; +} + +export interface MetadataResponse { + tables: AzureLogAnalyticsTable[]; +} + export interface Location { id: string; name: string; @@ -417,3 +426,46 @@ export type CheatsheetQueries = { export type DropdownCategories = { [key: string]: boolean; }; + +export enum QueryEditorPropertyType { + Number = 'number', + String = 'string', + Boolean = 'boolean', + DateTime = 'datetime', + TimeSpan = 'timeSpan', + Function = 'function', + Interval = 'interval', +} + +export interface QueryEditorProperty { + type: QueryEditorPropertyType; + name: string; +} + +export type QueryEditorOperatorType = string | boolean | number | SelectableValue; +export type QueryEditorOperatorValueType = QueryEditorOperatorType | QueryEditorOperatorType[]; + +export interface QueryEditorOperator { + name: string; + value: T; + labelValue?: string; +} + +export interface QueryEditorOperatorDefinition { + value: string; + supportTypes: QueryEditorPropertyType[]; + multipleValues: boolean; + booleanValues: boolean; + label?: string; + description?: string; +} + +export enum AggregateFunctions { + Sum = 'sum', + Avg = 'avg', + Count = 'count', + Dcount = 'dcount', + Max = 'max', + Min = 'min', + Percentile = 'percentile', +}