diff --git a/.betterer.results b/.betterer.results index b0eb03225f6..e2f3dade903 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3163,9 +3163,8 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"] + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"] ], "public/app/plugins/datasource/azuremonitor/components/MonitorConfig.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/docs/sources/datasources/azure-monitor/query-editor/index.md b/docs/sources/datasources/azure-monitor/query-editor/index.md index 53e14a2c3cf..b7be89e709c 100644 --- a/docs/sources/datasources/azure-monitor/query-editor/index.md +++ b/docs/sources/datasources/azure-monitor/query-editor/index.md @@ -147,10 +147,10 @@ The Azure documentation includes resources to help you learn KQL: - [SQL to Kusto cheat sheet](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/sqlcheatsheet) > **Time-range:** The time-range that will be used for the query can be modified via the time-range switch. Selecting `Query` will only make use of time-ranges specified within the query. -> Specifying `Intersection` will make use of the intersection between the time-ranges within the query and the Grafana time-range. -> If there are no time-ranges specified within the query, the Grafana time-range will be used. +> Specifying `Dashboard` will only make use of the Grafana time-range. +> If there are no time-ranges specified within the query, the default Log Analytics time-range will apply. > For more details on this change, refer to the [Azure Monitor Logs API documentation](https://learn.microsoft.com/en-us/rest/api/loganalytics/dataaccess/query/get?tabs=HTTP#uri-parameters). -> Note: v9.4.12, v10.0, and v10.0.1 do not have this switch and will implicitly use the intersection of the Grafana and query time-ranges. +> If the `Intersection` option was previously chosen it will be migrated by default to `Dashboard`. This example query returns a virtual machine's CPU performance, averaged over 5ms time grains: diff --git a/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts index 68a300d4b50..86c383bf696 100644 --- a/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts @@ -165,7 +165,11 @@ export const defaultAzureMetricQuery: Partial = { */ export interface AzureLogsQuery { /** - * If set to true the intersection of time ranges specified in the query and Grafana will be used. Otherwise the query time ranges will be used. Defaults to false + * 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. + */ + dashboardTime?: boolean; + /** + * @deprecated Use dashboardTime instead */ intersectTime?: boolean; /** @@ -185,7 +189,11 @@ export interface AzureLogsQuery { */ resultFormat?: ResultFormat; /** - * Workspace ID. This was removed in Grafana 8, but remains for backwards compat + * If dashboardTime is set to true this value dictates which column the time filter will be applied to. Defaults to the first tables timeSpan column, the first datetime column found, or TimeGenerated + */ + timeColumn?: string; + /** + * Workspace ID. This was removed in Grafana 8, but remains for backwards compat. */ workspace?: string; } diff --git a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go index 4e3f9667865..36d3d3767e5 100644 --- a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go @@ -118,7 +118,10 @@ type AppInsightsMetricNameQueryKind string // Azure Monitor Logs sub-query properties type AzureLogsQuery struct { - // If set to true the intersection of time ranges specified in the query and Grafana will be used. Otherwise the query time ranges will be used. Defaults to false + // 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. + DashboardTime *bool `json:"dashboardTime,omitempty"` + + // @deprecated Use dashboardTime instead IntersectTime *bool `json:"intersectTime,omitempty"` // KQL query to be executed. @@ -131,7 +134,10 @@ type AzureLogsQuery struct { Resources []string `json:"resources,omitempty"` ResultFormat *ResultFormat `json:"resultFormat,omitempty"` - // Workspace ID. This was removed in Grafana 8, but remains for backwards compat + // If dashboardTime is set to true this value dictates which column the time filter will be applied to. Defaults to the first tables timeSpan column, the first datetime column found, or TimeGenerated + TimeColumn *string `json:"timeColumn,omitempty"` + + // Workspace ID. This was removed in Grafana 8, but remains for backwards compat. Workspace *string `json:"workspace,omitempty"` } diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go index b136cf6c3ae..ad10bfaf363 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go @@ -47,7 +47,8 @@ type AzureLogAnalyticsQuery struct { Resources []string QueryType string AppInsightsQuery bool - IntersectTime bool + DashboardTime bool + TimeColumn string } func (e *AzureLogAnalyticsDatasource) ResourceRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) (http.ResponseWriter, error) { @@ -107,7 +108,8 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries traceExploreQuery := "" traceParentExploreQuery := "" traceLogsExploreQuery := "" - intersectTime := false + dashboardTime := false + timeColumn := "" if query.QueryType == string(dataquery.AzureQueryTypeAzureLogAnalytics) { queryJSONModel := types.LogJSONQuery{} err := json.Unmarshal(query.JSON, &queryJSONModel) @@ -143,8 +145,16 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries queryString = *azureLogAnalyticsTarget.Query } - if azureLogAnalyticsTarget.IntersectTime != nil { - intersectTime = *azureLogAnalyticsTarget.IntersectTime + if azureLogAnalyticsTarget.DashboardTime != nil { + dashboardTime = *azureLogAnalyticsTarget.DashboardTime + if dashboardTime { + if azureLogAnalyticsTarget.TimeColumn != nil { + timeColumn = *azureLogAnalyticsTarget.TimeColumn + } else { + // Final fallback to TimeGenerated if no column is provided + timeColumn = "TimeGenerated" + } + } } } @@ -218,7 +228,8 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries return nil, fmt.Errorf("failed to create traces logs explore query: %s", err) } - intersectTime = true + dashboardTime = true + timeColumn = "timestamp" } apiURL := getApiURL(resourceOrWorkspace, appInsightsQuery) @@ -241,7 +252,8 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(ctx context.Context, queries TraceParentExploreQuery: traceParentExploreQuery, TraceLogsExploreQuery: traceLogsExploreQuery, AppInsightsQuery: appInsightsQuery, - IntersectTime: intersectTime, + DashboardTime: dashboardTime, + TimeColumn: timeColumn, }) } @@ -432,11 +444,14 @@ func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, queryUR "query": query.Query, } - if query.IntersectTime { + if query.DashboardTime { from := query.TimeRange.From.Format(time.RFC3339) to := query.TimeRange.To.Format(time.RFC3339) timespan := fmt.Sprintf("%s/%s", from, to) body["timespan"] = timespan + body["query_datetimescope_from"] = from + body["query_datetimescope_to"] = to + body["query_datetimescope_column"] = query.TimeColumn } if len(query.Resources) > 1 && query.QueryType == string(dataquery.AzureQueryTypeAzureLogAnalytics) && !query.AppInsightsQuery { diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go index a904f158d37..cefd8c116d9 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go @@ -107,7 +107,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", "query": "Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", "resultFormat": "%s", - "intersectTime": false + "dashboardTime": false } }`, types.TimeSeries)), RefID: "A", @@ -126,7 +126,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", "query": "Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", "resultFormat": "%s", - "intersectTime": false + "dashboardTime": false } }`, types.TimeSeries)), Query: "Perf | where ['TimeGenerated'] >= datetime('2018-03-15T13:00:00Z') and ['TimeGenerated'] <= datetime('2018-03-15T13:34:00Z') | where ['Computer'] in ('comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, 34000ms), Computer", @@ -134,7 +134,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TimeRange: timeRange, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), AppInsightsQuery: false, - IntersectTime: false, + DashboardTime: false, }, }, Err: require.NoError, @@ -172,7 +172,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { Resources: []string{}, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), AppInsightsQuery: false, - IntersectTime: false, + DashboardTime: false, }, }, Err: require.NoError, @@ -210,7 +210,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { Resources: []string{}, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), AppInsightsQuery: false, - IntersectTime: false, + DashboardTime: false, }, }, Err: require.NoError, @@ -225,7 +225,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", "query": "Perf", "resultFormat": "%s", - "intersectTime": false + "dashboardTime": false } }`, types.TimeSeries)), RefID: "A", @@ -243,14 +243,14 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", "query": "Perf", "resultFormat": "%s", - "intersectTime": false + "dashboardTime": false } }`, types.TimeSeries)), Query: "Perf", Resources: []string{"/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace"}, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), AppInsightsQuery: false, - IntersectTime: false, + DashboardTime: false, }, }, Err: require.NoError, @@ -265,7 +265,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "resources": ["/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace2"], "query": "Perf", "resultFormat": "%s", - "intersectTime": false + "dashboardTime": false } }`, types.TimeSeries)), RefID: "A", @@ -284,7 +284,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "resources": ["/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace2"], "query": "Perf", "resultFormat": "%s", - "intersectTime": false + "dashboardTime": false } }`, types.TimeSeries)), Query: "Perf", @@ -292,7 +292,52 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TimeRange: timeRange, QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), AppInsightsQuery: false, - IntersectTime: false, + DashboardTime: false, + }, + }, + Err: require.NoError, + }, + { + name: "Query that uses dashboard time", + queryModel: []backend.DataQuery{ + { + JSON: []byte(fmt.Sprintf(`{ + "queryType": "Azure Log Analytics", + "azureLogAnalytics": { + "resources": ["/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace"], + "query": "Perf", + "resultFormat": "%s", + "dashboardTime": true, + "timeColumn": "TimeGenerated" + } + }`, types.TimeSeries)), + RefID: "A", + TimeRange: timeRange, + QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + }, + }, + azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ + { + RefID: "A", + ResultFormat: types.TimeSeries, + URL: "v1/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace/query", + JSON: []byte(fmt.Sprintf(`{ + "queryType": "Azure Log Analytics", + "azureLogAnalytics": { + "resources": ["/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace"], + "query": "Perf", + "resultFormat": "%s", + "dashboardTime": true, + "timeColumn": "TimeGenerated" + } + }`, types.TimeSeries)), + Query: "Perf", + Resources: []string{"/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace"}, + TimeRange: timeRange, + QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), + AppInsightsQuery: false, + DashboardTime: true, + TimeColumn: "TimeGenerated", }, }, Err: require.NoError, @@ -373,7 +418,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -451,7 +497,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -526,7 +573,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"${__data.fields.traceID}\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -604,7 +652,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -687,7 +736,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -770,7 +820,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -853,7 +904,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -928,7 +980,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"${__data.fields.traceID}\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -1006,7 +1059,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -1052,7 +1106,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { TraceLogsExploreQuery: "union availabilityResults,\n" + "customEvents,\n" + "dependencies,\n" + "exceptions,\n" + "pageViews,\n" + "requests,\n" + "traces\n" + "| where operation_Id == \"test-op-id\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -1134,7 +1189,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').traces\n" + "| where operation_Id == \"op-id-multi\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -1213,7 +1269,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').traces\n" + "| where operation_Id == \"${__data.fields.traceID}\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -1295,7 +1352,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r2').traces\n" + "| where operation_Id == \"op-id-multi\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -1384,7 +1442,8 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { "app('/subscriptions/test-sub/resourcegroups/test-rg/providers/microsoft.insights/components/r3').traces\n" + "| where operation_Id == \"op-id-non-overlapping\"", AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }, }, Err: require.NoError, @@ -1411,7 +1470,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { req, err := ds.createRequest(ctx, url, &AzureLogAnalyticsQuery{ Resources: []string{"r"}, Query: "Perf", - IntersectTime: false, + DashboardTime: false, AppInsightsQuery: false, }) require.NoError(t, err) @@ -1430,30 +1489,6 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { } }) - t.Run("creates a request with timespan", func(t *testing.T) { - ds := AzureLogAnalyticsDatasource{} - req, err := ds.createRequest(ctx, url, &AzureLogAnalyticsQuery{ - Resources: []string{"r"}, - Query: "Perf", - IntersectTime: true, - AppInsightsQuery: false, - }) - require.NoError(t, err) - if req.URL.String() != url { - t.Errorf("Expecting %s, got %s", url, req.URL.String()) - } - expectedHeaders := http.Header{"Content-Type": []string{"application/json"}} - if !cmp.Equal(req.Header, expectedHeaders) { - t.Errorf("Unexpected HTTP headers: %v", cmp.Diff(req.Header, expectedHeaders)) - } - expectedBody := `{"query":"Perf","timespan":"0001-01-01T00:00:00Z/0001-01-01T00:00:00Z"}` - body, err := io.ReadAll(req.Body) - require.NoError(t, err) - if !cmp.Equal(string(body), expectedBody) { - t.Errorf("Unexpected Body: %v", cmp.Diff(string(body), expectedBody)) - } - }) - t.Run("creates a request with multiple resources", func(t *testing.T) { ds := AzureLogAnalyticsDatasource{} req, err := ds.createRequest(ctx, url, &AzureLogAnalyticsQuery{ @@ -1461,7 +1496,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { Query: "Perf", QueryType: string(dataquery.AzureQueryTypeAzureLogAnalytics), AppInsightsQuery: false, - IntersectTime: false, + DashboardTime: false, }) require.NoError(t, err) expectedBody := `{"query":"Perf","workspaces":["/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r1","/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r2"]}` @@ -1472,7 +1507,7 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { } }) - t.Run("creates a request with timerange from query", func(t *testing.T) { + t.Run("creates a request with timerange from dashboard", func(t *testing.T) { ds := AzureLogAnalyticsDatasource{} from := time.Now() to := from.Add(3 * time.Hour) @@ -1485,10 +1520,11 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { To: to, }, AppInsightsQuery: false, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "TimeGenerated", }) require.NoError(t, err) - expectedBody := fmt.Sprintf(`{"query":"Perf","timespan":"%s/%s","workspaces":["/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r1","/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r2"]}`, from.Format(time.RFC3339), to.Format(time.RFC3339)) + expectedBody := fmt.Sprintf(`{"query":"Perf","query_datetimescope_column":"TimeGenerated","query_datetimescope_from":"%s","query_datetimescope_to":"%s","timespan":"%s/%s","workspaces":["/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r1","/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.OperationalInsights/workspaces/r2"]}`, from.Format(time.RFC3339), to.Format(time.RFC3339), from.Format(time.RFC3339), to.Format(time.RFC3339)) body, err := io.ReadAll(req.Body) require.NoError(t, err) if !cmp.Equal(string(body), expectedBody) { @@ -1508,10 +1544,11 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { To: to, }, AppInsightsQuery: true, - IntersectTime: true, + DashboardTime: true, + TimeColumn: "timestamp", }) require.NoError(t, err) - expectedBody := fmt.Sprintf(`{"applications":["/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1","/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r2"],"query":"","timespan":"%s/%s"}`, from.Format(time.RFC3339), to.Format(time.RFC3339)) + expectedBody := fmt.Sprintf(`{"applications":["/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1","/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r2"],"query":"","query_datetimescope_column":"timestamp","query_datetimescope_from":"%s","query_datetimescope_to":"%s","timespan":"%s/%s"}`, from.Format(time.RFC3339), to.Format(time.RFC3339), from.Format(time.RFC3339), to.Format(time.RFC3339)) body, err := io.ReadAll(req.Body) require.NoError(t, err) if !cmp.Equal(string(body), expectedBody) { diff --git a/public/app/plugins/datasource/azuremonitor/__mocks__/query.ts b/public/app/plugins/datasource/azuremonitor/__mocks__/query.ts index 7b194c48179..fab83f3fdcd 100644 --- a/public/app/plugins/datasource/azuremonitor/__mocks__/query.ts +++ b/public/app/plugins/datasource/azuremonitor/__mocks__/query.ts @@ -18,7 +18,8 @@ export default function createMockQuery(overrides?: Partial): resultFormat: ResultFormat.Table, workspace: 'e3fe4fde-ad5e-4d60-9974-e2f3562ffdf2', resources: ['test-resource'], - intersectTime: false, + dashboardTime: false, + timeColumn: 'TimeGenerated', ...overrides?.azureLogAnalytics, }, diff --git a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/__mocks__/schema.ts b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/__mocks__/schema.ts index 5a03accb62d..eb7a33695c1 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/__mocks__/schema.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/__mocks__/schema.ts @@ -1,3 +1,5 @@ +import { AzureLogAnalyticsMetadataTable, EngineSchema } from '../../types'; + export default class FakeSchemaData { static getLogAnalyticsFakeSchema() { return { @@ -317,4 +319,68 @@ export default class FakeSchemaData { ], }; } + + static getLogAnalyticsFakeEngineSchema(tableOverride?: AzureLogAnalyticsMetadataTable[]): EngineSchema { + const database = { + name: 'test', + tables: tableOverride ?? [ + { + id: 't/Alert', + name: 'Alert', + timespanColumn: 'TimeGenerated', + columns: [ + { name: 'TimeGenerated', type: 'datetime' }, + { name: 'AlertSeverity', type: 'string' }, + { name: 'SourceDisplayName', type: 'string' }, + { name: 'AlertName', type: 'string' }, + { name: 'AlertDescription', type: 'string' }, + { name: 'SourceSystem', type: 'string' }, + { name: 'QueryExecutionStartTime', type: 'datetime' }, + { name: 'QueryExecutionEndTime', type: 'datetime' }, + { name: 'Query', type: 'string' }, + { name: 'RemediationJobId', type: 'string' }, + { name: 'RemediationRunbookName', type: 'string' }, + { name: 'AlertRuleId', type: 'string' }, + { name: 'AlertRuleInstanceId', type: 'string' }, + { name: 'ThresholdOperator', type: 'string' }, + { name: 'ThresholdValue', type: 'int' }, + { name: 'LinkToSearchResults', type: 'string' }, + { name: 'ServiceDeskConnectionName', type: 'string' }, + { name: 'ServiceDeskId', type: 'string' }, + { name: 'ServiceDeskWorkItemLink', type: 'string' }, + { name: 'ServiceDeskWorkItemType', type: 'string' }, + { name: 'ResourceId', type: 'string' }, + { name: 'ResourceType', type: 'string' }, + { name: 'ResourceValue', type: 'string' }, + { name: 'RootObjectName', type: 'string' }, + { name: 'ObjectDisplayName', type: 'string' }, + { name: 'Computer', type: 'string' }, + { name: 'AlertPriority', type: 'string' }, + { name: 'SourceFullName', type: 'string' }, + { name: 'AlertId', type: 'string' }, + { name: 'RepeatCount', type: 'int' }, + { name: 'AlertState', type: 'string' }, + { name: 'ResolvedBy', type: 'string' }, + { name: 'LastModifiedBy', type: 'string' }, + { name: 'TimeRaised', type: 'datetime' }, + { name: 'TimeResolved', type: 'datetime' }, + { name: 'TimeLastModified', type: 'datetime' }, + ], + related: { solutions: [] }, + }, + ], + functions: [], + majorVersion: 0, + minorVersion: 0, + }; + return { + clusterType: 'Engine', + cluster: { + connectionString: 'test', + databases: [database], + }, + database: database, + globalScalarParameters: [], + }; + } } diff --git a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.test.ts index 4259936f878..13de4a8e91d 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -40,15 +40,15 @@ describe('AzureLogAnalyticsDatasource', () => { }); it('should return a schema to use with monaco-kusto', async () => { - const result = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace'); + const { database } = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace'); - expect(result.database.tables).toHaveLength(2); - expect(result.database.tables[0].name).toBe('Alert'); - expect(result.database.tables[0].timespanColumn).toBe('TimeGenerated'); - expect(result.database.tables[1].name).toBe('AzureActivity'); - expect(result.database.tables[0].columns).toHaveLength(69); + expect(database?.tables).toHaveLength(2); + expect(database?.tables[0].name).toBe('Alert'); + expect(database?.tables[0].timespanColumn).toBe('TimeGenerated'); + expect(database?.tables[1].name).toBe('AzureActivity'); + expect(database?.tables[0].columns).toHaveLength(69); - expect(result.database.functions[1].inputParameters).toEqual([ + expect(database?.functions[1].inputParameters).toEqual([ { name: 'RangeStart', type: 'datetime', @@ -77,7 +77,7 @@ describe('AzureLogAnalyticsDatasource', () => { it('should include macros as suggested functions', async () => { const result = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace'); - expect(result.database.functions.map((f: { name: string }) => f.name)).toEqual([ + expect(result.database?.functions.map((f: { name: string }) => f.name)).toEqual([ 'Func1', '_AzureBackup_GetVaults', '$__timeFilter', @@ -90,7 +90,7 @@ describe('AzureLogAnalyticsDatasource', () => { it('should include template variables as global parameters', async () => { const result = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace'); - expect(result.globalParameters.map((f: { name: string }) => f.name)).toEqual([`$${singleVariable.name}`]); + expect(result.globalScalarParameters?.map((f: { name: string }) => f.name)).toEqual([`$${singleVariable.name}`]); }); }); diff --git a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts index ed6092bcbe6..009e8580245 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts @@ -131,7 +131,8 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< resources, // Workspace was removed in Grafana 8, but remains for backwards compat workspace, - intersectTime: target.azureLogAnalytics.intersectTime, + dashboardTime: item.dashboardTime, + timeColumn: templateSrv.replace(item.timeColumn, scopedVars), }, }; } diff --git a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/utils.ts b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/utils.ts index 60f9a10008e..1e61ef898ca 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/utils.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/utils.ts @@ -1,5 +1,6 @@ import { VariableModel } from '@grafana/data'; +import { EngineSchema } from '../types'; import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata'; // matches (name):(type) = (defaultValue) @@ -48,7 +49,7 @@ export function transformMetadataToKustoSchema( sourceSchema: AzureLogAnalyticsMetadata, nameOrIdOrSomething: string, templateVariables: VariableModel[] -) { +): EngineSchema { const database = { name: nameOrIdOrSomething, tables: sourceSchema.tables, @@ -114,7 +115,7 @@ export function transformMetadataToKustoSchema( ); // Adding macros as global parameters - const globalParameters = templateVariables.map((v) => { + const globalScalarParameters = templateVariables.map((v) => { return { name: `$${v.name}`, type: 'dynamic', @@ -128,6 +129,6 @@ export function transformMetadataToKustoSchema( databases: [database], }, database: database, - globalParameters, + globalScalarParameters, }; } diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx index cb8d96001cf..1f0568572af 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.test.tsx @@ -185,7 +185,7 @@ describe('LogsQueryEditor', () => { ); }); - it('should update the intersectTime prop', async () => { + it('should update the dashboardTime prop', async () => { const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); const query = createMockQuery(); const onChange = jest.fn(); @@ -200,13 +200,13 @@ describe('LogsQueryEditor', () => { /> ); - const intersectionOption = await screen.findByLabelText('Intersection'); - await userEvent.click(intersectionOption); + const dashboardTimeOption = await screen.findByLabelText('Dashboard'); + await userEvent.click(dashboardTimeOption); expect(onChange).toBeCalledWith( expect.objectContaining({ azureLogAnalytics: expect.objectContaining({ - intersectTime: true, + dashboardTime: true, }), }) ); diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx index 601ce14da8e..45de3fa3f72 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -1,11 +1,11 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental'; -import { Alert, InlineField, RadioButtonGroup } from '@grafana/ui'; +import { Alert } from '@grafana/ui'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, ResultFormat } from '../../types'; +import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, ResultFormat, EngineSchema } from '../../types'; import FormatAsField from '../FormatAsField'; import ResourceField from '../ResourceField'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types'; @@ -13,7 +13,8 @@ import { parseResourceDetails } from '../ResourcePicker/utils'; import AdvancedResourcePicker from './AdvancedResourcePicker'; import QueryField from './QueryField'; -import { setFormatAs, setIntersectTime } from './setQueryValue'; +import { TimeManagement } from './TimeManagement'; +import { setFormatAs } from './setQueryValue'; import useMigrations from './useMigrations'; interface LogsQueryEditorProps { @@ -49,6 +50,15 @@ const LogsQueryEditor = ({ // Only resources with the same metricNamespace can be selected return rowResourceNS !== selectedRowSampleNs; }; + const [schema, setSchema] = useState(); + + useEffect(() => { + if (query.azureLogAnalytics?.resources && query.azureLogAnalytics.resources.length) { + datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.resources[0]).then((schema) => { + setSchema(schema); + }); + } + }, [query.azureLogAnalytics?.resources, datasource.azureLogAnalyticsDatasource]); return ( @@ -81,22 +91,14 @@ const LogsQueryEditor = ({ )} selectionNotice={() => 'You may only choose items of the same resource type.'} /> - - onChange(setIntersectTime(query, val))} - /> - + diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx index 2387cf9ed9c..2311c733422 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx @@ -1,14 +1,14 @@ +import { EngineSchema, Schema } from '@kusto/monaco-kusto'; import { Uri } from 'monaco-editor'; -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { CodeEditor, Monaco, MonacoEditor } from '@grafana/ui'; -import { Deferred } from 'app/core/utils/deferred'; import { AzureQueryEditorFieldProps } from '../../types'; import { setKustoQuery } from './setQueryValue'; -interface MonacoPromise { +interface MonacoEditorValues { editor: MonacoEditor; monaco: Monaco; } @@ -17,50 +17,39 @@ interface MonacoLanguages { kusto: { getKustoWorker: () => Promise< (url: Uri) => Promise<{ - setSchema: (schema: any, clusterUrl: string, name: string) => void; + setSchema: (schema: Schema) => void; }> >; }; } -const QueryField = ({ query, datasource, onQueryChange }: AzureQueryEditorFieldProps) => { - const monacoPromiseRef = useRef>(); - function getPromise() { - if (!monacoPromiseRef.current) { - monacoPromiseRef.current = new Deferred(); - } - - return monacoPromiseRef.current.promise; - } +const QueryField = ({ query, onQueryChange, schema }: AzureQueryEditorFieldProps) => { + const [monaco, setMonaco] = useState(); useEffect(() => { - if (!query.azureLogAnalytics?.resources || !query.azureLogAnalytics.resources.length) { + if (!schema || !monaco) { return; } - const promises = [ - datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.resources[0]), - getPromise(), - ] as const; + const setupEditor = async ({ monaco, editor }: MonacoEditorValues, schema: EngineSchema) => { + try { + const languages = monaco.languages as unknown as MonacoLanguages; + const model = editor.getModel(); + if (model) { + const kustoWorker = await languages.kusto.getKustoWorker(); + const kustoMode = await kustoWorker(model?.uri); + await kustoMode.setSchema(schema); + } + } catch (err) { + console.error(err); + } + }; - // the kusto schema call might fail, but it's okay for that to happen silently - Promise.all(promises).then(([schema, { monaco, editor }]) => { - const languages = monaco.languages as unknown as MonacoLanguages; - - languages.kusto - .getKustoWorker() - .then((kusto) => { - const model = editor.getModel(); - return model && kusto(model.uri); - }) - .then((worker) => { - worker?.setSchema(schema, 'https://help.kusto.windows.net', 'Samples'); - }); - }); - }, [datasource.azureLogAnalyticsDatasource, query.azureLogAnalytics?.resources]); + setupEditor(monaco, schema).catch((err) => console.error(err)); + }, [schema, monaco]); const handleEditorMount = useCallback((editor: MonacoEditor, monaco: Monaco) => { - monacoPromiseRef.current?.resolve?.({ editor, monaco }); + setMonaco({ monaco, editor }); }, []); const onChange = useCallback( diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.test.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.test.tsx new file mode 100644 index 00000000000..f2626df6c0f --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.test.tsx @@ -0,0 +1,172 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import createMockDatasource from '../../__mocks__/datasource'; +import createMockQuery from '../../__mocks__/query'; +import FakeSchemaData from '../../azure_log_analytics/__mocks__/schema'; + +import { TimeManagement } from './TimeManagement'; + +const variableOptionGroup = { + label: 'Template variables', + options: [], +}; + +describe('LogsQueryEditor.TimeManagement', () => { + it('should render the column picker if Dashboard is chosen', async () => { + const mockDatasource = createMockDatasource(); + const query = createMockQuery({ azureLogAnalytics: { timeColumn: undefined } }); + const onChange = jest.fn(); + + const { rerender } = render( + {}} + schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema()} + /> + ); + + const dashboardTimeOption = await screen.findByLabelText('Dashboard'); + await userEvent.click(dashboardTimeOption); + + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + dashboardTime: true, + }), + }) + ); + + rerender( + {}} + schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema()} + /> + ); + + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + timeColumn: 'TimeGenerated', + }), + }) + ); + }); + + it('should render the default value if no time columns exist', async () => { + const mockDatasource = createMockDatasource(); + const query = createMockQuery(); + const onChange = jest.fn(); + + render( + {}} + schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema([ + { + id: 't/Alert', + name: 'Alert', + timespanColumn: 'TimeGenerated', + columns: [], + related: { + solutions: [], + }, + }, + ])} + /> + ); + + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + timeColumn: 'TimeGenerated', + }), + }) + ); + }); + + it('should render the first time column if no default exists', async () => { + const mockDatasource = createMockDatasource(); + const query = createMockQuery(); + const onChange = jest.fn(); + + render( + {}} + schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema([ + { + id: 't/Alert', + name: 'Alert', + timespanColumn: '', + columns: [{ name: 'Timespan', type: 'datetime' }], + related: { + solutions: [], + }, + }, + ])} + /> + ); + + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureLogAnalytics: expect.objectContaining({ + timeColumn: 'Timespan', + }), + }) + ); + }); + + it('should render the query time column if it exists', async () => { + const mockDatasource = createMockDatasource(); + const query = createMockQuery(); + const onChange = jest.fn(); + + render( + {}} + schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema([ + { + id: 't/Alert', + name: 'Alert', + timespanColumn: '', + columns: [{ name: 'TestTimeColumn', type: 'datetime' }], + related: { + solutions: [], + }, + }, + ])} + /> + ); + + expect(onChange).not.toBeCalled(); + expect(screen.getByText('Alert > TestTimeColumn')).toBeInTheDocument(); + }); +}); diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.tsx new file mode 100644 index 00000000000..f4432719ace --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.tsx @@ -0,0 +1,137 @@ +import React, { useCallback, useEffect, useState } from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { InlineField, RadioButtonGroup, Select } from '@grafana/ui'; + +import { AzureQueryEditorFieldProps } from '../../types'; + +import { setDashboardTime, setTimeColumn } from './setQueryValue'; + +export function TimeManagement({ query, onQueryChange: onChange, schema }: AzureQueryEditorFieldProps) { + const [defaultTimeColumns, setDefaultTimeColumns] = useState(); + const [timeColumns, setTimeColumns] = useState(); + + const setDefaultColumn = useCallback((column: string) => onChange(setTimeColumn(query, column)), [query, onChange]); + + useEffect(() => { + if (schema && query.azureLogAnalytics?.dashboardTime) { + const timeColumnOptions: SelectableValue[] = []; + const timeColumnsSet: Set = new Set(); + const defaultColumnsMap: Map = new Map(); + const db = schema.database; + if (db) { + for (const table of db.tables) { + const cols = table.columns.reduce((prev, curr, i) => { + if (curr.type === 'datetime') { + if (!table.timespanColumn || table.timespanColumn !== curr.name) { + prev.push({ value: curr.name, label: `${table.name} > ${curr.name}` }); + timeColumnsSet.add(curr.name); + } + } + return prev; + }, []); + timeColumnOptions.push(...cols); + if (table.timespanColumn && !defaultColumnsMap.has(table.timespanColumn)) { + defaultColumnsMap.set(table.timespanColumn, { + value: table.timespanColumn, + label: table.timespanColumn, + }); + } + } + } + setTimeColumns(timeColumnOptions); + const defaultColumns = Array.from(defaultColumnsMap.values()); + setDefaultTimeColumns(defaultColumns); + + // Set default value + if ( + !query.azureLogAnalytics.timeColumn || + (query.azureLogAnalytics.timeColumn && + !timeColumnsSet.has(query.azureLogAnalytics.timeColumn) && + !defaultColumnsMap.has(query.azureLogAnalytics.timeColumn)) + ) { + if (defaultColumns && defaultColumns.length) { + setDefaultColumn(defaultColumns[0].value); + setDefaultColumn(defaultColumns[0].value); + return; + } else if (timeColumnOptions && timeColumnOptions.length) { + setDefaultColumn(timeColumnOptions[0].value); + return; + } else { + setDefaultColumn('TimeGenerated'); + return; + } + } + } + }, [schema, query.azureLogAnalytics?.dashboardTime, query.azureLogAnalytics?.timeColumn, setDefaultColumn]); + + const handleTimeColumnChange = useCallback( + (change: SelectableValue) => { + if (!change.value) { + return; + } + + const newQuery = setTimeColumn(query, change.value); + onChange(newQuery); + }, + [onChange, query] + ); + return ( + <> + + Specifies the time-range used to query. The Query option will only use time-ranges specified in + the query. Dashboard will only use the Grafana time-range. + + } + > + onChange(setDashboardTime(query, val))} + /> + + {query.azureLogAnalytics?.dashboardTime && ( + + Specifies the time column used for filtering. Defaults to the first tables timeSpan column, + the first datetime column found or TimeGenerated. + + } + > +