Azure Monitor: Add logs query builder (#99055)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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[] };
|
||||
|
||||
@@ -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;
|
||||
|
||||
+164
@@ -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<string>;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorColumnsExpression: Partial<BuilderQueryEditorColumnsExpression> = {
|
||||
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<BuilderQueryEditorWhereExpressionItems>;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorWhereExpression: Partial<BuilderQueryEditorWhereExpression> = {
|
||||
expressions: [],
|
||||
};
|
||||
|
||||
export interface BuilderQueryEditorWhereExpressionArray {
|
||||
expressions: Array<BuilderQueryEditorWhereExpression>;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorWhereExpressionArray: Partial<BuilderQueryEditorWhereExpressionArray> = {
|
||||
expressions: [],
|
||||
};
|
||||
|
||||
export interface BuilderQueryEditorFunctionParameterExpression {
|
||||
fieldType: BuilderQueryEditorPropertyType;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface BuilderQueryEditorReduceExpression {
|
||||
focus?: boolean;
|
||||
parameters?: Array<BuilderQueryEditorFunctionParameterExpression>;
|
||||
property?: BuilderQueryEditorProperty;
|
||||
reduce?: BuilderQueryEditorProperty;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorReduceExpression: Partial<BuilderQueryEditorReduceExpression> = {
|
||||
parameters: [],
|
||||
};
|
||||
|
||||
export interface BuilderQueryEditorReduceExpressionArray {
|
||||
expressions: Array<BuilderQueryEditorReduceExpression>;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorReduceExpressionArray: Partial<BuilderQueryEditorReduceExpressionArray> = {
|
||||
expressions: [],
|
||||
};
|
||||
|
||||
export interface BuilderQueryEditorGroupByExpression {
|
||||
focus?: boolean;
|
||||
interval?: BuilderQueryEditorProperty;
|
||||
property?: BuilderQueryEditorProperty;
|
||||
type?: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export interface BuilderQueryEditorGroupByExpressionArray {
|
||||
expressions: Array<BuilderQueryEditorGroupByExpression>;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorGroupByExpressionArray: Partial<BuilderQueryEditorGroupByExpressionArray> = {
|
||||
expressions: [],
|
||||
};
|
||||
|
||||
export interface BuilderQueryEditorOrderByExpression {
|
||||
order: BuilderQueryEditorOrderByOptions;
|
||||
property: BuilderQueryEditorProperty;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export interface BuilderQueryEditorOrderByExpressionArray {
|
||||
expressions: Array<BuilderQueryEditorOrderByExpression>;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorOrderByExpressionArray: Partial<BuilderQueryEditorOrderByExpressionArray> = {
|
||||
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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+152
@@ -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<SelectableValue<string>>;
|
||||
onChange: (item: BuilderQueryEditorReduceExpression) => void;
|
||||
onDelete: () => void;
|
||||
templateVariableOptions: SelectableValue<string>;
|
||||
}
|
||||
|
||||
const AggregateItem: React.FC<AggregateItemProps> = ({
|
||||
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<BuilderQueryEditorReduceExpression>) => {
|
||||
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 (
|
||||
<InputGroup>
|
||||
<Select
|
||||
aria-label="aggregate function"
|
||||
width={inputFieldSize}
|
||||
value={aggregate.reduce?.name ? { label: aggregate.reduce.name, value: aggregate.reduce.name } : null}
|
||||
options={aggregateOptions}
|
||||
onChange={(e) => handleAggregateChange(e.value)}
|
||||
/>
|
||||
|
||||
{isPercentile ? (
|
||||
<>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={percentileValue ?? ''}
|
||||
width={inputFieldSize}
|
||||
onChange={(e) => {
|
||||
const val = Number(e.currentTarget.value);
|
||||
if (!isNaN(val) && val >= 0 && val <= 100) {
|
||||
handlePercentileChange(val.toString());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label style={{ margin: '9px 9px 0 9px' }}>OF</Label>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{!isCountAggregate ? (
|
||||
<Select
|
||||
aria-label="column"
|
||||
width={inputFieldSize}
|
||||
value={columnValue ? { label: columnValue, value: columnValue } : null}
|
||||
options={selectableOptions}
|
||||
onChange={(e) => handleColumnChange(e.value)}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<AccessoryButton aria-label="remove" icon="times" variant="secondary" onClick={onDelete} />
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default AggregateItem;
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { EditorField, EditorFieldGroup, EditorList, EditorRow } from '@grafana/plugin-ui';
|
||||
|
||||
import { BuilderQueryEditorReduceExpression } from '../../dataquery.gen';
|
||||
import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types';
|
||||
|
||||
import AggregateItem from './AggregateItem';
|
||||
import { BuildAndUpdateOptions } from './utils';
|
||||
|
||||
interface AggregateSectionProps {
|
||||
query: AzureMonitorQuery;
|
||||
allColumns: AzureLogAnalyticsMetadataColumn[];
|
||||
templateVariableOptions: SelectableValue<string>;
|
||||
buildAndUpdateQuery: (options: Partial<BuildAndUpdateOptions>) => void;
|
||||
}
|
||||
export const AggregateSection: React.FC<AggregateSectionProps> = ({
|
||||
query,
|
||||
allColumns,
|
||||
buildAndUpdateQuery,
|
||||
templateVariableOptions,
|
||||
}) => {
|
||||
const builderQuery = query.azureLogAnalytics?.builderQuery;
|
||||
const [aggregates, setAggregates] = useState<BuilderQueryEditorReduceExpression[]>(
|
||||
builderQuery?.reduce?.expressions || []
|
||||
);
|
||||
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null);
|
||||
const hasLoadedAggregates = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const currentTable = builderQuery?.from?.property.name || null;
|
||||
|
||||
if (prevTable.current !== currentTable || builderQuery?.reduce?.expressions.length === 0) {
|
||||
setAggregates([]);
|
||||
hasLoadedAggregates.current = false;
|
||||
prevTable.current = currentTable;
|
||||
}
|
||||
}, [builderQuery]);
|
||||
|
||||
const availableColumns: Array<SelectableValue<string>> = builderQuery?.columns?.columns?.length
|
||||
? builderQuery.columns.columns.map((col) => ({ label: col, value: col }))
|
||||
: allColumns.map((col) => ({ label: col.name, value: col.name }));
|
||||
|
||||
const onChange = (newItems: Array<Partial<BuilderQueryEditorReduceExpression>>) => {
|
||||
setAggregates(newItems);
|
||||
|
||||
buildAndUpdateQuery({
|
||||
reduce: newItems,
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteAggregate = (aggregateToDelete: BuilderQueryEditorReduceExpression) => {
|
||||
setAggregates((prevAggregates) => {
|
||||
const updatedAggregates = prevAggregates.filter(
|
||||
(agg) =>
|
||||
agg.property?.name !== aggregateToDelete.property?.name || agg.reduce?.name !== aggregateToDelete.reduce?.name
|
||||
);
|
||||
|
||||
buildAndUpdateQuery({
|
||||
reduce: updatedAggregates.length === 0 ? [] : updatedAggregates,
|
||||
});
|
||||
|
||||
return updatedAggregates;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="aggregate-section">
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<EditorField
|
||||
label="Aggregate"
|
||||
optional={true}
|
||||
tooltip={`Perform calculations across rows of data, such as count, sum, average, minimum, maximum, standard deviation or percentiles.`}
|
||||
>
|
||||
<EditorList
|
||||
items={aggregates}
|
||||
onChange={onChange}
|
||||
renderItem={makeRenderAggregate(availableColumns, onDeleteAggregate, templateVariableOptions)}
|
||||
/>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function makeRenderAggregate(
|
||||
availableColumns: Array<SelectableValue<string>>,
|
||||
onDeleteAggregate: (aggregate: BuilderQueryEditorReduceExpression) => void,
|
||||
templateVariableOptions: SelectableValue<string>
|
||||
) {
|
||||
return function renderAggregate(
|
||||
item: BuilderQueryEditorReduceExpression,
|
||||
onChange: (item: BuilderQueryEditorReduceExpression) => void
|
||||
) {
|
||||
return (
|
||||
<AggregateItem
|
||||
aggregate={item}
|
||||
onChange={onChange}
|
||||
onDelete={() => onDeleteAggregate(item)}
|
||||
columns={availableColumns}
|
||||
templateVariableOptions={templateVariableOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
import { BuilderQueryEditorExpressionType } from '../../dataquery.gen';
|
||||
|
||||
import { AzureMonitorKustoQueryBuilder } from './AzureMonitorKustoQueryBuilder';
|
||||
|
||||
describe('AzureMonitorKustoQueryParser', () => {
|
||||
it('returns empty string if from table is not specified', () => {
|
||||
const builderQuery: any = { from: { property: { name: '' } } };
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('builds a query with table and project', () => {
|
||||
const builderQuery: any = {
|
||||
from: { property: { name: 'Logs' } },
|
||||
columns: { columns: ['TimeGenerated', 'Level', 'Message'] },
|
||||
};
|
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery);
|
||||
expect(result).toContain('Logs');
|
||||
expect(result).toContain('project TimeGenerated, Level, Message');
|
||||
});
|
||||
|
||||
it('includes time filter when needed', () => {
|
||||
const builderQuery: any = {
|
||||
from: { property: { name: 'Logs' } },
|
||||
timeFilter: {
|
||||
expressions: [
|
||||
{
|
||||
type: BuilderQueryEditorExpressionType.Operator,
|
||||
operator: { name: '$__timeFilter' },
|
||||
property: { name: 'TimeGenerated' },
|
||||
},
|
||||
],
|
||||
},
|
||||
columns: { columns: ['TimeGenerated', 'Level'] },
|
||||
};
|
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery);
|
||||
expect(result).toContain('$__timeFilter(TimeGenerated)');
|
||||
});
|
||||
|
||||
it('handles fuzzy search expressions', () => {
|
||||
const builderQuery: any = {
|
||||
from: { property: { name: 'Logs' } },
|
||||
fuzzySearch: {
|
||||
expressions: [
|
||||
{
|
||||
type: BuilderQueryEditorExpressionType.Operator,
|
||||
operator: { name: 'contains', value: 'fail' },
|
||||
property: { name: 'Message' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery);
|
||||
expect(result).toContain("Message contains 'fail'");
|
||||
});
|
||||
|
||||
it('applies additional filters', () => {
|
||||
const builderQuery: any = {
|
||||
from: { property: { name: 'Logs' } },
|
||||
where: {
|
||||
expressions: [
|
||||
{
|
||||
type: BuilderQueryEditorExpressionType.Operator,
|
||||
operator: { name: '==', value: 'Error' },
|
||||
property: { name: 'Level' },
|
||||
},
|
||||
{
|
||||
type: BuilderQueryEditorExpressionType.Operator,
|
||||
operator: { name: 'contains', value: 'fail' },
|
||||
property: { name: 'Message' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery);
|
||||
expect(result).toContain("Level == 'Error'");
|
||||
expect(result).toContain("Message contains 'fail'");
|
||||
});
|
||||
|
||||
it('handles where expressions with operator', () => {
|
||||
const builderQuery: any = {
|
||||
from: { property: { name: 'Logs' } },
|
||||
columns: { columns: ['Level', 'Message'] },
|
||||
where: {
|
||||
expressions: [
|
||||
{
|
||||
type: BuilderQueryEditorExpressionType.Operator,
|
||||
operator: { name: '==', value: 'Error' },
|
||||
property: { name: 'Level' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery);
|
||||
expect(result).toContain("Level == 'Error'");
|
||||
});
|
||||
|
||||
it('handles summarize with percentile function', () => {
|
||||
const builderQuery: any = {
|
||||
from: { property: { name: 'Logs' } },
|
||||
reduce: {
|
||||
expressions: [
|
||||
{
|
||||
reduce: { name: 'percentile' },
|
||||
parameters: [{ value: '95' }, { value: 'Duration' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery);
|
||||
expect(result).toContain('summarize percentile(95, Duration)');
|
||||
});
|
||||
|
||||
it('handles summarize with basic aggregation function like avg', () => {
|
||||
const builderQuery: any = {
|
||||
from: { property: { name: 'Logs' } },
|
||||
reduce: {
|
||||
expressions: [
|
||||
{
|
||||
reduce: { name: 'avg' },
|
||||
property: { name: 'ResponseTime' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery);
|
||||
expect(result).toContain('summarize avg(ResponseTime)');
|
||||
});
|
||||
|
||||
it('skips summarize when reduce expressions are invalid', () => {
|
||||
const builderQuery: any = {
|
||||
from: { property: { name: 'Logs' } },
|
||||
reduce: {
|
||||
expressions: [
|
||||
{
|
||||
reduce: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery);
|
||||
expect(result).not.toContain('summarize');
|
||||
});
|
||||
|
||||
it('adds summarize with groupBy', () => {
|
||||
const builderQuery: any = {
|
||||
from: { property: { name: 'Logs' } },
|
||||
columns: { columns: ['Level'] },
|
||||
groupBy: {
|
||||
expressions: [{ property: { name: 'Level' } }],
|
||||
},
|
||||
reduce: {
|
||||
expressions: [
|
||||
{
|
||||
reduce: { name: 'count' },
|
||||
property: { name: 'Level' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery);
|
||||
expect(result).toContain('summarize count() by Level');
|
||||
});
|
||||
|
||||
it('adds order by clause', () => {
|
||||
const builderQuery: any = {
|
||||
from: { property: { name: 'Logs' } },
|
||||
columns: { columns: ['TimeGenerated', 'Level'] },
|
||||
orderBy: {
|
||||
expressions: [{ property: { name: 'TimeGenerated' }, order: 'desc' }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery);
|
||||
expect(result).toContain('order by TimeGenerated desc');
|
||||
});
|
||||
|
||||
it('adds limit clause', () => {
|
||||
const builderQuery: any = {
|
||||
from: { property: { name: 'Logs' } },
|
||||
columns: { columns: ['TimeGenerated', 'Level'] },
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery);
|
||||
expect(result).toContain('limit 50');
|
||||
});
|
||||
});
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
BuilderQueryEditorWhereExpression,
|
||||
BuilderQueryEditorWhereExpressionArray,
|
||||
BuilderQueryEditorWhereExpressionItems,
|
||||
BuilderQueryExpression,
|
||||
} from '../../dataquery.gen';
|
||||
|
||||
const isNestedExpression = (
|
||||
exp: BuilderQueryEditorWhereExpression | BuilderQueryEditorWhereExpressionItems
|
||||
): exp is BuilderQueryEditorWhereExpressionItems =>
|
||||
'operator' in exp &&
|
||||
'property' in exp &&
|
||||
typeof exp.operator?.name === 'string' &&
|
||||
typeof exp.property?.name === 'string';
|
||||
|
||||
const buildCondition = (
|
||||
exp: BuilderQueryEditorWhereExpression | BuilderQueryEditorWhereExpressionItems
|
||||
): string | undefined => {
|
||||
if ('expressions' in exp && Array.isArray(exp.expressions)) {
|
||||
const isGroupOfFilters = exp.expressions.every((e) => 'operator' in e && 'property' in e);
|
||||
|
||||
const nested = exp.expressions.map(buildCondition).filter((c): c is string => Boolean(c));
|
||||
|
||||
if (nested.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const joiner = isGroupOfFilters ? ' or ' : ' and ';
|
||||
const joined = nested.join(joiner);
|
||||
|
||||
return nested.length > 1 ? `(${joined})` : joined;
|
||||
}
|
||||
|
||||
if (isNestedExpression(exp)) {
|
||||
const { name: op, value } = exp.operator;
|
||||
const { name: prop } = exp.property;
|
||||
const escapedValue = String(value).replace(/'/g, "''");
|
||||
return op === '$__timeFilter' ? `$__timeFilter(${prop})` : `${prop} ${op} '${escapedValue}'`;
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export const appendWhere = (
|
||||
phrases: string[],
|
||||
timeFilter?: BuilderQueryEditorWhereExpressionArray,
|
||||
fuzzySearch?: BuilderQueryEditorWhereExpressionArray,
|
||||
where?: BuilderQueryEditorWhereExpressionArray
|
||||
): void => {
|
||||
const groups = [timeFilter, fuzzySearch, where];
|
||||
|
||||
groups.forEach((group) => {
|
||||
group?.expressions.forEach((exp) => {
|
||||
const condition = buildCondition(exp);
|
||||
if (condition) {
|
||||
phrases.push(`where ${condition}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const appendProject = (builderQuery: BuilderQueryExpression, phrases: string[]) => {
|
||||
const selectedColumns = builderQuery.columns?.columns || [];
|
||||
if (selectedColumns.length > 0) {
|
||||
phrases.push(`project ${selectedColumns.join(', ')}`);
|
||||
}
|
||||
};
|
||||
|
||||
const appendSummarize = (builderQuery: BuilderQueryExpression, phrases: string[]) => {
|
||||
const summarizeAlreadyAdded = phrases.some((phrase) => phrase.startsWith('summarize'));
|
||||
if (summarizeAlreadyAdded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reduceExprs = builderQuery.reduce?.expressions ?? [];
|
||||
const groupBy = builderQuery.groupBy?.expressions?.map((exp) => exp.property?.name).filter(Boolean) ?? [];
|
||||
|
||||
const summarizeParts = reduceExprs
|
||||
.map((expr) => {
|
||||
if (!expr.reduce?.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const func = expr.reduce.name;
|
||||
|
||||
if (func === 'percentile') {
|
||||
const percentileValue = expr.parameters?.[0]?.value;
|
||||
const column = expr.parameters?.[1]?.value ?? expr.property?.name ?? '';
|
||||
return column ? `percentile(${percentileValue}, ${column})` : null;
|
||||
}
|
||||
|
||||
const column = expr.property?.name ?? '';
|
||||
return func === 'count' ? 'count()' : column ? `${func}(${column})` : func;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (summarizeParts.length === 0 && groupBy.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const summarizeClause =
|
||||
summarizeParts.length > 0
|
||||
? `summarize ${summarizeParts.join(', ')}${groupBy.length > 0 ? ` by ${groupBy.join(', ')}` : ''}`
|
||||
: `summarize by ${groupBy.join(', ')}`;
|
||||
|
||||
phrases.push(summarizeClause);
|
||||
};
|
||||
|
||||
const appendOrderBy = (builderQuery: BuilderQueryExpression, phrases: string[]) => {
|
||||
const orderBy = builderQuery.orderBy?.expressions || [];
|
||||
if (!orderBy.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clauses = orderBy.map((order) => `${order.property?.name} ${order.order}`).filter(Boolean);
|
||||
if (clauses.length > 0) {
|
||||
phrases.push(`order by ${clauses.join(', ')}`);
|
||||
}
|
||||
};
|
||||
|
||||
const appendLimit = (builderQuery: BuilderQueryExpression, phrases: string[]) => {
|
||||
if (builderQuery.limit && builderQuery.limit > 0) {
|
||||
phrases.push(`limit ${builderQuery.limit}`);
|
||||
}
|
||||
};
|
||||
|
||||
const toQuery = (builderQuery: BuilderQueryExpression): string => {
|
||||
const { from, timeFilter, fuzzySearch, where } = builderQuery;
|
||||
if (!from?.property?.name) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const phrases: string[] = [];
|
||||
phrases.push(from.property.name);
|
||||
|
||||
appendWhere(phrases, timeFilter, fuzzySearch, where);
|
||||
appendProject(builderQuery, phrases);
|
||||
appendSummarize(builderQuery, phrases);
|
||||
appendOrderBy(builderQuery, phrases);
|
||||
appendLimit(builderQuery, phrases);
|
||||
|
||||
return phrases.join('\n| ');
|
||||
};
|
||||
|
||||
export const AzureMonitorKustoQueryBuilder = {
|
||||
toQuery,
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Button, Combobox, ComboboxOption, Label, Select } from '@grafana/ui';
|
||||
|
||||
import { BuilderQueryEditorWhereExpressionItems } from '../../dataquery.gen';
|
||||
|
||||
import { inputFieldSize, toOperatorOptions, valueToDefinition } from './utils';
|
||||
|
||||
interface FilterItemProps {
|
||||
filter: BuilderQueryEditorWhereExpressionItems;
|
||||
filterIndex: number;
|
||||
groupIndex: number;
|
||||
usedColumns: string[];
|
||||
selectableOptions: Array<SelectableValue<string>>;
|
||||
onChange: (groupIndex: number, field: 'property' | 'operator' | 'value', value: string, filterIndex: number) => void;
|
||||
onDelete: (groupIndex: number, filterIndex: number) => void;
|
||||
getFilterValues: (
|
||||
filter: BuilderQueryEditorWhereExpressionItems,
|
||||
inputValue: string
|
||||
) => Promise<Array<ComboboxOption<string>>>;
|
||||
showOr: boolean;
|
||||
}
|
||||
|
||||
export const FilterItem: React.FC<FilterItemProps> = ({
|
||||
filter,
|
||||
filterIndex,
|
||||
groupIndex,
|
||||
usedColumns,
|
||||
selectableOptions,
|
||||
onChange,
|
||||
onDelete,
|
||||
getFilterValues,
|
||||
showOr,
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Select
|
||||
aria-label="column"
|
||||
width={inputFieldSize}
|
||||
value={valueToDefinition(filter.property.name)}
|
||||
options={selectableOptions.filter((opt) => !usedColumns.includes(opt.value!))}
|
||||
onChange={(e) => e.value && onChange(groupIndex, 'property', e.value, filterIndex)}
|
||||
/>
|
||||
<Select
|
||||
aria-label="operator"
|
||||
width={12}
|
||||
value={{ label: filter.operator.name, value: filter.operator.name }}
|
||||
options={toOperatorOptions('string')}
|
||||
onChange={(e) => e.value && onChange(groupIndex, 'operator', e.value, filterIndex)}
|
||||
/>
|
||||
<Combobox
|
||||
aria-label="column value"
|
||||
value={
|
||||
filter.operator.value
|
||||
? {
|
||||
label: String(filter.operator.value),
|
||||
value: String(filter.operator.value),
|
||||
}
|
||||
: null
|
||||
}
|
||||
options={(inputValue: string) => getFilterValues(filter, inputValue)}
|
||||
onChange={(e) => e.value && onChange(groupIndex, 'value', String(e.value), filterIndex)}
|
||||
width={inputFieldSize}
|
||||
disabled={!filter.property?.name}
|
||||
/>
|
||||
<Button variant="secondary" icon="times" onClick={() => onDelete(groupIndex, filterIndex)} />
|
||||
{showOr && <Label style={{ padding: '9px 14px' }}>OR</Label>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { CoreApp, getDefaultTimeRange, SelectableValue, TimeRange } from '@grafana/data';
|
||||
import { EditorField, EditorFieldGroup, EditorRow, InputGroup } from '@grafana/plugin-ui';
|
||||
import { Button, ComboboxOption, Label, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
AzureQueryType,
|
||||
BuilderQueryEditorExpressionType,
|
||||
BuilderQueryEditorPropertyType,
|
||||
BuilderQueryEditorWhereExpression,
|
||||
BuilderQueryEditorWhereExpressionItems,
|
||||
} from '../../dataquery.gen';
|
||||
import Datasource from '../../datasource';
|
||||
import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types';
|
||||
|
||||
import { FilterItem } from './FilterItem';
|
||||
import { BuildAndUpdateOptions } from './utils';
|
||||
|
||||
interface FilterSectionProps {
|
||||
query: AzureMonitorQuery;
|
||||
allColumns: AzureLogAnalyticsMetadataColumn[];
|
||||
buildAndUpdateQuery: (options: Partial<BuildAndUpdateOptions>) => void;
|
||||
templateVariableOptions: SelectableValue<string>;
|
||||
datasource: Datasource;
|
||||
timeRange?: TimeRange;
|
||||
}
|
||||
|
||||
const filterDynamicColumns = (columns: string[], allColumns: AzureLogAnalyticsMetadataColumn[]) => {
|
||||
return columns.filter((col) =>
|
||||
allColumns.some((completeCol) => completeCol.name === col && completeCol.type !== 'dynamic')
|
||||
);
|
||||
};
|
||||
|
||||
export const FilterSection: React.FC<FilterSectionProps> = ({
|
||||
buildAndUpdateQuery,
|
||||
query,
|
||||
allColumns,
|
||||
templateVariableOptions,
|
||||
datasource,
|
||||
timeRange,
|
||||
}) => {
|
||||
const styles = useStyles2(() => ({ filters: css({ marginBottom: '8px' }) }));
|
||||
const builderQuery = query.azureLogAnalytics?.builderQuery;
|
||||
|
||||
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null);
|
||||
const [filters, setFilters] = useState<BuilderQueryEditorWhereExpression[]>(
|
||||
builderQuery?.where?.expressions?.map((group) => ({
|
||||
...group,
|
||||
expressions: group.expressions ?? [],
|
||||
})) || []
|
||||
);
|
||||
const hasLoadedFilters = useRef(false);
|
||||
|
||||
const variableOptions = Array.isArray(templateVariableOptions) ? templateVariableOptions : [templateVariableOptions];
|
||||
|
||||
const availableColumns: Array<SelectableValue<string>> = builderQuery?.columns?.columns?.length
|
||||
? filterDynamicColumns(builderQuery.columns.columns, allColumns).map((col) => ({ label: col, value: col }))
|
||||
: allColumns.filter((col) => col.type !== 'dynamic').map((col) => ({ label: col.name, value: col.name }));
|
||||
|
||||
const selectableOptions = [...availableColumns, ...variableOptions];
|
||||
|
||||
const usedColumnsInOtherGroups = (currentGroupIndex: number): string[] => {
|
||||
return filters
|
||||
.flatMap((group, idx) => (idx !== currentGroupIndex ? group.expressions : []))
|
||||
.map((exp) => exp.property.name)
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const currentTable = builderQuery?.from?.property.name || null;
|
||||
if (prevTable.current !== currentTable || builderQuery?.where?.expressions.length === 0) {
|
||||
setFilters([]);
|
||||
hasLoadedFilters.current = false;
|
||||
prevTable.current = currentTable;
|
||||
}
|
||||
}, [builderQuery]);
|
||||
|
||||
const updateFilters = (updated: BuilderQueryEditorWhereExpression[]) => {
|
||||
setFilters(updated);
|
||||
buildAndUpdateQuery({ where: updated });
|
||||
};
|
||||
|
||||
const onAddOrFilters = (
|
||||
groupIndex: number,
|
||||
field: 'property' | 'operator' | 'value',
|
||||
value: string,
|
||||
filterIndex?: number
|
||||
) => {
|
||||
const updated = [...filters];
|
||||
const group = updated[groupIndex];
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
let filter: BuilderQueryEditorWhereExpressionItems =
|
||||
filterIndex !== undefined
|
||||
? { ...group.expressions[filterIndex] }
|
||||
: {
|
||||
type: BuilderQueryEditorExpressionType.Operator,
|
||||
property: { name: '', type: BuilderQueryEditorPropertyType.String },
|
||||
operator: { name: '==', value: '' },
|
||||
};
|
||||
|
||||
if (field === 'property') {
|
||||
filter.property.name = value;
|
||||
filter.operator.value = '';
|
||||
} else if (field === 'operator') {
|
||||
filter.operator.name = value;
|
||||
} else if (field === 'value') {
|
||||
filter.operator.value = value;
|
||||
}
|
||||
|
||||
const isValid = filter.property.name && filter.operator.name && filter.operator.value !== '';
|
||||
|
||||
if (filterIndex !== undefined) {
|
||||
group.expressions[filterIndex] = filter;
|
||||
} else {
|
||||
group.expressions.push(filter);
|
||||
}
|
||||
|
||||
updated[groupIndex] = group;
|
||||
setFilters(updated);
|
||||
if (isValid) {
|
||||
updateFilters(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onAddAndFilters = () => {
|
||||
const updated = [
|
||||
...filters,
|
||||
{
|
||||
type: BuilderQueryEditorExpressionType.Or,
|
||||
expressions: [
|
||||
{
|
||||
type: BuilderQueryEditorExpressionType.Operator,
|
||||
property: { name: '', type: BuilderQueryEditorPropertyType.String },
|
||||
operator: { name: '==', value: '' },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
updateFilters(updated);
|
||||
};
|
||||
|
||||
const onDeleteFilter = (groupIndex: number, filterIndex: number) => {
|
||||
const updated = [...filters];
|
||||
updated[groupIndex].expressions.splice(filterIndex, 1);
|
||||
if (updated[groupIndex].expressions.length === 0) {
|
||||
updated.splice(groupIndex, 1);
|
||||
}
|
||||
updateFilters(updated);
|
||||
};
|
||||
|
||||
const getFilterValues = async (filter: BuilderQueryEditorWhereExpressionItems) => {
|
||||
const from = timeRange?.from?.toISOString();
|
||||
const to = timeRange?.to?.toISOString();
|
||||
const timeColumn = query.azureLogAnalytics?.timeColumn || 'TimeGenerated';
|
||||
|
||||
const kustoQuery = `
|
||||
${query.azureLogAnalytics?.builderQuery?.from?.property.name}
|
||||
| where ${timeColumn} >= datetime(${from}) and ${timeColumn} <= datetime(${to})
|
||||
| distinct ${filter.property.name}
|
||||
| limit 1000
|
||||
`;
|
||||
|
||||
const results: any = await lastValueFrom(
|
||||
datasource.azureLogAnalyticsDatasource.query({
|
||||
requestId: 'azure-logs-builder-filter-values',
|
||||
interval: '',
|
||||
intervalMs: 0,
|
||||
scopedVars: {},
|
||||
timezone: '',
|
||||
app: CoreApp.Unknown,
|
||||
startTime: 0,
|
||||
range: timeRange || getDefaultTimeRange(),
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
queryType: AzureQueryType.LogAnalytics,
|
||||
azureLogAnalytics: {
|
||||
query: kustoQuery,
|
||||
resources: query.azureLogAnalytics?.resources ?? [],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
if (results.state === 'Done') {
|
||||
const values = results.data?.[0]?.fields?.[0]?.values ?? [];
|
||||
|
||||
return values.toArray().map(
|
||||
(v: any): ComboboxOption<string> => ({
|
||||
label: String(v),
|
||||
value: String(v),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<EditorField label="Filters" optional tooltip="Narrow results by applying conditions to specific columns.">
|
||||
<div className={styles.filters}>
|
||||
{filters.length === 0 || filters.every((g) => g.expressions.length === 0) ? (
|
||||
<InputGroup>
|
||||
<Button variant="secondary" onClick={onAddAndFilters} icon="plus" />
|
||||
</InputGroup>
|
||||
) : (
|
||||
<>
|
||||
{filters.map((group, groupIndex) => (
|
||||
<div key={groupIndex}>
|
||||
{groupIndex > 0 && filters[groupIndex - 1]?.expressions.length > 0 && (
|
||||
<Label style={{ padding: '9px 14px' }}>AND</Label>
|
||||
)}
|
||||
<InputGroup>
|
||||
<>
|
||||
{group.expressions.map((filter, filterIndex) => (
|
||||
<FilterItem
|
||||
key={`${groupIndex}-${filterIndex}`}
|
||||
filter={filter}
|
||||
filterIndex={filterIndex}
|
||||
groupIndex={groupIndex}
|
||||
usedColumns={usedColumnsInOtherGroups(groupIndex)}
|
||||
selectableOptions={selectableOptions}
|
||||
onChange={onAddOrFilters}
|
||||
onDelete={onDeleteFilter}
|
||||
getFilterValues={getFilterValues}
|
||||
showOr={filterIndex < group.expressions.length - 1}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<Button
|
||||
variant="secondary"
|
||||
style={{ marginLeft: '15px' }}
|
||||
onClick={() => onAddOrFilters(groupIndex, 'property', '')}
|
||||
icon="plus"
|
||||
/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
))}
|
||||
{filters.some((g) => g.expressions.length > 0) && (
|
||||
<Button variant="secondary" onClick={onAddAndFilters} style={{ marginTop: '8px' }}>
|
||||
Add group
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
);
|
||||
};
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { EditorRow, EditorFieldGroup, EditorField, InputGroup } from '@grafana/plugin-ui';
|
||||
import { Button, Input, Select } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
BuilderQueryEditorExpressionType,
|
||||
BuilderQueryEditorWhereExpression,
|
||||
BuilderQueryEditorPropertyType,
|
||||
} from '../../dataquery.gen';
|
||||
import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types';
|
||||
|
||||
import { BuildAndUpdateOptions, removeExtraQuotes } from './utils';
|
||||
|
||||
interface FuzzySearchProps {
|
||||
query: AzureMonitorQuery;
|
||||
allColumns: AzureLogAnalyticsMetadataColumn[];
|
||||
buildAndUpdateQuery: (options: Partial<BuildAndUpdateOptions>) => void;
|
||||
templateVariableOptions: SelectableValue<string>;
|
||||
}
|
||||
|
||||
export const FuzzySearch: React.FC<FuzzySearchProps> = ({
|
||||
buildAndUpdateQuery,
|
||||
query,
|
||||
allColumns,
|
||||
templateVariableOptions,
|
||||
}) => {
|
||||
const builderQuery = query.azureLogAnalytics?.builderQuery;
|
||||
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const [selectedColumn, setSelectedColumn] = useState<string>('');
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const hasLoadedFuzzySearch = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const currentTable = builderQuery?.from?.property.name || null;
|
||||
|
||||
if (prevTable.current !== currentTable) {
|
||||
setSearchTerm('');
|
||||
setSelectedColumn('');
|
||||
setIsOpen(false);
|
||||
hasLoadedFuzzySearch.current = false;
|
||||
prevTable.current = currentTable;
|
||||
}
|
||||
|
||||
if (!hasLoadedFuzzySearch.current && builderQuery?.fuzzySearch?.expressions?.length) {
|
||||
const fuzzy = builderQuery.fuzzySearch.expressions[0];
|
||||
setSearchTerm(removeExtraQuotes(String(fuzzy.expressions[0].operator?.value ?? '')));
|
||||
setSelectedColumn(fuzzy.expressions[0].property?.name ?? '*');
|
||||
setIsOpen(true);
|
||||
hasLoadedFuzzySearch.current = true;
|
||||
}
|
||||
}, [builderQuery]);
|
||||
|
||||
const columnOptions: Array<SelectableValue<string>> = allColumns.map((col) => ({
|
||||
label: col.name,
|
||||
value: col.name,
|
||||
}));
|
||||
|
||||
const safeTemplateVariables: Array<SelectableValue<string>> = Array.isArray(templateVariableOptions)
|
||||
? templateVariableOptions
|
||||
: [templateVariableOptions];
|
||||
|
||||
const defaultColumn: SelectableValue<string> = { label: 'All Columns *', value: '*' };
|
||||
const selectableOptions = [defaultColumn, ...columnOptions, ...safeTemplateVariables];
|
||||
|
||||
const updateFuzzySearch = (column: string, term: string) => {
|
||||
setSearchTerm(term);
|
||||
setSelectedColumn(column);
|
||||
|
||||
const fuzzyExpression: BuilderQueryEditorWhereExpression = {
|
||||
type: BuilderQueryEditorExpressionType.Operator,
|
||||
expressions: [
|
||||
{
|
||||
type: BuilderQueryEditorExpressionType.Property,
|
||||
operator: { name: 'has', value: term },
|
||||
property: { name: column || '*', type: BuilderQueryEditorPropertyType.String },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
buildAndUpdateQuery({
|
||||
fuzzySearch: term ? [fuzzyExpression] : [],
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteFuzzySearch = () => {
|
||||
setSearchTerm('');
|
||||
setSelectedColumn('');
|
||||
setIsOpen(false);
|
||||
|
||||
buildAndUpdateQuery({
|
||||
fuzzySearch: [],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<EditorField
|
||||
label="Fuzzy Search"
|
||||
optional={true}
|
||||
tooltip={`Find approximate text matches with tolerance for spelling variations. By default, fuzzy search scans all
|
||||
columns (*) in the entire table, not just specific fields.`}
|
||||
>
|
||||
<InputGroup>
|
||||
{isOpen ? (
|
||||
<>
|
||||
<Input
|
||||
className="width-10"
|
||||
type="text"
|
||||
placeholder="Enter search term"
|
||||
value={searchTerm}
|
||||
onChange={(e) => updateFuzzySearch(selectedColumn, e.currentTarget.value)}
|
||||
/>
|
||||
<Select
|
||||
aria-label="Select Column"
|
||||
options={selectableOptions}
|
||||
value={{ label: selectedColumn || '*', value: selectedColumn || '*' }}
|
||||
onChange={(e: SelectableValue<string>) => updateFuzzySearch(e.value ?? '*', searchTerm)}
|
||||
width="auto"
|
||||
/>
|
||||
<Button variant="secondary" icon="times" onClick={onDeleteFuzzySearch} />
|
||||
</>
|
||||
) : (
|
||||
<Button variant="secondary" onClick={() => setIsOpen(true)} icon="plus" />
|
||||
)}
|
||||
</InputGroup>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
);
|
||||
};
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { AccessoryButton, InputGroup } from '@grafana/plugin-ui';
|
||||
import { Select } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
BuilderQueryEditorGroupByExpression,
|
||||
BuilderQueryEditorPropertyType,
|
||||
BuilderQueryEditorExpressionType,
|
||||
} from '../../dataquery.gen';
|
||||
|
||||
import { inputFieldSize } from './utils';
|
||||
|
||||
interface GroupByItemProps {
|
||||
groupBy: BuilderQueryEditorGroupByExpression;
|
||||
columns: Array<SelectableValue<string>>;
|
||||
onChange: (item: BuilderQueryEditorGroupByExpression) => void;
|
||||
onDelete: () => void;
|
||||
templateVariableOptions: SelectableValue<string>;
|
||||
}
|
||||
|
||||
export const GroupByItem: React.FC<GroupByItemProps> = ({
|
||||
groupBy,
|
||||
onChange,
|
||||
onDelete,
|
||||
columns,
|
||||
templateVariableOptions,
|
||||
}) => {
|
||||
const columnOptions: Array<SelectableValue<string>> =
|
||||
columns.length > 0
|
||||
? columns.map((c) => ({ label: c.label, value: c.value }))
|
||||
: [{ label: 'No columns available', value: '' }];
|
||||
|
||||
const selectableOptions: Array<SelectableValue<string>> = [
|
||||
...columnOptions,
|
||||
...(templateVariableOptions
|
||||
? Array.isArray(templateVariableOptions)
|
||||
? templateVariableOptions
|
||||
: [templateVariableOptions]
|
||||
: []),
|
||||
];
|
||||
|
||||
const handleChange = (selectedValue: SelectableValue<string>) => {
|
||||
if (!selectedValue.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isTemplateVariable = selectedValue.value.startsWith('$');
|
||||
const selectedColumn = columns.find((c) => c.value === selectedValue.value);
|
||||
|
||||
onChange({
|
||||
...groupBy,
|
||||
property: {
|
||||
name: selectedValue.value,
|
||||
type: isTemplateVariable
|
||||
? BuilderQueryEditorPropertyType.String
|
||||
: selectedColumn?.type || BuilderQueryEditorPropertyType.String,
|
||||
},
|
||||
interval: groupBy.interval,
|
||||
type: BuilderQueryEditorExpressionType.Group_by,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<InputGroup>
|
||||
<Select
|
||||
aria-label="column"
|
||||
width={inputFieldSize}
|
||||
value={groupBy.property?.name ? { label: groupBy.property.name, value: groupBy.property.name } : null}
|
||||
options={selectableOptions}
|
||||
allowCustomValue
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<AccessoryButton aria-label="remove" icon="times" variant="secondary" onClick={onDelete} />
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
+150
@@ -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<BuildAndUpdateOptions>) => void;
|
||||
templateVariableOptions: SelectableValue<string>;
|
||||
}
|
||||
|
||||
export const GroupBySection: React.FC<GroupBySectionProps> = ({
|
||||
query,
|
||||
buildAndUpdateQuery,
|
||||
allColumns,
|
||||
templateVariableOptions,
|
||||
}) => {
|
||||
const builderQuery = query.azureLogAnalytics?.builderQuery;
|
||||
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null);
|
||||
const [groupBys, setGroupBys] = useState<BuilderQueryEditorGroupByExpression[]>(
|
||||
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<SelectableValue<string>> = [];
|
||||
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<Partial<BuilderQueryEditorGroupByExpression>>) => {
|
||||
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 (
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<EditorField
|
||||
label="Group by"
|
||||
optional={true}
|
||||
tooltip={`Organize results into categories based on specified columns. Group by can be used independently to list
|
||||
unique values in selected columns, or combined with aggregate functions to produce summary statistics for
|
||||
each group. When used alone, it returns distinct combinations of the specified columns.`}
|
||||
>
|
||||
<InputGroup>
|
||||
{groupBys.length > 0 ? (
|
||||
<EditorList
|
||||
items={groupBys}
|
||||
onChange={handleGroupByChange}
|
||||
renderItem={makeRenderGroupBy(availableColumns, onDeleteGroupBy, templateVariableOptions)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
onClick={() =>
|
||||
handleGroupByChange([
|
||||
{
|
||||
type: BuilderQueryEditorExpressionType.Group_by,
|
||||
property: { type: BuilderQueryEditorPropertyType.String, name: '' },
|
||||
},
|
||||
])
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</InputGroup>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
);
|
||||
};
|
||||
|
||||
const makeRenderGroupBy = (
|
||||
columns: Array<SelectableValue<string>>,
|
||||
onDeleteGroupBy: (propertyName: string) => void,
|
||||
templateVariableOptions: SelectableValue<string>
|
||||
) => {
|
||||
return (
|
||||
item: BuilderQueryEditorGroupByExpression,
|
||||
onChangeItem: (updatedItem: BuilderQueryEditorGroupByExpression) => void,
|
||||
onDeleteItem: () => void
|
||||
) => (
|
||||
<GroupByItem
|
||||
groupBy={item}
|
||||
onChange={(updatedItem) => {
|
||||
onChangeItem(updatedItem);
|
||||
}}
|
||||
onDelete={() => {
|
||||
if (item.property?.name) {
|
||||
onDeleteGroupBy(item.property.name);
|
||||
}
|
||||
onDeleteItem();
|
||||
}}
|
||||
columns={columns}
|
||||
templateVariableOptions={templateVariableOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { css } from '@emotion/css';
|
||||
import Prism from 'prismjs';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/plugin-ui';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import 'prismjs/components/prism-kusto';
|
||||
import 'prismjs/themes/prism-tomorrow.min.css';
|
||||
|
||||
interface KQLPreviewProps {
|
||||
query: string;
|
||||
hidden: boolean;
|
||||
setHidden: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const KQLPreview: React.FC<KQLPreviewProps> = ({ query, hidden, setHidden }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
Prism.highlightAll();
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<EditorField label="Query Preview">
|
||||
<>
|
||||
<Button hidden={!hidden} variant="secondary" onClick={() => setHidden(false)} size="sm">
|
||||
show
|
||||
</Button>
|
||||
<div className={styles.codeBlock} hidden={hidden}>
|
||||
<pre className={styles.code}>
|
||||
<code className="language-kusto">{query}</code>
|
||||
</pre>
|
||||
</div>
|
||||
<Button hidden={hidden} variant="secondary" onClick={() => setHidden(true)} size="sm">
|
||||
hide
|
||||
</Button>
|
||||
</>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
codeBlock: css({
|
||||
width: '100%',
|
||||
display: 'table',
|
||||
tableLayout: 'fixed',
|
||||
}),
|
||||
code: css({
|
||||
marginBottom: '4px',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export default KQLPreview;
|
||||
+41
@@ -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<BuildAndUpdateOptions>) => void;
|
||||
}
|
||||
|
||||
export const LimitSection: React.FC<LimitSectionProps> = (props) => {
|
||||
const { buildAndUpdateQuery } = props;
|
||||
const [limit, setLimit] = useState<number>(1000);
|
||||
|
||||
const handleQueryLimitUpdate = (newLimit: number) => {
|
||||
buildAndUpdateQuery({
|
||||
limit: newLimit,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<EditorField label="Limit" optional={true} tooltip={`Restrict the number of rows returned (default is 1000).`}>
|
||||
<Input
|
||||
className="width-5"
|
||||
type="number"
|
||||
placeholder="Enter limit"
|
||||
value={limit}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value.replace(/[^0-9]/g, '');
|
||||
setLimit(Number(newValue));
|
||||
handleQueryLimitUpdate(Number(newValue));
|
||||
}}
|
||||
/>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
);
|
||||
};
|
||||
+158
@@ -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<string>;
|
||||
datasource: Datasource;
|
||||
timeRange?: TimeRange;
|
||||
}
|
||||
|
||||
export const LogsQueryBuilder: React.FC<LogsQueryBuilderProps> = (props) => {
|
||||
const { query, onQueryChange, schema, datasource, timeRange } = props;
|
||||
const [isKQLPreviewHidden, setIsKQLPreviewHidden] = useState<boolean>(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 (
|
||||
<span data-testid={selectors.components.queryEditor.logsQueryEditor.container.input}>
|
||||
<EditorRows>
|
||||
{schema && tables.length === 0 && (
|
||||
<Alert severity="warning" title="Resource loaded successfully but without any tables" />
|
||||
)}
|
||||
<TableSection {...props} tables={tables} allColumns={allColumns} buildAndUpdateQuery={buildAndUpdateQuery} />
|
||||
<FilterSection
|
||||
{...props}
|
||||
allColumns={allColumns}
|
||||
buildAndUpdateQuery={buildAndUpdateQuery}
|
||||
datasource={datasource}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
<AggregateSection {...props} allColumns={allColumns} buildAndUpdateQuery={buildAndUpdateQuery} />
|
||||
<GroupBySection {...props} allColumns={allColumns} buildAndUpdateQuery={buildAndUpdateQuery} />
|
||||
<OrderBySection {...props} allColumns={allColumns} buildAndUpdateQuery={buildAndUpdateQuery} />
|
||||
<FuzzySearch {...props} allColumns={allColumns} buildAndUpdateQuery={buildAndUpdateQuery} />
|
||||
<LimitSection {...props} buildAndUpdateQuery={buildAndUpdateQuery} />
|
||||
<KQLPreview
|
||||
query={query.azureLogAnalytics?.query || ''}
|
||||
hidden={isKQLPreviewHidden}
|
||||
setHidden={setIsKQLPreviewHidden}
|
||||
/>
|
||||
</EditorRows>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
+158
@@ -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<BuildAndUpdateOptions>) => void;
|
||||
}
|
||||
|
||||
export const OrderBySection: React.FC<OrderBySectionProps> = ({ query, allColumns, buildAndUpdateQuery }) => {
|
||||
const builderQuery = query.azureLogAnalytics?.builderQuery;
|
||||
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null);
|
||||
const hasLoadedOrderBy = useRef(false);
|
||||
|
||||
const [orderBy, setOrderBy] = useState<BuilderQueryEditorOrderByExpression[]>(
|
||||
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<SelectableValue<string>> = [
|
||||
{ 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 (
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<EditorField
|
||||
label="Order By"
|
||||
optional={true}
|
||||
tooltip={`Sort results based on one or more columns in ascending or descending order.`}
|
||||
>
|
||||
<>
|
||||
{orderBy.length > 0 ? (
|
||||
orderBy.map((entry, index) => (
|
||||
<InputGroup key={index}>
|
||||
<Select
|
||||
aria-label="Order by column"
|
||||
width={inputFieldSize}
|
||||
value={entry.property?.name ? { label: entry.property.name, value: entry.property.name } : null}
|
||||
options={columnOptions}
|
||||
onChange={(e) => e.value && handleOrderByChange(index, 'column', e.value)}
|
||||
/>
|
||||
<Label style={{ margin: '9px 9px 0 9px' }}>BY</Label>
|
||||
<Select
|
||||
aria-label="Order Direction"
|
||||
width={inputFieldSize}
|
||||
value={orderOptions.find((o) => o.value === entry.order) || null}
|
||||
options={orderOptions}
|
||||
onChange={(e) => e.value && handleOrderByChange(index, 'order', e.value)}
|
||||
/>
|
||||
<Button variant="secondary" icon="times" onClick={() => onDeleteOrderBy(index)} />
|
||||
{index === orderBy.length - 1 ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleOrderByChange(-1, 'column', '')}
|
||||
icon="plus"
|
||||
style={{ marginLeft: '15px' }}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</InputGroup>
|
||||
))
|
||||
) : (
|
||||
<InputGroup>
|
||||
<Button variant="secondary" onClick={() => handleOrderByChange(-1, 'column', '')} icon="plus" />
|
||||
</InputGroup>
|
||||
)}
|
||||
</>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
);
|
||||
};
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import React from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { EditorField, EditorFieldGroup, EditorRow, InputGroup } from '@grafana/plugin-ui';
|
||||
import { Button, Select } from '@grafana/ui';
|
||||
|
||||
import { BuilderQueryEditorExpressionType, BuilderQueryEditorPropertyType } from '../../dataquery.gen';
|
||||
import { AzureMonitorQuery, AzureLogAnalyticsMetadataColumn, AzureLogAnalyticsMetadataTable } from '../../types';
|
||||
|
||||
import { BuildAndUpdateOptions, inputFieldSize } from './utils';
|
||||
|
||||
interface TableSectionProps {
|
||||
allColumns: AzureLogAnalyticsMetadataColumn[];
|
||||
tables: AzureLogAnalyticsMetadataTable[];
|
||||
query: AzureMonitorQuery;
|
||||
buildAndUpdateQuery: (options: Partial<BuildAndUpdateOptions>) => void;
|
||||
templateVariableOptions?: SelectableValue<string>;
|
||||
}
|
||||
|
||||
export const TableSection: React.FC<TableSectionProps> = (props) => {
|
||||
const { allColumns, query, tables, buildAndUpdateQuery, templateVariableOptions } = props;
|
||||
const builderQuery = query.azureLogAnalytics?.builderQuery;
|
||||
const selectedColumns = query.azureLogAnalytics?.builderQuery?.columns?.columns || [];
|
||||
|
||||
const tableOptions: Array<SelectableValue<string>> = tables.map((t) => ({
|
||||
label: t.name,
|
||||
value: t.name,
|
||||
}));
|
||||
|
||||
const columnOptions: Array<SelectableValue<string>> = allColumns.map((col) => ({
|
||||
label: col.name,
|
||||
value: col.name,
|
||||
type: col.type,
|
||||
}));
|
||||
|
||||
const selectAllOption: SelectableValue<string> = {
|
||||
label: 'All Columns',
|
||||
value: '__all_columns__',
|
||||
};
|
||||
|
||||
const selectableOptions: Array<SelectableValue<string>> = [
|
||||
selectAllOption,
|
||||
...columnOptions,
|
||||
...(templateVariableOptions
|
||||
? Array.isArray(templateVariableOptions)
|
||||
? templateVariableOptions
|
||||
: [templateVariableOptions]
|
||||
: []),
|
||||
];
|
||||
|
||||
const handleTableChange = (selected: SelectableValue<string>) => {
|
||||
const selectedTable = tables.find((t) => t.name === selected.value);
|
||||
if (!selectedTable) {
|
||||
return;
|
||||
}
|
||||
|
||||
buildAndUpdateQuery({
|
||||
from: {
|
||||
property: {
|
||||
name: selectedTable.name,
|
||||
type: BuilderQueryEditorPropertyType.String,
|
||||
},
|
||||
type: BuilderQueryEditorExpressionType.Property,
|
||||
},
|
||||
reduce: [],
|
||||
where: [],
|
||||
fuzzySearch: [],
|
||||
groupBy: [],
|
||||
orderBy: [],
|
||||
columns: [],
|
||||
});
|
||||
};
|
||||
|
||||
const handleColumnsChange = (selected: SelectableValue<string> | Array<SelectableValue<string>> | null) => {
|
||||
const selectedArray = Array.isArray(selected) ? selected : selected ? [selected] : [];
|
||||
|
||||
if (selectedArray.length === 0) {
|
||||
buildAndUpdateQuery({ columns: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
const includesAll = selectedArray.some((opt) => opt.value === '__all_columns__');
|
||||
const lastSelected = selectedArray[selectedArray.length - 1];
|
||||
|
||||
if (includesAll && lastSelected.value === '__all_columns__') {
|
||||
buildAndUpdateQuery({
|
||||
columns: allColumns.map((col) => col.name),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (includesAll && selectedArray.length > 1) {
|
||||
const filtered = selectedArray.filter((opt) => opt.value !== '__all_columns__');
|
||||
buildAndUpdateQuery({
|
||||
columns: filtered.map((opt) => opt.value!),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (includesAll && selectedArray.length === 1) {
|
||||
buildAndUpdateQuery({
|
||||
columns: allColumns.map((col) => col.name),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
buildAndUpdateQuery({
|
||||
columns: selectedArray.map((opt) => opt.value!),
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteAllColumns = () => {
|
||||
buildAndUpdateQuery({
|
||||
columns: [],
|
||||
});
|
||||
};
|
||||
|
||||
const allColumnNames = allColumns.length > 0 ? allColumns.map((col) => col.name) : [];
|
||||
|
||||
const areAllColumnsSelected =
|
||||
allColumnNames.length > 0 &&
|
||||
selectedColumns.length > 0 &&
|
||||
selectedColumns.length === allColumnNames.length &&
|
||||
allColumnNames.every((col) => selectedColumns.includes(col));
|
||||
|
||||
const columnSelectValue: Array<SelectableValue<string>> = areAllColumnsSelected
|
||||
? [selectAllOption]
|
||||
: selectedColumns.map((col) => ({ label: col, value: col }));
|
||||
|
||||
return (
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<EditorField label="Table">
|
||||
<Select
|
||||
aria-label="Table"
|
||||
value={builderQuery?.from?.property.name}
|
||||
options={tableOptions}
|
||||
placeholder="Select a table"
|
||||
onChange={handleTableChange}
|
||||
width={inputFieldSize}
|
||||
/>
|
||||
</EditorField>
|
||||
<EditorField label="Columns">
|
||||
<InputGroup>
|
||||
<Select
|
||||
aria-label="Columns"
|
||||
isMulti
|
||||
isClearable
|
||||
closeMenuOnSelect={false}
|
||||
value={columnSelectValue}
|
||||
options={selectableOptions}
|
||||
placeholder="Select columns"
|
||||
onChange={handleColumnsChange}
|
||||
isDisabled={!builderQuery?.from?.property.name}
|
||||
width={30}
|
||||
/>
|
||||
<Button variant="secondary" icon="times" onClick={onDeleteAllColumns} />
|
||||
</InputGroup>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { QueryEditorProperty, QueryEditorPropertyType } from '../../types';
|
||||
|
||||
export enum QueryEditorExpressionType {
|
||||
Property = 'property',
|
||||
Operator = 'operator',
|
||||
Reduce = 'reduce',
|
||||
FunctionParameter = 'functionParameter',
|
||||
GroupBy = 'groupBy',
|
||||
Or = 'or',
|
||||
And = 'and',
|
||||
}
|
||||
|
||||
export interface QueryEditorExpression {
|
||||
type: QueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export interface QueryEditorFunctionParameterExpression extends QueryEditorExpression {
|
||||
value: string;
|
||||
fieldType: QueryEditorPropertyType;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface QueryEditorReduceExpression extends QueryEditorExpression {
|
||||
property: QueryEditorProperty;
|
||||
reduce: QueryEditorProperty;
|
||||
parameters?: QueryEditorFunctionParameterExpression[];
|
||||
focus?: boolean;
|
||||
}
|
||||
|
||||
export interface QueryEditorGroupByExpression extends QueryEditorExpression {
|
||||
property: QueryEditorProperty;
|
||||
interval?: QueryEditorProperty;
|
||||
focus?: boolean;
|
||||
}
|
||||
|
||||
export interface QueryEditorArrayExpression extends QueryEditorExpression {
|
||||
expressions: QueryEditorExpression[] | QueryEditorArrayExpression[];
|
||||
}
|
||||
|
||||
export interface QueryEditorReduceExpression extends QueryEditorExpression {
|
||||
property: QueryEditorProperty;
|
||||
reduce: QueryEditorProperty;
|
||||
parameters?: QueryEditorFunctionParameterExpression[];
|
||||
focus?: boolean;
|
||||
}
|
||||
|
||||
export interface QueryEditorPropertyExpression extends QueryEditorExpression {
|
||||
property: QueryEditorProperty;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { escapeRegExp } from 'lodash';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import {
|
||||
BuilderQueryExpression,
|
||||
BuilderQueryEditorExpressionType,
|
||||
BuilderQueryEditorPropertyType,
|
||||
BuilderQueryEditorReduceExpression,
|
||||
BuilderQueryEditorWhereExpression,
|
||||
BuilderQueryEditorGroupByExpression,
|
||||
BuilderQueryEditorOrderByExpression,
|
||||
BuilderQueryEditorPropertyExpression,
|
||||
} from '../../dataquery.gen';
|
||||
import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types';
|
||||
|
||||
const DYNAMIC_TYPE_ARRAY_DELIMITER = '["`indexer`"]';
|
||||
export const inputFieldSize = 20;
|
||||
|
||||
export const valueToDefinition = (name: string) => {
|
||||
return {
|
||||
value: name,
|
||||
label: name.replace(new RegExp(escapeRegExp(DYNAMIC_TYPE_ARRAY_DELIMITER), 'g'), '[ ]'),
|
||||
};
|
||||
};
|
||||
|
||||
export const DEFAULT_LOGS_BUILDER_QUERY: BuilderQueryExpression = {
|
||||
columns: { columns: [], type: BuilderQueryEditorExpressionType.Property },
|
||||
from: {
|
||||
type: BuilderQueryEditorExpressionType.Property,
|
||||
property: { type: BuilderQueryEditorPropertyType.String, name: '' },
|
||||
},
|
||||
groupBy: { expressions: [], type: BuilderQueryEditorExpressionType.Group_by },
|
||||
reduce: { expressions: [], type: BuilderQueryEditorExpressionType.Reduce },
|
||||
where: { expressions: [], type: BuilderQueryEditorExpressionType.And },
|
||||
limit: 1000,
|
||||
};
|
||||
|
||||
export const OPERATORS_BY_TYPE: Record<string, Array<SelectableValue<string>>> = {
|
||||
string: [
|
||||
{ label: '==', value: '==' },
|
||||
{ label: '!=', value: '!=' },
|
||||
{ label: 'contains', value: 'contains' },
|
||||
{ label: '!contains', value: '!contains' },
|
||||
{ label: 'startswith', value: 'startswith' },
|
||||
{ label: 'endswith', value: 'endswith' },
|
||||
],
|
||||
int: [
|
||||
{ label: '==', value: '==' },
|
||||
{ label: '!=', value: '!=' },
|
||||
{ label: '>', value: '>' },
|
||||
{ label: '<', value: '<' },
|
||||
{ label: '>=', value: '>=' },
|
||||
{ label: '<=', value: '<=' },
|
||||
],
|
||||
datetime: [
|
||||
{ label: 'before', value: '<' },
|
||||
{ label: 'after', value: '>' },
|
||||
{ label: 'between', value: 'between' },
|
||||
],
|
||||
bool: [
|
||||
{ label: '==', value: '==' },
|
||||
{ label: '!=', value: '!=' },
|
||||
],
|
||||
};
|
||||
|
||||
export const toOperatorOptions = (type: string): Array<SelectableValue<string>> => {
|
||||
return OPERATORS_BY_TYPE[type] || OPERATORS_BY_TYPE.string;
|
||||
};
|
||||
|
||||
export const removeExtraQuotes = (value: string): string => {
|
||||
let strValue = String(value).trim();
|
||||
if ((strValue.startsWith("'") && strValue.endsWith("'")) || (strValue.startsWith('"') && strValue.endsWith('"'))) {
|
||||
return strValue.slice(1, -1);
|
||||
}
|
||||
return strValue;
|
||||
};
|
||||
|
||||
export interface BuildAndUpdateOptions {
|
||||
query: AzureMonitorQuery;
|
||||
onQueryUpdate: (newQuery: AzureMonitorQuery) => void;
|
||||
allColumns: AzureLogAnalyticsMetadataColumn[];
|
||||
limit?: number;
|
||||
reduce?: BuilderQueryEditorReduceExpression[];
|
||||
where?: BuilderQueryEditorWhereExpression[];
|
||||
fuzzySearch?: BuilderQueryEditorWhereExpression[];
|
||||
groupBy?: BuilderQueryEditorGroupByExpression[];
|
||||
orderBy?: BuilderQueryEditorOrderByExpression[];
|
||||
columns?: string[];
|
||||
from?: BuilderQueryEditorPropertyExpression;
|
||||
}
|
||||
|
||||
export const aggregateOptions = [
|
||||
{ label: 'sum', value: 'sum' },
|
||||
{ label: 'avg', value: 'avg' },
|
||||
{ label: 'percentile', value: 'percentile' },
|
||||
{ label: 'count', value: 'count' },
|
||||
{ label: 'min', value: 'min' },
|
||||
{ label: 'max', value: 'max' },
|
||||
{ label: 'dcount', value: 'dcount' },
|
||||
{ label: 'stdev', value: 'stdev' },
|
||||
];
|
||||
+36
@@ -19,6 +19,10 @@ jest.mock('@grafana/runtime', () => ({
|
||||
}
|
||||
return val;
|
||||
},
|
||||
getVariables: () => [
|
||||
{ name: 'var1', current: { value: 'value1' } },
|
||||
{ name: 'var2', current: { value: 'value2' } },
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -43,6 +47,7 @@ describe('LogsQueryEditor', () => {
|
||||
delete query?.subscription;
|
||||
delete query?.azureLogAnalytics?.resources;
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
const basicLogsEnabled = false;
|
||||
|
||||
render(
|
||||
@@ -51,6 +56,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={basicLogsEnabled}
|
||||
/>
|
||||
@@ -94,6 +100,7 @@ describe('LogsQueryEditor', () => {
|
||||
delete query?.azureLogAnalytics?.resources;
|
||||
const basicLogsEnabled = false;
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
render(
|
||||
<LogsQueryEditor
|
||||
@@ -101,6 +108,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={basicLogsEnabled}
|
||||
/>
|
||||
@@ -129,6 +137,7 @@ describe('LogsQueryEditor', () => {
|
||||
delete query?.azureLogAnalytics?.resources;
|
||||
const basicLogsEnabled = false;
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
render(
|
||||
<LogsQueryEditor
|
||||
@@ -136,6 +145,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={basicLogsEnabled}
|
||||
/>
|
||||
@@ -164,6 +174,7 @@ describe('LogsQueryEditor', () => {
|
||||
delete query?.azureLogAnalytics?.resources;
|
||||
const basicLogsEnabled = false;
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
render(
|
||||
<LogsQueryEditor
|
||||
@@ -171,6 +182,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={basicLogsEnabled}
|
||||
/>
|
||||
@@ -203,6 +215,7 @@ describe('LogsQueryEditor', () => {
|
||||
const query = createMockQuery();
|
||||
const basicLogsEnabled = false;
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
render(
|
||||
<LogsQueryEditor
|
||||
@@ -210,6 +223,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={basicLogsEnabled}
|
||||
/>
|
||||
@@ -233,6 +247,7 @@ describe('LogsQueryEditor', () => {
|
||||
const query = createMockQuery();
|
||||
const basicLogsEnabled = false;
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
const date = dateTime(new Date());
|
||||
render(
|
||||
@@ -241,6 +256,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={basicLogsEnabled}
|
||||
data={{
|
||||
@@ -274,6 +290,7 @@ describe('LogsQueryEditor', () => {
|
||||
});
|
||||
const basicLogsEnabled = true;
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
@@ -282,6 +299,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={basicLogsEnabled}
|
||||
/>
|
||||
@@ -300,6 +318,7 @@ describe('LogsQueryEditor', () => {
|
||||
});
|
||||
const basicLogsEnabled = true;
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
@@ -308,6 +327,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={basicLogsEnabled}
|
||||
/>
|
||||
@@ -328,6 +348,7 @@ describe('LogsQueryEditor', () => {
|
||||
});
|
||||
const basicLogsEnabled = false;
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
@@ -336,6 +357,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={basicLogsEnabled}
|
||||
/>
|
||||
@@ -354,6 +376,7 @@ describe('LogsQueryEditor', () => {
|
||||
});
|
||||
const basicLogsEnabled = true;
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
@@ -362,6 +385,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={basicLogsEnabled}
|
||||
/>
|
||||
@@ -382,6 +406,7 @@ describe('LogsQueryEditor', () => {
|
||||
});
|
||||
const basicLogsEnabled = true;
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
@@ -390,6 +415,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={basicLogsEnabled}
|
||||
/>
|
||||
@@ -412,6 +438,7 @@ describe('LogsQueryEditor', () => {
|
||||
basicLogsQuery: true,
|
||||
},
|
||||
});
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
mockDatasource.azureLogAnalyticsDatasource.getBasicLogsQueryUsage.mockResolvedValue(0);
|
||||
await act(async () => {
|
||||
@@ -421,6 +448,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={true}
|
||||
/>
|
||||
@@ -447,6 +475,7 @@ describe('LogsQueryEditor', () => {
|
||||
basicLogsQuery: true,
|
||||
},
|
||||
});
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
mockDatasource.azureLogAnalyticsDatasource.getBasicLogsQueryUsage.mockResolvedValue(0.45);
|
||||
await act(async () => {
|
||||
@@ -456,6 +485,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={true}
|
||||
/>
|
||||
@@ -481,6 +511,7 @@ describe('LogsQueryEditor', () => {
|
||||
query: '',
|
||||
},
|
||||
});
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
mockDatasource.azureLogAnalyticsDatasource.getBasicLogsQueryUsage.mockResolvedValue(0.5);
|
||||
await act(async () => {
|
||||
@@ -490,6 +521,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={true}
|
||||
/>
|
||||
@@ -509,6 +541,7 @@ describe('LogsQueryEditor', () => {
|
||||
},
|
||||
});
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
@@ -517,6 +550,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={false}
|
||||
/>
|
||||
@@ -532,6 +566,7 @@ describe('LogsQueryEditor', () => {
|
||||
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
|
||||
const query = { ...createMockQuery(), azureLogAnalytics: undefined };
|
||||
const onChange = jest.fn();
|
||||
const onQueryChange = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
@@ -540,6 +575,7 @@ describe('LogsQueryEditor', () => {
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
onQueryChange={onQueryChange}
|
||||
setError={() => {}}
|
||||
basicLogsEnabled={false}
|
||||
/>
|
||||
|
||||
+59
-11
@@ -2,12 +2,14 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
import { PanelData, TimeRange } from '@grafana/data';
|
||||
import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/plugin-ui';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { Alert, LinkButton, Text, TextLink } from '@grafana/ui';
|
||||
import { config, getTemplateSrv } from '@grafana/runtime';
|
||||
import { Alert, LinkButton, Space, Text, TextLink } from '@grafana/ui';
|
||||
|
||||
import { LogsEditorMode } from '../../dataquery.gen';
|
||||
import Datasource from '../../datasource';
|
||||
import { selectors } from '../../e2e/selectors';
|
||||
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, ResultFormat, EngineSchema } from '../../types';
|
||||
import { LogsQueryBuilder } from '../LogsQueryBuilder/LogsQueryBuilder';
|
||||
import ResourceField from '../ResourceField';
|
||||
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types';
|
||||
import { parseResourceDetails } from '../ResourcePicker/utils';
|
||||
@@ -27,6 +29,7 @@ interface LogsQueryEditorProps {
|
||||
basicLogsEnabled: boolean;
|
||||
subscriptionId?: string;
|
||||
onChange: (newQuery: AzureMonitorQuery) => void;
|
||||
onQueryChange: (newQuery: AzureMonitorQuery) => void;
|
||||
variableOptionGroup: { label: string; options: AzureMonitorOption[] };
|
||||
setError: (source: string, error: AzureMonitorErrorish | undefined) => void;
|
||||
hideFormatAs?: boolean;
|
||||
@@ -41,6 +44,7 @@ const LogsQueryEditor = ({
|
||||
subscriptionId,
|
||||
variableOptionGroup,
|
||||
onChange,
|
||||
onQueryChange,
|
||||
setError,
|
||||
hideFormatAs,
|
||||
timeRange,
|
||||
@@ -54,6 +58,7 @@ const LogsQueryEditor = ({
|
||||
const templateSrv = getTemplateSrv();
|
||||
const from = templateSrv?.replace('$__from');
|
||||
const to = templateSrv?.replace('$__to');
|
||||
const templateVariableOptions = templateSrv.getVariables();
|
||||
|
||||
const disableRow = (row: ResourceRow, selectedRows: ResourceRowGroup) => {
|
||||
if (selectedRows.length === 0) {
|
||||
@@ -93,6 +98,35 @@ const LogsQueryEditor = ({
|
||||
}
|
||||
}, [basicLogsEnabled, onChange, query, showBasicLogsToggle]);
|
||||
|
||||
useEffect(() => {
|
||||
const hasRawKql = !!query.azureLogAnalytics?.query;
|
||||
const hasNoBuilder = !query.azureLogAnalytics?.builderQuery;
|
||||
const modeUnset = query.azureLogAnalytics?.mode === undefined;
|
||||
|
||||
if (hasRawKql && hasNoBuilder && modeUnset) {
|
||||
onChange({
|
||||
...query,
|
||||
azureLogAnalytics: {
|
||||
...query.azureLogAnalytics,
|
||||
mode: LogsEditorMode.Raw,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [query, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (query.azureLogAnalytics?.mode === LogsEditorMode.Raw && query.azureLogAnalytics?.builderQuery !== undefined) {
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureLogAnalytics: {
|
||||
...query.azureLogAnalytics,
|
||||
builderQuery: undefined,
|
||||
query: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [query.azureLogAnalytics?.mode, onQueryChange, query]);
|
||||
|
||||
useEffect(() => {
|
||||
const getBasicLogsUsage = async (query: AzureMonitorQuery) => {
|
||||
try {
|
||||
@@ -197,15 +231,29 @@ const LogsQueryEditor = ({
|
||||
/>
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
<QueryField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
setError={setError}
|
||||
schema={schema}
|
||||
/>
|
||||
<Space />
|
||||
{query.azureLogAnalytics?.mode === LogsEditorMode.Builder &&
|
||||
!!config.featureToggles.azureMonitorLogsBuilderEditor ? (
|
||||
<LogsQueryBuilder
|
||||
query={query}
|
||||
schema={schema!}
|
||||
basicLogsEnabled={basicLogsEnabled}
|
||||
onQueryChange={onQueryChange}
|
||||
templateVariableOptions={templateVariableOptions}
|
||||
datasource={datasource}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
) : (
|
||||
<QueryField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
setError={setError}
|
||||
schema={schema}
|
||||
/>
|
||||
)}
|
||||
{dataIngestedWarning}
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
|
||||
+10
-4
@@ -85,6 +85,7 @@ export function TimeManagement({ query, onQueryChange: onChange, schema }: Azure
|
||||
setDisabledTimePicker(false);
|
||||
}
|
||||
}, [query.azureLogAnalytics?.basicLogsQuery]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineField
|
||||
@@ -92,23 +93,28 @@ export function TimeManagement({ query, onQueryChange: onChange, schema }: Azure
|
||||
tooltip={
|
||||
<span>
|
||||
Specifies the time-range used to query. The <code>Query</code> option will only use time-ranges specified in
|
||||
the query. <code>Dashboard</code> will only use the Grafana time-range.
|
||||
the query. <code>Dashboard</code> will only use the Grafana time-range. In Logs Builder mode, only Dashboard
|
||||
time will be used.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<RadioButtonGroup
|
||||
options={[
|
||||
{ label: 'Query', value: 'query' },
|
||||
{ label: 'Query', value: 'query', disabled: query.azureLogAnalytics?.mode === 'builder' },
|
||||
{ label: 'Dashboard', value: 'dashboard' },
|
||||
]}
|
||||
value={query.azureLogAnalytics?.dashboardTime ? 'dashboard' : 'query'}
|
||||
value={
|
||||
query.azureLogAnalytics?.dashboardTime || query.azureLogAnalytics?.mode === 'builder'
|
||||
? 'dashboard'
|
||||
: 'query'
|
||||
}
|
||||
size={'md'}
|
||||
onChange={(val) => onChange(setDashboardTime(query, val))}
|
||||
disabled={disabledTimePicker}
|
||||
disabledOptions={disabledTimePicker ? ['query'] : []}
|
||||
/>
|
||||
</InlineField>
|
||||
{query.azureLogAnalytics?.dashboardTime && (
|
||||
{(query.azureLogAnalytics?.dashboardTime || query.azureLogAnalytics?.mode === 'builder') && (
|
||||
<InlineField
|
||||
label="Time Column"
|
||||
tooltip={
|
||||
|
||||
+14
-6
@@ -23,12 +23,18 @@ jest.mock('@grafana/ui', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
___esModule: true,
|
||||
...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' } },
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -99,11 +105,13 @@ describe('Azure Monitor QueryEditor', () => {
|
||||
const metrics = await screen.findByLabelText(/Service/);
|
||||
await selectOptionInTest(metrics, 'Logs');
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
refId: mockQuery.refId,
|
||||
datasource: mockQuery.datasource,
|
||||
queryType: AzureQueryType.LogAnalytics,
|
||||
});
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
refId: mockQuery.refId,
|
||||
datasource: mockQuery.datasource,
|
||||
queryType: AzureQueryType.LogAnalytics,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('displays error messages from frontend Azure calls', async () => {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { debounce } from 'lodash';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
|
||||
import { CoreApp, QueryEditorProps } from '@grafana/data';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Alert, Button, CodeEditor, Space } from '@grafana/ui';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, CodeEditor, Space } from '@grafana/ui';
|
||||
|
||||
import AzureMonitorDatasource from '../../datasource';
|
||||
import { selectors } from '../../e2e/selectors';
|
||||
@@ -97,26 +96,14 @@ const QueryEditor = ({
|
||||
onClose={() => setAzureLogsCheatSheetModalOpen(false)}
|
||||
onChange={(a) => onChange({ ...a, queryType: AzureQueryType.LogAnalytics })}
|
||||
/>
|
||||
<div className={css({ display: 'flex', alignItems: 'center' })}>
|
||||
<QueryHeader query={query} onQueryChange={onQueryChange} />
|
||||
{query.queryType === AzureQueryType.LogAnalytics && (
|
||||
<Button
|
||||
aria-label="Azure logs kick start your query button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setAzureLogsCheatSheetModalOpen((prevValue) => !prevValue);
|
||||
|
||||
reportInteraction('grafana_azure_logs_query_patterns_opened', {
|
||||
version: 'v2',
|
||||
editorMode: query.azureLogAnalytics,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Kick start your query
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<QueryHeader
|
||||
query={query}
|
||||
onQueryChange={onQueryChange}
|
||||
setAzureLogsCheatSheetModalOpen={setAzureLogsCheatSheetModalOpen}
|
||||
onRunQuery={baseOnRunQuery}
|
||||
data={data}
|
||||
app={app}
|
||||
/>
|
||||
<EditorForQueryType
|
||||
data={data}
|
||||
subscriptionId={subscriptionId}
|
||||
@@ -124,11 +111,11 @@ const QueryEditor = ({
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onQueryChange}
|
||||
onQueryChange={onChange}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
setError={setError}
|
||||
range={range}
|
||||
/>
|
||||
|
||||
{errorMessage && (
|
||||
<>
|
||||
<Space v={2} />
|
||||
@@ -146,6 +133,8 @@ interface EditorForQueryTypeProps extends Omit<AzureMonitorQueryEditorProps, 'on
|
||||
basicLogsEnabled: boolean;
|
||||
variableOptionGroup: { label: string; options: AzureMonitorOption[] };
|
||||
setError: (source: string, error: AzureMonitorErrorish | undefined) => 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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -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<React.SetStateAction<boolean>>;
|
||||
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<LogsEditorMode | null>(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<AzureQueryType>) => {
|
||||
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 (
|
||||
<span data-testid={selectors.components.queryEditor.header.select}>
|
||||
<EditorHeader>
|
||||
<ConfirmModal
|
||||
isOpen={showModeSwitchWarning}
|
||||
title="Switch editor mode?"
|
||||
body={
|
||||
pendingModeChange === LogsEditorMode.Builder
|
||||
? 'Switching to Builder will discard your current KQL query and clear the KQL editor. Are you sure?'
|
||||
: 'Switching to KQL will discard your current builder settings. Are you sure?'
|
||||
}
|
||||
confirmText={`Switch to ${pendingModeChange === LogsEditorMode.Builder ? 'Builder' : 'KQL'}`}
|
||||
onConfirm={() => {
|
||||
if (pendingModeChange) {
|
||||
applyModeChange(pendingModeChange);
|
||||
}
|
||||
setShowModeSwitchWarning(false);
|
||||
setPendingModeChange(null);
|
||||
}}
|
||||
onDismiss={() => {
|
||||
setShowModeSwitchWarning(false);
|
||||
setPendingModeChange(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
<InlineSelect
|
||||
label="Service"
|
||||
value={query.queryType === AzureQueryType.TraceExemplar ? AzureQueryType.AzureTraces : query.queryType}
|
||||
@@ -43,6 +135,45 @@ export const QueryHeader = ({ query, onQueryChange }: QueryTypeFieldProps) => {
|
||||
options={queryTypes}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{query.queryType === AzureQueryType.LogAnalytics && query.azureLogAnalytics?.mode === LogsEditorMode.Raw && (
|
||||
<Button
|
||||
aria-label="Azure logs kick start your query button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setAzureLogsCheatSheetModalOpen((prev) => !prev);
|
||||
reportInteraction('grafana_azure_logs_query_patterns_opened', {
|
||||
version: 'v2',
|
||||
editorMode: query.azureLogAnalytics,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Kick start your query
|
||||
</Button>
|
||||
)}
|
||||
<FlexItem grow={1} />
|
||||
{query.queryType === AzureQueryType.LogAnalytics && !!config.featureToggles.azureMonitorLogsBuilderEditor && (
|
||||
<RadioButtonGroup
|
||||
size="sm"
|
||||
options={EDITOR_MODES}
|
||||
value={query.azureLogAnalytics?.mode || LogsEditorMode.Builder}
|
||||
onChange={onLogsModeChange}
|
||||
data-testid="azure-query-header-logs-radio-button"
|
||||
/>
|
||||
)}
|
||||
{query.azureLogAnalytics?.mode === LogsEditorMode.Builder &&
|
||||
!!config.featureToggles.azureMonitorLogsBuilderEditor &&
|
||||
app !== CoreApp.Explore && (
|
||||
<Button
|
||||
variant="primary"
|
||||
icon={isLoading ? 'spinner' : 'play'}
|
||||
size="sm"
|
||||
onClick={onRunQuery}
|
||||
data-testid={selectors.components.queryEditor.logsQueryEditor.runQuery.button}
|
||||
>
|
||||
Run query
|
||||
</Button>
|
||||
)}
|
||||
</EditorHeader>
|
||||
</span>
|
||||
);
|
||||
|
||||
+10
-2
@@ -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(<VariableEditor {...defaultProps} onChange={onChange} />);
|
||||
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',
|
||||
|
||||
+2
@@ -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}
|
||||
|
||||
@@ -50,7 +50,7 @@ const FormatAsField = ({
|
||||
value={resultFormat}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
width={38}
|
||||
width={20}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<string>;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorColumnsExpression: Partial<BuilderQueryEditorColumnsExpression> = {
|
||||
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<BuilderQueryEditorWhereExpressionItems>;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorWhereExpression: Partial<BuilderQueryEditorWhereExpression> = {
|
||||
expressions: [],
|
||||
};
|
||||
|
||||
export interface BuilderQueryEditorWhereExpressionArray {
|
||||
expressions: Array<BuilderQueryEditorWhereExpression>;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorWhereExpressionArray: Partial<BuilderQueryEditorWhereExpressionArray> = {
|
||||
expressions: [],
|
||||
};
|
||||
|
||||
export interface BuilderQueryEditorFunctionParameterExpression {
|
||||
fieldType: BuilderQueryEditorPropertyType;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface BuilderQueryEditorReduceExpression {
|
||||
focus?: boolean;
|
||||
parameters?: Array<BuilderQueryEditorFunctionParameterExpression>;
|
||||
property?: BuilderQueryEditorProperty;
|
||||
reduce?: BuilderQueryEditorProperty;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorReduceExpression: Partial<BuilderQueryEditorReduceExpression> = {
|
||||
parameters: [],
|
||||
};
|
||||
|
||||
export interface BuilderQueryEditorReduceExpressionArray {
|
||||
expressions: Array<BuilderQueryEditorReduceExpression>;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorReduceExpressionArray: Partial<BuilderQueryEditorReduceExpressionArray> = {
|
||||
expressions: [],
|
||||
};
|
||||
|
||||
export interface BuilderQueryEditorGroupByExpression {
|
||||
focus?: boolean;
|
||||
interval?: BuilderQueryEditorProperty;
|
||||
property?: BuilderQueryEditorProperty;
|
||||
type?: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export interface BuilderQueryEditorGroupByExpressionArray {
|
||||
expressions: Array<BuilderQueryEditorGroupByExpression>;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorGroupByExpressionArray: Partial<BuilderQueryEditorGroupByExpressionArray> = {
|
||||
expressions: [],
|
||||
};
|
||||
|
||||
export interface BuilderQueryEditorOrderByExpression {
|
||||
order: BuilderQueryEditorOrderByOptions;
|
||||
property: BuilderQueryEditorProperty;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export interface BuilderQueryEditorOrderByExpressionArray {
|
||||
expressions: Array<BuilderQueryEditorOrderByExpression>;
|
||||
type: BuilderQueryEditorExpressionType;
|
||||
}
|
||||
|
||||
export const defaultBuilderQueryEditorOrderByExpressionArray: Partial<BuilderQueryEditorOrderByExpressionArray> = {
|
||||
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.
|
||||
|
||||
@@ -76,6 +76,9 @@ export const components = {
|
||||
formatSelection: {
|
||||
input: 'data-testid format-selection',
|
||||
},
|
||||
runQuery: {
|
||||
button: 'data-testid run-query',
|
||||
},
|
||||
},
|
||||
argsQueryEditor: {
|
||||
container: {
|
||||
|
||||
@@ -264,6 +264,15 @@ export interface AzureAPIResponse<T> {
|
||||
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<string>;
|
||||
export type QueryEditorOperatorValueType = QueryEditorOperatorType | QueryEditorOperatorType[];
|
||||
|
||||
export interface QueryEditorOperator<T = QueryEditorOperatorValueType> {
|
||||
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',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user