From 4e24b2977c7b0334637ec38e5448c9bb068c39d1 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 20 Nov 2025 08:38:49 +0100 Subject: [PATCH 001/378] Alerting: Fix expression queries when coming from a panel (#114095) * Add getDefaultExpressions tests * Fix expression queries when building form values from a panel * make query params empty and add some better types * update snapshots * fix tests * Revert devenv changes * update shapshot --------- Co-authored-by: Gilles De Mey --- .../PanelAlertTabContent.test.tsx.snap | 8 +- ...eEditorGrafanaRecordingRules.test.tsx.snap | 8 +- .../SimplifiedRuleEditor.test.tsx.snap | 40 +--- .../RuleEditorGrafanaRules.test.tsx.snap | 8 +- .../alerting/unified/utils/rule-form.test.ts | 103 +++++++++ .../alerting/unified/utils/rule-form.ts | 210 +++++++++++------- .../PanelDataAlertingTab.test.tsx.snap | 8 +- 7 files changed, 246 insertions(+), 139 deletions(-) diff --git a/public/app/features/alerting/unified/__snapshots__/PanelAlertTabContent.test.tsx.snap b/public/app/features/alerting/unified/__snapshots__/PanelAlertTabContent.test.tsx.snap index 4c1b0bb8b65..e36613807ec 100644 --- a/public/app/features/alerting/unified/__snapshots__/PanelAlertTabContent.test.tsx.snap +++ b/public/app/features/alerting/unified/__snapshots__/PanelAlertTabContent.test.tsx.snap @@ -52,9 +52,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -90,9 +88,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRecordingRules.test.tsx.snap b/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRecordingRules.test.tsx.snap index fdd49bc80d7..7aa2e489209 100644 --- a/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRecordingRules.test.tsx.snap +++ b/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRecordingRules.test.tsx.snap @@ -78,9 +78,7 @@ exports[`RuleEditor grafana recording rules can create new grafana recording rul "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -208,9 +206,7 @@ exports[`RuleEditor grafana recording rules can create new grafana recording rul "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap index 9ffe68ad3b4..1ff6c2a0502 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap @@ -79,9 +79,7 @@ exports[`Can create a new grafana managed alert using simplified routing can cre "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -117,9 +115,7 @@ exports[`Can create a new grafana managed alert using simplified routing can cre "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], @@ -249,9 +245,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -287,9 +281,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], @@ -422,9 +414,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -460,9 +450,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], @@ -598,9 +586,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -636,9 +622,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], @@ -771,9 +755,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -809,9 +791,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], diff --git a/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap index 298f5e0542a..6dd5b9af077 100644 --- a/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap +++ b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap @@ -81,9 +81,7 @@ exports[`RuleEditor grafana managed rules can create new grafana managed alert 1 "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -119,9 +117,7 @@ exports[`RuleEditor grafana managed rules can create new grafana managed alert 1 "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], diff --git a/public/app/features/alerting/unified/utils/rule-form.test.ts b/public/app/features/alerting/unified/utils/rule-form.test.ts index 7137066229e..2e9b7a5730b 100644 --- a/public/app/features/alerting/unified/utils/rule-form.test.ts +++ b/public/app/features/alerting/unified/utils/rule-form.test.ts @@ -1,4 +1,5 @@ import { PromQuery } from '@grafana/prometheus'; +import { ExpressionDatasourceUID, ExpressionQueryType } from 'app/features/expressions/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; import { AlertDataQuery, @@ -8,6 +9,7 @@ import { RulerAlertingRuleDTO, } from 'app/types/unified-alerting-dto'; +import { EvalFunction } from '../../state/alertDef'; import { mockDataSource, mockRuleWithLocation, mockRulerGrafanaRecordingRule } from '../mocks'; import { getDefaultFormValues } from '../rule-editor/formDefaults'; import { setupDataSources } from '../testSetup/datasources'; @@ -21,6 +23,7 @@ import { formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO, getContactPointsFromDTO, + getDefaultExpressions, getInstantFromDataQuery, getNotificationSettingsForDTO, rulerRuleToFormValues, @@ -417,3 +420,103 @@ describe('getInstantFromDataQuery', () => { expect(result).toBe(false); }); }); + +describe('getDefaultExpressions', () => { + it('should create a reduce expression as the first query', () => { + const result = getDefaultExpressions('B', 'C'); + const reduceQuery = result[0]; + const model = reduceQuery.model; + + expect(reduceQuery.refId).toBe('B'); + expect(reduceQuery.datasourceUid).toBe(ExpressionDatasourceUID); + expect(reduceQuery.queryType).toBe(''); + expect(model.type).toBe(ExpressionQueryType.reduce); + expect(model.datasource?.uid).toBe(ExpressionDatasourceUID); + expect(model.reducer).toBe('last'); + }); + + it('should create reduce expression with proper conditions structure', () => { + const result = getDefaultExpressions('B', 'C'); + const reduceQuery = result[0]; + const model = reduceQuery.model; + + expect(model.conditions).toHaveLength(1); + expect(model.expression).toBe('A'); + expect(model.conditions?.[0]).toEqual({ + type: 'query', + evaluator: { + params: [], + type: EvalFunction.IsAbove, + }, + operator: { + type: 'and', + }, + query: { + params: [], + }, + reducer: { + params: [], + type: 'last', + }, + }); + }); + + it('should create a threshold expression as the second query', () => { + const result = getDefaultExpressions('B', 'C'); + const thresholdQuery = result[1]; + const model = thresholdQuery.model; + + expect(thresholdQuery.refId).toBe('C'); + expect(thresholdQuery.datasourceUid).toBe(ExpressionDatasourceUID); + expect(thresholdQuery.queryType).toBe(''); + expect(model.type).toBe(ExpressionQueryType.threshold); + expect(model.datasource?.uid).toBe(ExpressionDatasourceUID); + }); + + it('should create threshold expression with proper conditions structure', () => { + const result = getDefaultExpressions('B', 'C'); + const thresholdQuery = result[1]; + const model = thresholdQuery.model; + + expect(model.conditions).toHaveLength(1); + expect(model.conditions?.[0]).toEqual({ + type: 'query', + evaluator: { + params: [0], + type: EvalFunction.IsAbove, + }, + operator: { + type: 'and', + }, + query: { + params: [], + }, + reducer: { + params: [], + type: 'last', + }, + }); + }); + + it('should reference the reduce expression in the threshold expression', () => { + const result = getDefaultExpressions('B', 'C'); + const thresholdQuery = result[1]; + const model = thresholdQuery.model; + + expect(model.expression).toBe('B'); + }); + + it('should properly use different refIds throughout the structure', () => { + const result = getDefaultExpressions('X', 'Y'); + const reduceModel = result[0].model; + const thresholdModel = result[1].model; + + expect(result[0].refId).toBe('X'); + expect(reduceModel.refId).toBe('X'); + expect(reduceModel.conditions?.[0].query.params).toEqual([]); + + expect(result[1].refId).toBe('Y'); + expect(thresholdModel.refId).toBe('Y'); + expect(thresholdModel.expression).toBe('X'); + }); +}); diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 899ed22575b..3c000d6cab7 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -536,85 +536,18 @@ export const getDefaultRecordingRulesQueries = ( }, ]; }; -const getDefaultExpressions = (...refIds: [string, string]): AlertQuery[] => { + +export const getDefaultExpressions = (...refIds: [string, string]) => { const refOne = refIds[0]; const refTwo = refIds[1]; - const reduceExpression: ExpressionQuery = { - refId: refIds[0], - type: ExpressionQueryType.reduce, - datasource: { - uid: ExpressionDatasourceUID, - type: ExpressionDatasourceRef.type, - }, - conditions: [ - { - type: 'query', - evaluator: { - params: [], - type: EvalFunction.IsAbove, - }, - operator: { - type: 'and', - }, - query: { - params: [refOne], - }, - reducer: { - params: [], - type: 'last', - }, - }, - ], - reducer: 'last', - expression: 'A', - }; + const reduceQuery = getDefaultReduceExpression({ inputRefId: 'A', reduceRefId: refOne }); + const thresholdQuery = getDefaultThresholdExpression({ inputRefId: refOne, thresholdRefId: refTwo }); - const thresholdExpression: ExpressionQuery = { - refId: refTwo, - type: ExpressionQueryType.threshold, - datasource: { - uid: ExpressionDatasourceUID, - type: ExpressionDatasourceRef.type, - }, - conditions: [ - { - type: 'query', - evaluator: { - params: [0], - type: EvalFunction.IsAbove, - }, - operator: { - type: 'and', - }, - query: { - params: [refTwo], - }, - reducer: { - params: [], - type: 'last', - }, - }, - ], - expression: refOne, - }; - - return [ - { - refId: refOne, - datasourceUid: ExpressionDatasourceUID, - queryType: '', - model: reduceExpression, - }, - { - refId: refTwo, - datasourceUid: ExpressionDatasourceUID, - queryType: '', - model: thresholdExpression, - }, - ]; + return [reduceQuery, thresholdQuery] as const; }; -const getDefaultExpressionsForRecording = (refOne: string): AlertQuery[] => { + +const getDefaultExpressionsForRecording = (refOne: string): Array> => { const reduceExpression: ExpressionQuery = { refId: refOne, type: ExpressionQueryType.reduce, @@ -633,7 +566,7 @@ const getDefaultExpressionsForRecording = (refOne: string): AlertQuery[] => { type: 'and', }, query: { - params: [refOne], + params: [], }, reducer: { params: [], @@ -655,6 +588,95 @@ const getDefaultExpressionsForRecording = (refOne: string): AlertQuery[] => { ]; }; +function getDefaultReduceExpression({ + inputRefId, + reduceRefId, +}: { + inputRefId: string; + reduceRefId: string; +}): AlertQuery { + const reduceExpression: ExpressionQuery = { + refId: reduceRefId, + type: ExpressionQueryType.reduce, + datasource: { + uid: ExpressionDatasourceUID, + type: ExpressionDatasourceRef.type, + }, + conditions: [ + { + type: 'query', + evaluator: { + params: [], + type: EvalFunction.IsAbove, + }, + operator: { + type: 'and', + }, + query: { + params: [], + }, + reducer: { + params: [], + type: 'last', + }, + }, + ], + reducer: 'last', + expression: inputRefId, + }; + + return { + refId: reduceRefId, + datasourceUid: ExpressionDatasourceUID, + queryType: '', + model: reduceExpression, + }; +} + +function getDefaultThresholdExpression({ + inputRefId, + thresholdRefId, +}: { + inputRefId: string; + thresholdRefId: string; +}): AlertQuery { + const thresholdExpression: ExpressionQuery = { + refId: thresholdRefId, + type: ExpressionQueryType.threshold, + datasource: { + uid: ExpressionDatasourceUID, + type: ExpressionDatasourceRef.type, + }, + conditions: [ + { + type: 'query', + evaluator: { + params: [0], + type: EvalFunction.IsAbove, + }, + operator: { + type: 'and', + }, + query: { + params: [], + }, + reducer: { + params: [], + type: 'last', + }, + }, + ], + expression: inputRefId, + }; + + return { + refId: thresholdRefId, + datasourceUid: ExpressionDatasourceUID, + queryType: '', + model: thresholdExpression, + }; +} + const dataQueriesToGrafanaQueries = async ( queries: DataQuery[], relativeTimeRange: RelativeTimeRange, @@ -736,14 +758,23 @@ export const panelToRuleFormValues = async ( return undefined; } + const lastQuery = queries.at(-1); + if (!lastQuery) { + return undefined; + } + if (!queries.find((query) => query.datasourceUid === ExpressionDatasourceUID)) { - const [reduceExpression, _thresholdExpression] = getDefaultExpressions(getNextRefId(queries), '-'); + const reduceExpression = getDefaultReduceExpression({ + inputRefId: lastQuery.refId, + reduceRefId: getNextRefId(queries), + }); queries.push(reduceExpression); - const [_reduceExpression, thresholdExpression] = getDefaultExpressions( - reduceExpression.refId, - getNextRefId(queries) - ); + const thresholdExpression = getDefaultThresholdExpression({ + inputRefId: reduceExpression.refId, + thresholdRefId: getNextRefId(queries), + }); + queries.push(thresholdExpression); } @@ -808,14 +839,23 @@ export const scenesPanelToRuleFormValues = async (vizPanel: VizPanel): Promise

query.datasourceUid === ExpressionDatasourceUID)) { - const [reduceExpression, _thresholdExpression] = getDefaultExpressions(getNextRefId(grafanaQueries), '-'); + const reduceExpression = getDefaultReduceExpression({ + inputRefId: lastQuery.refId, + reduceRefId: getNextRefId(grafanaQueries), + }); grafanaQueries.push(reduceExpression); - const [_reduceExpression, thresholdExpression] = getDefaultExpressions( - reduceExpression.refId, - getNextRefId(grafanaQueries) - ); + const thresholdExpression = getDefaultThresholdExpression({ + inputRefId: reduceExpression.refId, + thresholdRefId: getNextRefId(grafanaQueries), + }); + grafanaQueries.push(thresholdExpression); } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/__snapshots__/PanelDataAlertingTab.test.tsx.snap b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/__snapshots__/PanelDataAlertingTab.test.tsx.snap index 8710a533fdd..858dc3812c1 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/__snapshots__/PanelDataAlertingTab.test.tsx.snap +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/__snapshots__/PanelDataAlertingTab.test.tsx.snap @@ -47,9 +47,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -85,9 +83,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], From 1d763df932100fc32698d176ab86cbd953548204 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Thu, 20 Nov 2025 09:50:07 +0100 Subject: [PATCH 002/378] Docs: Document font_min_text_size setting in reporting section (#114155) * Docs: Document font_min_text_size setting in reporting section * better wording --- .../configure-grafana/enterprise-configuration/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md b/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md index 601a258764f..7f1cf5e3cb5 100644 --- a/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md +++ b/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md @@ -190,6 +190,10 @@ Name of the TrueType font file with bold style. Name of the TrueType font file with italic style. Default is `DejaVuSansCondensed-Oblique.ttf`. +### font_min_text_size + +The minimum pixel size that Grafana uses when rendering fonts. Default is `4`. + ### max_retries_per_panel Maximum number of times the following reporting rendering requests are retried before returning an error: generating PDFs, generating embedded dashboard images for report emails, and generating attached CSV files. To disable the retry feature, enter `0`. This is available in public preview and requires the `reportingRetries` feature toggle. Default is `3`. From cc6e0370930c8c5b4183f50b70713953e4c20033 Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Thu, 20 Nov 2025 10:00:27 +0100 Subject: [PATCH 003/378] Alerting: Add message to alert_rule_version table (Part 1). (#114194) This adds a `message` column to the `alert_rule_version` table. This follows the pattern established for dashboards as closely as possible. A new type is introduced internally for passing the new `message` field around in a type-safe manner, but doing the same for the API types becomes very messy. In that case, a new field is added with omitempty. Note this PR is only: - The column addition - The "read" path; API for listing versions Subsequent PRs will add code to actually set the message when updating rules. --- pkg/services/ngalert/api/api_ruler.go | 14 +++++- pkg/services/ngalert/api/api_ruler_test.go | 45 ++++++++++++++++--- pkg/services/ngalert/api/persist.go | 2 +- .../api/tooling/definitions/cortex-ruler.go | 3 ++ pkg/services/ngalert/models/alert_rule.go | 7 +++ pkg/services/ngalert/store/alert_rule.go | 9 ++-- pkg/services/ngalert/store/alert_rule_test.go | 4 +- pkg/services/ngalert/store/compat.go | 13 ++++++ pkg/services/ngalert/store/models.go | 3 +- pkg/services/ngalert/tests/fakes/rules.go | 23 ++++++++-- .../sqlstore/migrations/ualert/tables.go | 3 ++ 11 files changed, 105 insertions(+), 21 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 8e368ac1e8e..268ed7b8c5d 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -385,14 +385,24 @@ func (srv RulerSrv) RouteGetRuleVersionsByUID(c *contextmodel.ReqContext, ruleUI } sort.Slice(rules, func(i, j int) bool { return rules[i].ID > rules[j].ID }) result := make(apimodels.GettableRuleVersions, 0, len(rules)) - userUIDmapping := srv.getUserUIDmapping(ctx, rules) + userUIDmapping := srv.getUserUIDmapping(ctx, alertRuleVersionsToAlertRules(rules)) for _, rule := range rules { // do not provide provenance status because we do not have historical changes for it - result = append(result, toGettableExtendedRuleNode(*rule, map[string]ngmodels.Provenance{}, userUIDmapping)) + ruleNode := toGettableExtendedRuleNode(rule.AlertRule, map[string]ngmodels.Provenance{}, userUIDmapping) + ruleNode.GrafanaManagedAlert.Message = rule.Message + result = append(result, ruleNode) } return response.JSON(http.StatusOK, result) } +func alertRuleVersionsToAlertRules(vs []*ngmodels.AlertRuleVersion) []*ngmodels.AlertRule { + result := make([]*ngmodels.AlertRule, len(vs)) + for i := range vs { + result[i] = &vs[i].AlertRule + } + return result +} + func (srv RulerSrv) RoutePostNameRulesConfig(c *contextmodel.ReqContext, ruleGroupConfig apimodels.PostableRuleGroupConfig, namespaceUID string) response.Response { var deletePermanently bool if c.QueryBool("deletePermanently") { diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index cb6a93b27b5..c6ddeee2a6c 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -560,11 +560,17 @@ func TestRouteGetRuleVersionsByUID(t *testing.T) { ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], f) rule := gen.GenerateRef() - history := gen.With(gen.WithUID(rule.UID)).GenerateManyRef(3) + historyRules := gen.With(gen.WithUID(rule.UID)).GenerateManyRef(3) + history := make([]*models.AlertRuleVersion, len(historyRules)) // simulate order of the history rule.ID = 100 - for i, alertRule := range history { + for i, alertRule := range historyRules { alertRule.ID = rule.ID - int64(i) - 1 + + history[i] = &models.AlertRuleVersion{ + AlertRule: *alertRule, + Message: fmt.Sprintf("revision %d", i), + } } ruleStore.PutRule(context.Background(), rule) @@ -584,11 +590,22 @@ func TestRouteGetRuleVersionsByUID(t *testing.T) { require.Len(t, result, len(history)+1) // history + current version t.Run("should be in correct order", func(t *testing.T) { - expectedHistory := append([]*models.AlertRule{rule}, history...) + expectedHistory := append([]*models.AlertRuleVersion{{AlertRule: *rule}}, history...) for i, rul := range expectedHistory { assert.Equal(t, rul.UID, result[i].GrafanaManagedAlert.UID) } }) + + t.Run("should have correct messages", func(t *testing.T) { + expectedMessages := make([]string, 0, len(history)+1) + expectedMessages = append(expectedMessages, "") + for i := range history { + expectedMessages = append(expectedMessages, history[i].Message) + } + for i := range expectedMessages { + assert.Equal(t, expectedMessages[i], result[i].GrafanaManagedAlert.Message) + } + }) }) t.Run("NotFound when rule does not exist", func(t *testing.T) { @@ -599,10 +616,17 @@ func TestRouteGetRuleVersionsByUID(t *testing.T) { UID: "test", } guid := uuid.NewString() - history := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey)).GenerateManyRef(3) + historyRules := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey)).GenerateManyRef(3) + history := make([]*models.AlertRuleVersion, len(historyRules)) + for i, alertRule := range historyRules { + history[i] = &models.AlertRuleVersion{ + AlertRule: *alertRule, + Message: fmt.Sprintf("revision %d", i), + } + } ruleStore.History[guid] = append(ruleStore.History[guid], history...) // even if history is full of records - perms := createPermissionsForRules(history, orgID) + perms := createPermissionsForRules(historyRules, orgID) req := createRequestContextWithPerms(orgID, perms, nil) response := createService(ruleStore, nil).RouteGetRuleVersionsByUID(req, ruleKey.UID) @@ -643,10 +667,17 @@ func TestRouteGetRuleVersionsByUID(t *testing.T) { guid := uuid.NewString() rule := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey), gen.WithNamespaceUID(anotherFolder.UID)).GenerateRef() ruleStore.PutRule(context.Background(), rule) - history := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey)).GenerateManyRef(3) + historyRules := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey)).GenerateManyRef(3) + history := make([]*models.AlertRuleVersion, len(historyRules)) + for i, alertRule := range historyRules { + history[i] = &models.AlertRuleVersion{ + AlertRule: *alertRule, + Message: fmt.Sprintf("revision %d", i), + } + } ruleStore.History[guid] = history - perms := createPermissionsForRules(history, orgID) // grant permissions to all records in history but not the rule itself + perms := createPermissionsForRules(historyRules, orgID) // grant permissions to all records in history but not the rule itself req := createRequestContextWithPerms(orgID, perms, nil) response := createService(ruleStore, nil).RouteGetRuleVersionsByUID(req, ruleKey.UID) diff --git a/pkg/services/ngalert/api/persist.go b/pkg/services/ngalert/api/persist.go index 52c4ca59a14..9e7df81f48e 100644 --- a/pkg/services/ngalert/api/persist.go +++ b/pkg/services/ngalert/api/persist.go @@ -35,6 +35,6 @@ type RuleStore interface { // IncreaseVersionForAllRulesInNamespaces Increases version for all rules that have specified namespace uids IncreaseVersionForAllRulesInNamespaces(ctx context.Context, orgID int64, namespaceUIDs []string) ([]ngmodels.AlertRuleKeyWithVersion, error) - GetAlertRuleVersions(ctx context.Context, orgID int64, guid string) ([]*ngmodels.AlertRule, error) + GetAlertRuleVersions(ctx context.Context, orgID int64, guid string) ([]*ngmodels.AlertRuleVersion, error) accesscontrol.RuleUIDToNamespaceStore } diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index 21e6b438ac2..15cdb16d185 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -617,6 +617,9 @@ type GettableGrafanaRule struct { Metadata *AlertRuleMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` GUID string `json:"guid" yaml:"guid"` MissingSeriesEvalsToResolve *int64 `json:"missing_series_evals_to_resolve,omitempty" yaml:"missing_series_evals_to_resolve,omitempty"` + + // Field is only populated when listing alert rule versions. + Message string `yaml:"message,omitempty" json:"message,omitempty"` } // UserInfo represents user-related information, including a unique identifier and a name. diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 9f899bc2af7..a4e1e8fcc04 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -369,6 +369,13 @@ type AlertRule struct { MissingSeriesEvalsToResolve *int64 } +type AlertRuleVersion struct { + AlertRule + + // Message is only stored in the alert_rule_version table. + Message string +} + type AlertRuleMetadata struct { EditorSettings EditorSettings `json:"editor_settings"` PrometheusStyleRule *PrometheusStyleRule `json:"prometheus_style_rule,omitempty"` diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 9044bb369c5..b05ba01a562 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -192,8 +192,8 @@ func (st DBstore) GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAler return result, err } -func (st DBstore) GetAlertRuleVersions(ctx context.Context, orgID int64, guid string) ([]*ngmodels.AlertRule, error) { - alertRules := make([]*ngmodels.AlertRule, 0) +func (st DBstore) GetAlertRuleVersions(ctx context.Context, orgID int64, guid string) ([]*ngmodels.AlertRuleVersion, error) { + alertRules := make([]*ngmodels.AlertRuleVersion, 0) err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { rows, err := sess.Table(new(alertRuleVersion)).Where("rule_org_id = ? AND rule_guid = ?", orgID, guid).Asc("id").Rows(new(alertRuleVersion)) if err != nil { @@ -213,7 +213,7 @@ func (st DBstore) GetAlertRuleVersions(ctx context.Context, orgID int64, guid st if previousVersion != nil && previousVersion.EqualSpec(*rule) { continue } - converted, err := alertRuleToModelsAlertRule(alertRuleVersionToAlertRule(*rule), st.Logger) + converted, err := alertRuleVersionToModelsAlertRuleVersion(*rule, st.Logger) if err != nil { st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "GetAlertRuleVersions", "error", err, "version_id", rule.ID) continue @@ -226,7 +226,7 @@ func (st DBstore) GetAlertRuleVersions(ctx context.Context, orgID int64, guid st if err != nil { return nil, err } - slices.SortFunc(alertRules, func(a, b *ngmodels.AlertRule) int { + slices.SortFunc(alertRules, func(a, b *ngmodels.AlertRuleVersion) int { if a.ID > b.ID { return -1 } @@ -257,6 +257,7 @@ func (st DBstore) ListDeletedRules(ctx context.Context, orgID int64) ([]*ngmodel st.Logger.Error("Invalid rule version found in DB store, ignoring it", "func", "GetAlertRuleVersions", "error", err) continue } + // Note: Message is not returned as a message cannot be set when deleting rules. converted, err := alertRuleToModelsAlertRule(alertRuleVersionToAlertRule(*rule), st.Logger) if err != nil { st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "GetAlertRuleVersions", "error", err, "version_id", rule.ID) diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 0c49528f8d6..9a491120c8c 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -1682,7 +1682,7 @@ func TestIntegrationGetRuleVersions(t *testing.T) { require.NoError(t, err) assert.Len(t, versions, 2) assert.IsDecreasing(t, versions[0].ID, versions[1].ID) - diff := versions[1].Diff(versions[0], AlertRuleFieldsToIgnoreInDiff[:]...) + diff := versions[1].Diff(&versions[0].AlertRule, AlertRuleFieldsToIgnoreInDiff[:]...) assert.ElementsMatch(t, []string{"Title", "RuleGroupIndex"}, diff.Paths()) }) @@ -1712,7 +1712,7 @@ func TestIntegrationGetRuleVersions(t *testing.T) { versions, err := store.GetAlertRuleVersions(context.Background(), ruleV3.OrgID, ruleV3.GUID) require.NoError(t, err) assert.Len(t, versions, 3) - diff := versions[0].Diff(versions[1], AlertRuleFieldsToIgnoreInDiff[:]...) + diff := versions[0].Diff(&versions[1].AlertRule, AlertRuleFieldsToIgnoreInDiff[:]...) assert.ElementsMatch(t, []string{"RuleGroup", "NamespaceUID"}, diff.Paths()) }) } diff --git a/pkg/services/ngalert/store/compat.go b/pkg/services/ngalert/store/compat.go index 709450d5ed8..4f4194facc1 100644 --- a/pkg/services/ngalert/store/compat.go +++ b/pkg/services/ngalert/store/compat.go @@ -210,6 +210,7 @@ func alertRuleToAlertRuleVersion(rule alertRule) alertRuleVersion { Version: rule.Version, Created: rule.Updated, // assuming the Updated time as the creation time CreatedBy: rule.UpdatedBy, + Message: "", // Message is set by caller when creating versions Title: rule.Title, Condition: rule.Condition, Data: rule.Data, @@ -261,3 +262,15 @@ func alertRuleVersionToAlertRule(version alertRuleVersion) alertRule { MissingSeriesEvalsToResolve: version.MissingSeriesEvalsToResolve, } } + +func alertRuleVersionToModelsAlertRuleVersion(version alertRuleVersion, l log.Logger) (models.AlertRuleVersion, error) { + result, err := alertRuleToModelsAlertRule(alertRuleVersionToAlertRule(version), l) + if err != nil { + return models.AlertRuleVersion{}, err + } + + return models.AlertRuleVersion{ + AlertRule: result, + Message: version.Message, + }, nil +} diff --git a/pkg/services/ngalert/store/models.go b/pkg/services/ngalert/store/models.go index 254ec8cb048..24167a225dd 100644 --- a/pkg/services/ngalert/store/models.go +++ b/pkg/services/ngalert/store/models.go @@ -69,10 +69,11 @@ type alertRuleVersion struct { NotificationSettings string `xorm:"notification_settings"` Metadata string `xorm:"metadata"` MissingSeriesEvalsToResolve *int64 `xorm:"missing_series_evals_to_resolve"` + Message string } // EqualSpec compares two alertRuleVersion objects for equality based on their specifications and returns true if they match. -// The comparison is very basic and can produce false-negative. Fields excluded: ID, ParentVersion, RestoredFrom, Version, Created, RuleGroupIndex and CreatedBy +// The comparison is very basic and can produce false-negative. Fields excluded: ID, ParentVersion, RestoredFrom, Version, Created, RuleGroupIndex, CreatedBy and Message func (a alertRuleVersion) EqualSpec(b alertRuleVersion) bool { return a.RuleOrgID == b.RuleOrgID && a.RuleGUID == b.RuleGUID && diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index a0dd60e32e5..09abeaffba3 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -23,7 +23,7 @@ type RuleStore struct { mtx sync.Mutex // OrgID -> RuleGroup -> Namespace -> Rules Rules map[int64][]*models.AlertRule - History map[string][]*models.AlertRule + History map[string][]*models.AlertRuleVersion Deleted map[int64][]*models.AlertRule Hook func(cmd any) error // use Hook if you need to intercept some query and return an error RecordedOps []any @@ -43,7 +43,7 @@ func NewRuleStore(t *testing.T) *RuleStore { return nil }, Folders: map[int64][]*folder.Folder{}, - History: map[string][]*models.AlertRule{}, + History: map[string][]*models.AlertRuleVersion{}, } } @@ -55,7 +55,10 @@ mainloop: for _, r := range rules { rgs := f.Rules[r.OrgID] cp := models.CopyRule(r) - f.History[r.GUID] = append(f.History[r.GUID], cp) + f.History[r.GUID] = append(f.History[r.GUID], &models.AlertRuleVersion{ + AlertRule: *cp, + Message: "", + }) for idx, rulePtr := range rgs { if rulePtr.UID == r.UID { rgs[idx] = r @@ -87,6 +90,18 @@ mainloop: } } +// AppendHistory appends to rules to the version history with the given change message. +func (f *RuleStore) AppendHistory(guid string, rules []*models.AlertRule, message string) { + versions := make([]*models.AlertRuleVersion, len(rules)) + for i := range rules { + versions[i] = &models.AlertRuleVersion{ + AlertRule: *rules[i], + Message: message, + } + } + f.History[guid] = append(f.History[guid], versions...) +} + // GetRecordedCommands filters recorded commands using predicate function. Returns the subset of the recorded commands that meet the predicate func (f *RuleStore) GetRecordedCommands(predicate func(cmd any) (any, bool)) []any { f.mtx.Lock() @@ -563,7 +578,7 @@ func (f *RuleStore) GetNamespacesByRuleUID(ctx context.Context, orgID int64, uid return namespacesMap, nil } -func (f *RuleStore) GetAlertRuleVersions(_ context.Context, orgID int64, guid string) ([]*models.AlertRule, error) { +func (f *RuleStore) GetAlertRuleVersions(_ context.Context, orgID int64, guid string) ([]*models.AlertRuleVersion, error) { f.mtx.Lock() defer f.mtx.Unlock() diff --git a/pkg/services/sqlstore/migrations/ualert/tables.go b/pkg/services/sqlstore/migrations/ualert/tables.go index d6d98ebfec9..706b8dcf026 100644 --- a/pkg/services/sqlstore/migrations/ualert/tables.go +++ b/pkg/services/sqlstore/migrations/ualert/tables.go @@ -24,6 +24,9 @@ func AddTablesMigrations(mg *migrator.Migrator) { mg.AddMigration("add last_applied column to alert_configuration_history", migrator.NewAddColumnMigration(migrator.Table{Name: "alert_configuration_history"}, &migrator.Column{ Name: "last_applied", Type: migrator.DB_Int, Nullable: false, Default: "0", })) + mg.AddMigration("add message column to alert_rule_version", migrator.NewAddColumnMigration(migrator.Table{Name: "alert_rule_version"}, &migrator.Column{ + Name: "message", Type: migrator.DB_Text, Nullable: true, + })) // End of migration log, add new migrations above this line. } From fbe29596f10102dfa4137384823116c46271ffa1 Mon Sep 17 00:00:00 2001 From: petergreen86 <37148775+petergreen86@users.noreply.github.com> Date: Thu, 20 Nov 2025 09:20:02 +0000 Subject: [PATCH 004/378] Docs: Team Sync correction (#114043) --- .../configure-access/configure-authentication/github/index.md | 2 +- .../configure-access/configure-authentication/gitlab/index.md | 2 +- .../configure-access/configure-authentication/google/index.md | 2 +- .../configure-access/configure-authentication/keycloak/index.md | 2 +- .../configure-access/configure-authentication/okta/index.md | 2 +- .../setup-grafana/configure-access/configure-team-sync.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sources/setup-grafana/configure-access/configure-authentication/github/index.md b/docs/sources/setup-grafana/configure-access/configure-authentication/github/index.md index 7675d739951..5924e033f90 100644 --- a/docs/sources/setup-grafana/configure-access/configure-authentication/github/index.md +++ b/docs/sources/setup-grafana/configure-access/configure-authentication/github/index.md @@ -213,7 +213,7 @@ role_attribute_path = [login=='octocat'][0] && 'GrafanaAdmin' || 'Viewer' ## Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and to customers on select Grafana Cloud plans. For pricing information, visit [pricing](https://grafana.com/pricing/) or contact our sales team. +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/products/cloud/) {{< /admonition >}} By using Team Sync, you can map teams from your GitHub organization to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-access/configure-authentication/gitlab/index.md b/docs/sources/setup-grafana/configure-access/configure-authentication/gitlab/index.md index 913df8f4792..d3769a72ad8 100644 --- a/docs/sources/setup-grafana/configure-access/configure-authentication/gitlab/index.md +++ b/docs/sources/setup-grafana/configure-access/configure-authentication/gitlab/index.md @@ -238,7 +238,7 @@ use_refresh_token = true ## Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and to customers on select Grafana Cloud plans. For pricing information, visit [pricing](https://grafana.com/pricing/) or contact our sales team. +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/products/cloud/) {{< /admonition >}} By using Team Sync, you can map GitLab groups to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-access/configure-authentication/google/index.md b/docs/sources/setup-grafana/configure-access/configure-authentication/google/index.md index df0cc342d38..d122bb7e0f7 100644 --- a/docs/sources/setup-grafana/configure-access/configure-authentication/google/index.md +++ b/docs/sources/setup-grafana/configure-access/configure-authentication/google/index.md @@ -162,7 +162,7 @@ auto_login = true ### Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and to customers on select Grafana Cloud plans. For pricing information, visit [pricing](https://grafana.com/pricing/) or contact our sales team. +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/products/cloud/). {{< /admonition >}} With team sync, you can easily add users to teams by utilizing their Google groups. To set up team sync for Google OAuth, refer to the following example. diff --git a/docs/sources/setup-grafana/configure-access/configure-authentication/keycloak/index.md b/docs/sources/setup-grafana/configure-access/configure-authentication/keycloak/index.md index 6a9f6488994..6ce0f7a47b2 100644 --- a/docs/sources/setup-grafana/configure-access/configure-authentication/keycloak/index.md +++ b/docs/sources/setup-grafana/configure-access/configure-authentication/keycloak/index.md @@ -111,7 +111,7 @@ viewer ## Team sync {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and to customers on select Grafana Cloud plans. For pricing information, visit [pricing](https://grafana.com/pricing/) or contact our sales team. +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/products/cloud/). {{< /admonition >}} [Team Sync](https://grafana.com/docs/grafana//setup-grafana/configure-access/configure-team-sync/) is a feature that allows you to map groups from your identity provider to Grafana teams. This is useful if you want to give your users access to specific dashboards or folders based on their group membership. diff --git a/docs/sources/setup-grafana/configure-access/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-access/configure-authentication/okta/index.md index 6cb2d7d13c8..7bce3cb59da 100644 --- a/docs/sources/setup-grafana/configure-access/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-access/configure-authentication/okta/index.md @@ -238,7 +238,7 @@ org_mapping = ["Group 1:org_foo:Viewer", "Group 2:org_bar:Editor", "*:3:Editor"] ### Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and to customers on select Grafana Cloud plans. For pricing information, visit [pricing](https://grafana.com/pricing/) or contact our sales team. +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/products/cloud/). {{< /admonition >}} By using Team Sync, you can link your Okta groups to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-access/configure-team-sync.md b/docs/sources/setup-grafana/configure-access/configure-team-sync.md index 1741fd33edc..a4fe4b28c48 100644 --- a/docs/sources/setup-grafana/configure-access/configure-team-sync.md +++ b/docs/sources/setup-grafana/configure-access/configure-team-sync.md @@ -18,7 +18,7 @@ weight: 600 Team sync lets you set up synchronization between your auth providers teams and teams in Grafana. This enables LDAP, OAuth, or SAML users who are members of certain teams or groups to automatically be added or removed as members of certain teams in Grafana. {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} Grafana keeps track of all synchronized users in teams, and you can see which users have been synchronized in the team members list, see `LDAP` label in screenshot. From bf509de89f71a0141ae1c13fc85a805245761c3b Mon Sep 17 00:00:00 2001 From: Anna Urbiztondo Date: Thu, 20 Nov 2025 10:24:47 +0100 Subject: [PATCH 005/378] Docs: IaC edits (#114086) * Removed Grizzly from table * Fixes * Review * Reviews * Edits * More edits * Prettier * Remove agent * Review, removing Agent docs * Prettier * Fixes * Prettier * Edit * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Update docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> * Feedback * Prettier * Variable style edits --------- Co-authored-by: Clayton Cornell <131809008+clayton-cornell@users.noreply.github.com> --- .../as-code/infrastructure-as-code/_index.md | 43 +- .../infrastructure-as-code/ansible/_index.md | 42 +- .../ansible/ansible-cloud-stack/index.md | 354 +++++++------- .../ansible-grafana-agent-linux/index.md | 156 ------- .../ansible/ansible-multiple-agents/index.md | 207 --------- .../manage-dashboards-argocd.md | 435 +++++++++--------- ...operator-dashboards-folders-datasources.md | 136 +++--- .../observability-as-code/schema-v2/_index.md | 2 +- 8 files changed, 531 insertions(+), 844 deletions(-) delete mode 100644 docs/sources/as-code/infrastructure-as-code/ansible/ansible-grafana-agent-linux/index.md delete mode 100644 docs/sources/as-code/infrastructure-as-code/ansible/ansible-multiple-agents/index.md diff --git a/docs/sources/as-code/infrastructure-as-code/_index.md b/docs/sources/as-code/infrastructure-as-code/_index.md index 1579602467b..c976df81106 100644 --- a/docs/sources/as-code/infrastructure-as-code/_index.md +++ b/docs/sources/as-code/infrastructure-as-code/_index.md @@ -4,17 +4,23 @@ keywords: - Quickstart - Grafana Cloud menuTitle: Infrastructure as code -title: Provision Grafana Cloud with infrastructure as code +title: Provision Grafana Cloud with Infrastructure as code weight: 800 +labels: + products: + - cloud canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/ --- -# Provision Grafana Cloud with infrastructure as code +# Provision Grafana Cloud with Infrastructure as code -With Grafana Cloud, you can create dashboards via configuration files in source code. This enables you to review code, reuse it, and create better workflows. +With Grafana Cloud, you can use as-code tools to create and manage resources via code, and incorporate them efficiently into your own use cases. This enables you to review code, reuse it, and create better workflows. -Via code, you can _declaratively_ manage _what_ Grafana resources to use. -The as-code tools and tutorials that follow show you what do to, to declaratively manage Grafana resources, and incorporate them efficiently into your own use cases. +{{< admonition type="note" >}} + +Most of the tools defined here can be used with one another. + +{{< /admonition >}} ## Grafana Terraform provider @@ -67,7 +73,7 @@ To get started, see the [quickstart guides for the Grafana Ansible Collection](/ ### Who is this recommended for? -Like Terraform, the Grafana Ansible collection is best suited for people already using Ansible for non-Grafana use cases. The collection only works for Grafana Cloud right now, so it makes the most sense for Grafana Cloud customers who want to manage resources declaratively using Ansible. +Like Terraform, the Grafana Ansible collection is best suited for people already using Ansible for non-Grafana use cases. The collection only works for Grafana Cloud right now, so it makes the most sense for Grafana Cloud customers who want to manage resources using Ansible. ### Known limitations @@ -75,7 +81,7 @@ The Grafana Ansible collection only works for Grafana Cloud and only supports ei ## Grafana Operator -The Grafana Operator is a Kubernetes operator that can provision, manage, and operate Grafana instances and their associated resources within Kubernetes through Custom Resources. This Kubernetes-native tool eases the administration of Grafana, offering a declarative approach to managing dashboards, data sources, and folders. It also automatically syncs the Kubernetes Custom resources and the actual resources in the Grafana Instance. It supports leveraging Grafonnet for generating Grafana dashboard definitions for seamless dashboard configuration as code. +The Grafana Operator is a Kubernetes operator that can provision, manage, and operate Grafana instances and their associated resources within Kubernetes through Custom Resources. This Kubernetes-native tool eases the administration of Grafana, including managing dashboards, data sources, and folders. It also automatically syncs the Kubernetes Custom resources and the actual resources in the Grafana Instance. It supports leveraging Grafonnet for generating Grafana dashboard definitions for seamless dashboard configuration as code. To get started, see the [quickstart guides for the Grafana Operator](/docs/grafana-cloud/as-code/infrastructure-as-code/grafana-operator/) or check out the [Grafana Operator's documentation](https://grafana.github.io/grafana-operator/). @@ -120,7 +126,7 @@ To get started with the Grafana Crossplane provider, install Crossplane in the K kubectl crossplane install provider grafana/crossplane-provider-grafana:v0.1.0 ``` -During installation of the provider, CRDs for all the resources supported by the Terraform provider are added to the cluster so users can begin defining their Grafana resources as Kubernetes custom resources. The Crossplane provider ensures that whatever is defined in the custom resource definitions is what is visible in Grafana UI. If any changes are made directly in the UI, the changes will be discarded when the provider resyncs. This helps ensure that whatever is defined declaratively in the cluster will be the source of truth for Grafana resources. +During installation of the provider, CRDs for all the resources supported by the Terraform provider are added to the cluster so users can begin defining their Grafana resources as Kubernetes custom resources. The Crossplane provider ensures that whatever is defined in the custom resource definitions is what is visible in Grafana UI. If any changes are made directly in the UI, the changes will be discarded when the provider resyncs. This helps ensure that whatever is defined via code in the cluster will be the source of truth for Grafana resources. To get started, refer to the examples folder in the Grafana Crossplane repository. @@ -152,16 +158,15 @@ To use the Crossplane provider, you must have the Crossplane CLI and Crossplane ## Grafana as code comparison -Most of the tools defined here can be used with one another. The following chart compares the properties and tools mentioned above. -| Property/Tool | Grafana Terraform Provider | Grafana Ansible Collection | Grafana Operator | Grizzly | Grafana Crossplane Provider | -| -------------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| Grafana resources supported | All major Grafana resources | Grafana Cloud stack, plugins, API keys, dashboards, data sources, and folders | Dashboards, Datasources, Folders | Synthetic Monitoring checks, dashboards, data sources, folders, and Prometheus rules | All major Grafana resources | -| Tool format | HCL/JSON | YAML | YAML | Jsonnet/YAML/JSON | YAML/JSON | -| Follows Kubernetes-style manifests | | | ✓ | ✓ | ✓ | -| Easy dashboard building process | | | ✓ | ✓ | | -| Manage resources using Kubernetes | | | ✓ | | ✓ | -| Retrieves Grafana resource information | ✓ | | | | | -| Built-in resource sync process | | | ✓ | ✓ | ✓ | -| Recommended for | Existing Terraform users | Existing Ansible users | Users looking to manage Grafana resources from within Kubernetes | Users looking to define Grafana resources in a Kubernetes-style YAML and users looking to get built-in workflow support and sync process | Users looking to manage Grafana resources from within Kubernetes | +| Property/Tool | Grafana Terraform Provider | Grafana Ansible Collection | Grafana Operator | Grafana Crossplane Provider | +| -------------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | +| Grafana resources supported | All major Grafana resources | Grafana Cloud stack, plugins, API keys, dashboards, data sources, and folders | Dashboards, data sources, Folders | All major Grafana resources | +| Tool format | HCL/JSON | YAML | YAML | YAML/JSON | +| Follows Kubernetes-style manifests | | | ✓ | ✓ | +| Easy dashboard building process | | | ✓ | | +| Manage resources using Kubernetes | | | ✓ | ✓ | +| Retrieves Grafana resource information | ✓ | | | | +| Built-in resource sync process | | | ✓ | ✓ | +| Recommended for | Existing Terraform users | Existing Ansible users | Users looking to manage Grafana resources from within Kubernetes | Users looking to manage Grafana resources from within Kubernetes | diff --git a/docs/sources/as-code/infrastructure-as-code/ansible/_index.md b/docs/sources/as-code/infrastructure-as-code/ansible/_index.md index 46d44ff1f90..2f977b0166e 100644 --- a/docs/sources/as-code/infrastructure-as-code/ansible/_index.md +++ b/docs/sources/as-code/infrastructure-as-code/ansible/_index.md @@ -8,22 +8,42 @@ menuTitle: Ansible title: Grafana Ansible collection weight: 110 canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/ansible/ +aliases: + - ../../infrastructure-as-code/ansible/ansible-grafana-agent-linux + - ../../infrastructure-as-code/ansible/ansible-multiple-agents +labels: + products: + - cloud --- # Grafana Ansible collection -The [Grafana Ansible collection](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/) provides configuration management resources for Grafana. You can use it to manage resources such as dashboards, Cloud stacks, folders, and more. +The [Grafana Ansible collection](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/) provides configuration management resources for Grafana. You can use it to manage: -The collection also houses the [Grafana Agent role](https://github.com/grafana/grafana-ansible-collection/tree/main/roles/grafana_agent) which can be used to deploy and manage Grafana Agent across various Linux machines. +- Grafana Cloud stacks +- Dashboards +- Data sources +- Folders +- Alerting contact points +- Notification policies +- API keys + +If your resources aren't currently available in the Grafana Ansible collection, you can manage them on Grafana Cloud programmatically by writing Ansible playbooks that use the [Ansible's built-in URI module](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/uri_module.html) to call the [HTTP APIs](/docs/grafana/latest/developers/http_api/) to manage resources for the Grafana Cloud portal, as well as those within a stack. + +## Learn more + +Refer to [Create and manage a Grafana Cloud stack using Ansible](ansible-cloud-stack/) to learn how to create a Grafana Cloud stack and add a data source and dashboard using [Ansible](https://www.ansible.com/). + +To learn more about managing Grafana with Infrastructure as code: + +- [Grafana Ansible collection documentation](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/) +- [Ansible playbook best practices](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html) +- [Grafana API documentation](/docs/grafana/latest/developers/http_api/) +- [Grafana Cloud API documentation](https://grafana.com/docs/grafana-cloud/developer-resources/api-reference/) +- [Infrastructure as Code with Terraform](/docs/grafana/latest/as-code/infrastructure-as-code/terraform/) + +## Grafana Agent (deprecated) {{< docs/shared lookup="agent-deprecation.md" source="alloy" version="next" >}} -For resources currently not available in the Grafana Ansible collection, you can manage those resources on Grafana Cloud programmatically by writing Ansible playbooks that use the [Ansible's builtin uri module](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/uri_module.html) to call the [HTTP APIs](/docs/grafana/latest/developers/http_api/) to manage resources for the Grafana Cloud portal, as well as those within a stack. - -Use the following guides to get started using Ansible to manage your Grafana Cloud stack: - -| Topic | Description | -| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| [Create and manage a Grafana Cloud stack using Ansible](ansible-cloud-stack/) | Describes how to create a Grafana Cloud stack and add a data source and dashboard using [Ansible](https://www.ansible.com/). | -| [Install Grafana Agent on a Linux host using Ansible](ansible-grafana-agent-linux/) | Describes how to install the Grafana Agent on a Linux node using Ansible and use it to push logs to Grafana Cloud. | -| [Monitor multiple Linux hosts with Grafana Agent Role](ansible-multiple-agents/) | Describes how to use the Grafana Ansible collection to manage agents across multiple Linux hosts. | +The Ansible collection also houses [Grafana Agent role](https://github.com/grafana/grafana-ansible-collection/tree/main/roles/grafana_agent), which is now deprecated. diff --git a/docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md b/docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md index 5dcab277f04..55f875fd1a7 100644 --- a/docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md +++ b/docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md @@ -4,64 +4,78 @@ keywords: - Quickstart - Grafana Cloud - Ansible -title: Create and manage a Grafana Cloud stack using Ansible +title: Create and manage your Grafana Cloud stack using Ansible +menuTitle: Manage stack using Ansible weight: 100 canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/ --- -# Create and manage a Grafana Cloud stack using Ansible +# Create and manage your Grafana Cloud stack using Ansible -Learn how to add a data source, a dashboard, and a folder to a Grafana Cloud stack using Ansible collection for Grafana. +This guide shows you how to create a Grafana Cloud stack and add a data source, dashboard, and folder using the Ansible Collection for Grafana. You'll manage your Grafana infrastructure through Ansible playbooks. ## Before you begin -Before you begin, you should have the following available: +Before you begin, make sure you have the following available: -- A Grafana Cloud account. +- A Grafana Cloud account - [Ansible](https://docs.ansible.com/ansible/latest/installation_guide/index.html) installed on your machine +## Install the Grafana Ansible collection + +Install the Grafana Ansible collection: + +```sh +ansible-galaxy collection install grafana.grafana +``` + +This collection provides all the modules needed to manage Grafana Cloud stacks and resources. + ## Create a Cloud stack -1. Create a Grafana Cloud Access Policy and get a token. - You'll need this for the Ansible playbook to be able to create a Grafana Cloud stack. - Refer to [Create a Grafana Cloud Access Policy](/docs/grafana-cloud/security-and-account-management/authentication-and-permissions/access-policies/create-access-policies/). +First, create a Grafana Cloud Access Policy and get a token. You'll need this for the Ansible playbook to be able to create a Grafana Cloud stack. Refer to [Create a Grafana Cloud Access Policy](/docs/grafana-cloud/security-and-account-management/authentication-and-permissions/access-policies/create-access-policies/). -1. Create an Ansible playbook file. +Next, create an Ansible playbook file. This Ansible playbook creates a Grafana Cloud stack using the [Cloud stack module](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/cloud_stack_module.html#ansible-collections-grafana-grafana-cloud-stack-module). - This Ansible playbook will create a Grafana Cloud stack by using the [Cloud stack module](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/cloud_stack_module.html#ansible-collections-grafana-grafana-cloud-stack-module). +To do so, create a file named `cloud-stack.yml` and add the following: - Create a file named `cloud-stack.yml` and add the following: +```yaml +- name: Create Grafana Cloud stack + connection: local + hosts: localhost - ```yaml - - name: Create Grafana Cloud stack - connection: local - hosts: localhost + vars: + grafana_cloud_api_key: '' + stack_name: '' + org_name: '' - vars: - grafana_cloud_api_key: '' - stack_name: '' - org_name: '' + tasks: + - name: Create a Grafana Cloud stack + grafana.grafana.cloud_stack: + name: '{{ stack_name }}' + stack_slug: '{{ stack_name }}' + cloud_api_key: '{{ grafana_cloud_api_key }}' + org_slug: '{{ org_name }}' + delete_protection: true + state: present + register: stack_result - tasks: - - name: Create a Grafana Cloud stack - grafana.grafana.cloud_stack: - name: '{{ stack_name }}' - stack_slug: '{{ stack_name }}' - cloud_api_key: '{{ grafana_cloud_api_key }}' - org_slug: '{{ org_name }}' - delete_protection: true - state: present - ``` + - name: Display stack URL + debug: + msg: 'Stack created at: {{ stack_result.url }}' +``` -1. Replace the following field values: - - `` with a token from the Cloud Access Policy you created in the Grafana Cloud portal. - - `` with the name of your stack. - - `` with the name of the organization in Grafana Cloud. +Replace the placeholders with your values: -## Create an API key in the Grafana stack +- _``_: Token from the Cloud Access Policy you created in the Grafana Cloud portal +- _``_: Name of your stack +- _``_: Name of the organization in Grafana Cloud -Create an API key in the Grafana stack. -You'll need this key to configure Ansible to be able to create data source, folders, and dashboards. +The playbook registers the stack creation result and displays the stack URL, which you'll need for subsequent resource management. + +## Create an API key in your Grafana stack + +Create an API key in the Grafana stack. You'll need this key to configure Ansible to create data sources, folders, and dashboards. 1. Log into your Grafana Cloud instance. 2. Click **Administration** and select **API keys**. @@ -70,171 +84,169 @@ You'll need this key to configure Ansible to be able to create data source, fold 5. In **Role**, select **Admin** or **Editor** to associate the role with this API key. 6. Click **Copy** to save it for later use. -## Add a data source +## Add resources using playbooks -This guide uses the InfluxDB data source. -The required arguments vary depending on the type of data source you select. +### Add a data source -1. Create a file named `data-source.yml` and add the following: +The following steps use the InfluxDB data source. The required arguments vary depending on the type of data source you select. - ```yaml - - name: Add/Update data source - connection: local - hosts: localhost +Create a file named `data-source.yml`: - vars: - data_sources: - [ - { - name: '', - type: 'influxdb', - url: '', - user: '', - secureJsonData: { password: '' }, - database: '', - id: , - uid: '', - access: 'proxy', - }, - ] +```yaml +- name: Add/Update data source + connection: local + hosts: localhost - grafana_api_key: '' - stack_name: '' + vars: + grafana_url: 'https://.grafana.net' + grafana_api_key: '' + data_source_config: + name: '' + type: 'influxdb' + url: '' + user: '' + secureJsonData: + password: '' + database: '' + uid: '' + access: 'proxy' - tasks: - - name: Create/Update Data sources - grafana.grafana.datasource: - datasource: '{{ item }}' - stack_slug: '{{ stack_name }}' - grafana_api_key: '{{ grafana_api_key }}' - state: present - loop: '{{ data_sources }}' - ``` + tasks: + - name: Create/Update Data source + grafana.grafana.datasource: + dataSource: '{{ data_source_config }}' + grafana_url: '{{ grafana_url }}' + grafana_api_key: '{{ grafana_api_key }}' + state: present +``` -1. Replace the following field values: - - `` with the name of the data source to be added in Grafana. - - `` with URL of your data source. - - `` with the username for authenticating with your data source. - - `` with the password for authenticating with your data source. - - `` with name of your database. - - `` with the ID for your data source in Grafana. - - `` wth the UID for your data source in Grafana. - - `` with the name of your stack. - - `` with the [API key created in the Grafana instance](#create-an-api-key-in-the-grafana-stack). +Replace the placeholders with your values: -## Add a folder +- _``_: Name of the data source to be added in Grafana +- _``_: URL of your data source +- _``_: Username for authenticating with your data source +- _``_: Password for authenticating with your data source +- _``_: Name of your database +- _``_: UID for your data source in Grafana +- _``_: Name of your stack +- _``_: API key created in the Grafana instance -This Ansible playbook creates a folder in your Grafana instance by using the [Folder module](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/folder_module.html#ansible-collections-grafana-grafana-folder-module). +### Add a folder -1. Create a file named `folder.yml` and add the following: +This playbook creates a folder in your Grafana instance using the [Folder module](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/folder_module.html#ansible-collections-grafana-grafana-folder-module). - ```yaml - - name: Add/Update Folders - connection: local - hosts: localhost +Create a file named `folder.yml`: - vars: - folders: [{ title: '', uid: '' }] +```yaml +- name: Add/Update Folders + connection: local + hosts: localhost - stack_name: '' - grafana_api_key: +vars: + grafana_url: 'https://.grafana.net' + grafana_api_key: '' + folders: + - title: '' + uid: '' - tasks: - - name: Create/Update a Folder in Grafana - grafana.grafana.folder: - title: '{{ item.title }}' - uid: '{{ item.uid }}' - stack_slug: '{{ stack_name }}' - grafana_api_key: '{{ grafana_api_key }}' - state: present - loop: '{{ folders }}' - ``` + tasks: + - name: Create/Update a Folder in Grafana + grafana.grafana.folder: + title: '{{ item.title }}' + uid: '{{ item.uid }}' + grafana_url: '{{ grafana_url }}' + grafana_api_key: '{{ grafana_api_key }}' + state: present + loop: '{{ folders }}' +``` -1. Replace the following field values: - - `` with the name of the folder to be added in Grafana. - - `` with the UID for your folder in Grafana. - - `` with the name of your stack. - - `` with the [API key created in the Grafana instance](#create-an-api-key-in-the-grafana-stack). +Replace the placeholders with your values: -## Add a dashboard to the folder +- _``_: Name of the folder to be added in Grafana +- _``_: UID for your folder in Grafana +- _``_: Name of your stack +- _``_: API key created in the Grafana instance -This Ansible playbook iterates through the dashboard JSON source code files in the folder referenced in `dashboards_path` and adds them in the Grafana instance by using the [Dashboard module](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/dashboard_module.html#ansible-collections-grafana-grafana-dashboard-module). +### Add a dashboard to the folder -1. Create a file named `dashboard.yml` and add the following: +This playbook iterates through the dashboard JSON source code files in the folder referenced in `dashboards_path` and adds them to the Grafana instance using the [Dashboard module](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/dashboard_module.html#ansible-collections-grafana-grafana-dashboard-module). - ```yaml - - name: Add/Update Dashboards - connection: local - hosts: localhost +Create a file named `dashboard.yml`: - vars: - dashboards_path: # Example "./dashboards" - stack_name: "" - grafana_api_key: +```yaml +- name: Add/Update Dashboards + connection: local + hosts: localhost - tasks: - - name: Find dashboard files - find: - paths: "{{ dashboards_path }}" - file_type: file - recurse: Yes - patterns: "*.json" - register: files_matched - no_log: True + vars: + grafana_url: 'https://.grafana.net' + grafana_api_key: '' + dashboards_path: '' # Example "./dashboards" - - name: Create list of dashboard file names - set_fact: - dashboard_file_names: "{{ dashboard_file_names | default ([]) + [item.path] }}" - loop: "{{ files_matched.files }}" - no_log: True + tasks: + - name: Find dashboard files + find: + paths: '{{ dashboards_path }}' + file_type: file + recurse: true + patterns: '*.json' + register: files_matched + no_log: true - - name: Create/Update a dashboard - grafana.grafana.dashboard: - dashboard: "{{ lookup('ansible.builtin.file','{{ item }}' ) }}" - stack_slug: "{{ stack_name }}" - grafana_api_key: "{{ grafana_api_key }}" - state: present - loop: "{{ dashboard_file_names }}" - ``` + - name: Create list of dashboard file names + set_fact: + dashboard_file_names: '{{ dashboard_file_names | default([]) + [item.path] }}' + loop: '{{ files_matched.files }}' + no_log: true -1. Replace the following field values: - - `` with the path to the folder containing dashboard JSON source code files. - - `` with the name of your stack. - - `` with the [API key created in the Grafana instance](#create-an-api-key-in-the-grafana-stack). + - name: Create/Update a dashboard + grafana.grafana.dashboard: + dashboard: "{{ lookup('ansible.builtin.file', item) }}" + grafana_url: '{{ grafana_url }}' + grafana_api_key: '{{ grafana_api_key }}' + state: present + loop: '{{ dashboard_file_names }}' +``` + +Replace the placeholders with your values: + +- _``_: Path to the folder containing dashboard JSON source code files +- _``_: Name of your stack +- _``_: API key created in the Grafana instance ## Run the Ansible playbooks In a terminal, run the following commands from the directory where all of the Ansible playbooks are located. -1. To create the Grafana Cloud stack. +Create the Grafana Cloud stack: - ```shell - ansible-playbook cloud-stack.yml - ``` +```sh +ansible-playbook cloud-stack.yml +``` -1. To add a data source to the Grafana stack. +Add a data source to the Grafana stack: - ```shell - ansible-playbook data-source.yml - ``` +```sh +ansible-playbook data-source.yml +``` -1. To add a folder to the Grafana stack +Add a folder to the Grafana stack: - ```shell - ansible-playbook folder.yml - ``` +```sh +ansible-playbook folder.yml +``` -1. To add a dashboard to the folder in your Grafana stack. +Add a dashboard to the folder in your Grafana stack: - ```shell - ansible-playbook dashboard.yml - ``` +```sh +ansible-playbook dashboard.yml +``` -## Validation +## Validate your configuration -Once you run the Ansible playbooks, you should be able to verify the following: +After you've run the Ansible playbooks, you can verify the following: -- The new Grafana stack is created and visible in the Cloud Portal. +- The new Grafana Cloud stack is created and visible in the Cloud Portal. ![Cloud Portal](/static/img/docs/grafana-cloud/terraform/cloud_portal_tf.png) @@ -242,18 +254,22 @@ Once you run the Ansible playbooks, you should be able to verify the following: ![InfluxDB datasource](/media/docs/grafana-cloud/screenshot-influxdb_datasource_tf.png) -- A new folder in Grafana. - In the following image, a folder named `Demos` was added. +- A new folder is available in your Grafana stack. In the following image, a folder named `Demos` was added. ![Folder](/media/docs/grafana-cloud/screenshot-folder_tf.png) -- A new dashboard in the Grafana stack. - In the following image a dashboard named `InfluxDB Cloud Demos` was created inside the "Demos" folder. +- A new dashboard is visible in the Grafana stack. In the following image, a dashboard named `InfluxDB Cloud Demos` was created inside the "Demos" folder. ![InfluxDB dashboard](/static/img/docs/grafana-cloud/terraform/influxdb_dashboard_tf.png) -## Summary +## Next steps -In this guide, you created a Grafana Cloud stack along with a data source, folder, and dashboard imported from a JSON file using Ansible. +You've successfully created a Grafana Cloud stack along with a data source, a folder, and a dashboard using Ansible. Your Grafana infrastructure is now managed through code. -To learn more about managing Grafana using Ansible, refer to the [Grafana Ansible collection](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/). +To learn more about managing Grafana with Infrastructure as code: + +- [Grafana Ansible collection documentation](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/) +- [Ansible playbook best practices](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html) +- [Grafana API documentation](/docs/grafana/latest/developers/http_api/) +- [Grafana Cloud API documentation](https://grafana.com/docs/grafana-cloud/developer-resources/api-reference/) +- [Infrastructure as Code with Terraform](/docs/grafana/latest/as-code/infrastructure-as-code/terraform/) diff --git a/docs/sources/as-code/infrastructure-as-code/ansible/ansible-grafana-agent-linux/index.md b/docs/sources/as-code/infrastructure-as-code/ansible/ansible-grafana-agent-linux/index.md deleted file mode 100644 index 8fa3a6f8357..00000000000 --- a/docs/sources/as-code/infrastructure-as-code/ansible/ansible-grafana-agent-linux/index.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -keywords: - - Infrastructure as Code - - Quickstart - - Grafana Cloud - - Ansible -title: Install Grafana Agent on a Linux host using Ansible -weight: 200 -canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/ansible/ansible-grafana-agent-linux/ ---- - -# Install Grafana Agent on a Linux host using Ansible - -{{< docs/shared lookup="agent-deprecation.md" source="alloy" version="next" >}} - -This guide shows how to install Grafana Agent on a Linux host using [Ansible](https://www.ansible.com/) and to use it to push logs to Grafana Cloud. - -## Before you begin - -Before you begin, you should have the following available: - -- A Grafana Cloud account. -- A Linux machine -- Command line (terminal) access to that Linux machine with `unzip` binary installed -- Account permissions sufficient to install and use Grafana Agent on the Linux machine -- [Ansible](https://docs.ansible.com/ansible/latest/installation_guide/index.html) installed on the Linux machine - -## Choose your Grafana Agent installation method - -This guide covers two methods for installing and configuring Grafana Agent using Ansible: - -- Installing Grafana Agent in Flow mode -- Installing Grafana Agent in static mode - -Depending on your specific needs and the configuration of your environment, you may choose one method over the other for better compatibility or ease of setup. - - - -### Install Grafana Agent in flow mode using Ansible - -This Ansible playbook installs Grafana Agent in Flow mode and also creates a systemd service to manage it. - -It creates a user named `grafana-agent` on the Linux machine for running Grafana Agent. - -1. Create a file named `grafana-agent.yml` and add the following: - -```yaml -- name: Install Grafana Agent Flow - hosts: all - become: true - tasks: - - name: Install Grafana Agent Flow - ansible.builtin.include_role: - name: grafana.grafana.grafana_agent - vars: - grafana_agent_mode: flow - # Change config file on the host to .river - grafana_agent_config_filename: config.river - # Change config file to be copied - grafana_agent_provisioned_config_file: '' - # Remove default flags - grafana_agent_flags_extra: - server.http.listen-addr: '0.0.0.0:12345' -``` - -1. Replace the following field values: - - `` with the path to river configuration file on the Ansible Controller (Localhost). - -### Install Grafana Agent in static mode using Ansible - -This Ansible playbook installs Grafana Agent in static mode and also creates a systemd service to manage it. -It creates a user named `grafana-agent` on the Linux machine for running Grafana Agent. - -1. Create a file named `grafana-agent.yml` and add the following: - -```yaml -- name: Install Grafana Agent in static mode - hosts: all - become: true - - vars: - grafana_cloud_api_key: - logs_username: # Example - 411478 - loki_url: # Example - https://logs-prod-017.grafana.net/loki/api/v1/push - tasks: - - name: Install Grafana Agent in static mode - ansible.builtin.include_role: - name: grafana_agent - vars: - grafana_agent_logs_config: - configs: - - clients: - - basic_auth: - password: '{{ grafana_cloud_api_key }}' - username: '{{ logs_username }}' - url: '{{ loki_url }}' - name: default - positions: - filename: /tmp/positions.yaml - scrape_configs: - - job_name: integrations/node_exporter_direct_scrape - static_configs: - - targets: - - localhost - labels: - instance: hostname - __path__: /var/log/*.log - job: integrations/node_exporter - target_config: - sync_period: 10s -``` - -1. Replace the following field values: - - `` with a token from the Cloud Access Policy you created in the Grafana Cloud portal. - - `` with the Loki Username - - `` with the push endpoint URL of Loki Instance - -## Run the Ansible playbook on the Linux machine - -In the Linux machine's terminal, run the following command from the directory where the Ansible playbook is located. - -```shell -ansible-playbook grafana-agent.yml -``` - -## Validate - - - -1. Grafana Agent service on the Linux machine should be `active` and `running`. You should see a similar output: - - -```shell -$ sudo systemctl status grafana-agent.service - grafana-agent.service - Grafana Agent - Loaded: loaded (/etc/systemd/system/grafana-agent.service; enabled; vendor preset: enabled) - Active: active (running) since Wed 2022-07-20 09:56:15 UTC; 36s ago - Main PID: 3176 (agent-linux-amd) - Tasks: 8 (limit: 515) - Memory: 92.5M - CPU: 380ms - CGroup: /system.slice/grafana-agent.service - └─3176 /usr/local/bin/agent-linux-amd64 --config.file=/etc/grafana-cloud/agent-config.yaml -``` - -1. In a Grafana Cloud stack, click **Explore** in the left-side menu. - -1. At the top of the page, use the dropdown menu to select your Loki logs data source. In the Log Browser, run the query `{job="integrations/node_exporter"}` - - ![Loki Logs](/static/img/docs/grafana-cloud/ansible/ansible-agent-logs.png) - -## Summary - -In this guide, you installed Grafana Agent on a Linux node using Ansible and used it to pushed logs to Grafana Cloud. - -To learn more about the Grafana Ansible collection, refer to the [GitHub repository](https://github.com/grafana/grafana-ansible-collection) or its [documentation](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/index.html). diff --git a/docs/sources/as-code/infrastructure-as-code/ansible/ansible-multiple-agents/index.md b/docs/sources/as-code/infrastructure-as-code/ansible/ansible-multiple-agents/index.md deleted file mode 100644 index c6769e5ebde..00000000000 --- a/docs/sources/as-code/infrastructure-as-code/ansible/ansible-multiple-agents/index.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -menuTitle: Monitor multiple Linux hosts with the grafana_agent role -title: Monitor multiple Linux hosts with grafana_agent role -weight: 300 -canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/ansible/ansible-multiple-agents/ ---- - -# Monitor multiple Linux hosts with the `grafana_agent` role - -{{< docs/shared lookup="agent-deprecation.md" source="alloy" version="next" >}} - -Monitoring multiple Linux hosts can be difficult. -To make it easier, you can use the `grafana_agent` role with the [Grafana Ansible collection](../). -This guide shows how to use the `grafana_agent` Ansible role to deploy and manage Grafana Agent across multiple Linux hosts so you can monitor them using Grafana Cloud. - -## Before you begin - -Before you begin, you should have: - -- Linux hosts -- SSH access to the Linux hosts -- Account permissions sufficient to install and use Grafana Agent on the Linux hosts - -## Install the Grafana Ansible collection - -The [`grafana_agent` role](https://github.com/grafana/grafana-ansible-collection/tree/main/roles/grafana_agent) is available in the Grafana Ansible collection as of the 1.1.0 release. - -To install the Grafana Ansible collection, run this command: - -``` -ansible-galaxy collection install grafana.grafana -``` - -## Create an Ansible inventory file - -Next, you will set up your hosts and create an inventory file. - -1. Create your hosts and add public SSH keys to them. - - This example uses eight Linux hosts: two Ubuntu hosts, two CentOS hosts, two Fedora hosts, and two Debian hosts. - -1. Create an Ansible inventory file. - - The Ansible inventory, which resides in a file named `inventory`, looks similar to this: - - ``` - 146.190.208.216 # hostname = ubuntu-01 - 146.190.208.190 # hostname = ubuntu-02 - 137.184.155.128 # hostname = centos-01 - 146.190.216.129 # hostname = centos-02 - 198.199.82.174 # hostname = debian-01 - 198.199.77.93 # hostname = debian-02 - 143.198.182.156 # hostname = fedora-01 - 143.244.174.246 # hostname = fedora-02 - ``` - -1. Create an `ansible.cfg` file within the same directory as `inventory`, with the following values: - ``` - [defaults] - inventory = inventory # Path to the inventory file - private_key_file = ~/.ssh/id_rsa # Path to my private SSH Key - remote_user=root # username - ``` - {{< admonition type="note" >}} - If you are copying the previously listed files, remove the comments (#). - {{< /admonition >}} - -## Use the `grafana_agent` Ansible role - -Next you will create an Ansible playbook that calls the `grafana_agent` role from the `grafana.grafana` Ansible collection. - -To use the `grafana_agent` Ansible role: - -1. Create a file named `deploy-agent.yml` in the same directory as `ansible.cfg` and `inventory` and add the configuration below. - - ```yaml - - name: Install Grafana Agent - hosts: all - become: true - - vars: - grafana_cloud_api_key: - metrics_username: # Example - 825019 - logs_username: # Example - 411478 - prometheus_url: # Example - https://prometheus-us-central1.grafana.net/api/prom/push - loki_url: # Example - https://logs-prod-017.grafana.net/loki/api/v1/push - tasks: - - name: Install Grafana Agent - ansible.builtin.include_role: - name: grafana.grafana.grafana_agent - vars: - grafana_agent_metrics_config: - configs: - - name: integrations - remote_write: - - basic_auth: - password: '{{ grafana_cloud_api_key }}' - username: '{{ metrics_username }}' - url: '{{ prometheus_url }}' - - global: - scrape_interval: 60s - wal_directory: /tmp/grafana-agent-wal - grafana_agent_logs_config: - configs: - - name: default - clients: - - basic_auth: - password: '{{ grafana_cloud_api_key }}' - username: '{{ logs_username }}' - url: '{{ loki_url }}' - positions: - filename: /tmp/positions.yaml - target_config: - sync_period: 10s - scrape_configs: - - job_name: varlogs - static_configs: - - targets: [localhost] - labels: - instance: ${HOSTNAME:-default} - job: varlogs - __path__: /var/log/*log - grafana_agent_integrations_config: - node_exporter: - enabled: true - instance: ${HOSTNAME:-default} - prometheus_remote_write: - - basic_auth: - password: '{{ grafana_cloud_api_key }}' - username: '{{ metrics_username }}' - url: '{{ prometheus_url }}' - grafana_agent_env_vars: - HOSTNAME: '%H' - ``` - - The playbook calls the `grafana_agent` role from the `grafana.grafana` Ansible collection. - - The Agent configuration in this playbook send metrics and logs from the Linux hosts to Grafana Cloud along with the hostname of each instance - - Refer to the [Grafana Ansible documentation](https://github.com/grafana/grafana-ansible-collection/tree/main/roles/grafana_agent#role-variables) to understand the other variables you can pass to the `grafana_agent` role. - - When deploying the Agent across multiple instances for monitoring them, It is essential that the Agent is able to auto-detect the hostname for ease in monitoring. - Notice that the label `instance` has been set to the value `${HOSTNAME:-default}`, which is substituted by the value of the HOSTNAME environment variable in the Linux host. - - To read more about the variable substitution, refer to the Grafana Agent [node_exporter_config](/docs/grafana-cloud/send-data/agent/static/configuration/integrations/node-exporter-config/) documentation. - -1. To run the playbook, run this command: - - ``` - ansible-playbook deploy-agent.yml - ``` - -{{< admonition type="note" >}} -You can place the `deploy-agent.yml`, `ansible.cfg` and `inventory` files in different directories based on your needs. -{{< /admonition >}} - -## Check that logs and metrics are being ingested into Grafana Cloud - -Logs and metrics will soon be available in Grafana Cloud. -To test this, use the Explore feature. -Click the **Explore** icon (compass icon) in the vertical navigation bar. - -### Check logs - -To check logs: - -1. Use the drop-down menu at the top of the page to select your Loki logs data source. - -1. In the log browser, run the query `{instance="centos-01"}` where `centos-01` is the hostname of one of the Linux hosts. - - If you see log lines (shown in the example below), logs are being received. - - {{< figure alt="Grafana Explore showing a graph and log output from the preceding query" src="/static/assets/img/blog/ansible-to-manage-agent1.png" >}} - - If no log lines appear, logs aren't being collected. - -### Check metrics - -To check metrics: - -1. Use the drop-down menu at the top of the page to select your Prometheus data source. - -1. Run the query `{instance="centos-01"}` where `centos-01` is the hostname of one of the Linux hosts. - - If you see a metrics graph and table (shown in the example below), metrics are being received. - - {{< figure alt="Grafana Explore showing a graph and metrics table output from the preceding query" src="/static/assets/img/blog/ansible-to-manage-agent2.png" >}} - - If no metrics appear, metrics aren't being collected. - -### View dashboards - -Now that you have logs and metrics in Grafana, you can use dashboards to view them. -Here's an example of one of the prebuilt dashboards included with the Linux integration in Grafana Cloud: - -{{< figure alt="The Grafana Node Exporter integration dashboard showing panels of visualizations" src="/static/assets/img/blog/ansible-to-manage-agent3.png" >}} - -Using the **Instance** drop-down in the dashboard, you can select from the hostnames where you deployed Grafana Agent and start monitoring them. - -## Summary - -The `grafana_agent` Ansible role makes it easy to deploy and manage Grafana Agent across multiple machines. -This example showed Grafana Agent deployments across eight Linux hosts, but it's possible to monitor more hosts using the`grafana_agent` role. -To add monitor more Linux hosts, update the `inventory` file and re-run the Ansible playbook. - -To learn more about the Grafana Ansible collection, see its [GitHub repository](https://github.com/grafana/grafana-ansible-collection) or its [documentation](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/index.html). diff --git a/docs/sources/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd.md b/docs/sources/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd.md index 3c92270e860..0531a0c803f 100644 --- a/docs/sources/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd.md +++ b/docs/sources/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd.md @@ -5,295 +5,298 @@ keywords: - Grafana Cloud - Grafana Operator - ArgoCD -title: Manage Dashboards with GitOps Using ArgoCD +title: Manage dashboards with GitOps using ArgoCD +menuTitle: Manage dashboards with ArgoCD weight: 110 canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd/ --- -# Managing Grafana Dashboards with GitOps Using ArgoCD +# Manage Grafana dashboards with GitOps using ArgoCD -This guide will walk you through setting up a continuous deployment pipeline using ArgoCD to synchronize your Grafana dashboards with a Git repository. We'll use the Grafana Dashboard Custom Resource provided by the Grafana Operator to manage dashboard configurations declaratively. +This guide shows you how to set up a continuous deployment pipeline using ArgoCD to synchronize your Grafana dashboards with a Git repository. You'll use the Grafana Dashboard Custom Resource provided by the Grafana Operator to manage dashboard configurations declaratively. ## Prerequisites +Before you begin, make sure you have the following: + - An existing Grafana Cloud stack -- A Kubernetes cluster with Grafana Operator installed, as shown in [Grafana Operator Installation](/docs/grafana-cloud/as-code/infrastructure-as-code/grafana-operator/#installing-the-grafana-operator). -- ArgoCD installed on your Kubernetes cluster. Refer the [Installation Guide](https://argo-cd.readthedocs.io/en/stable/getting_started/). -- Git repository to store your dashboard configurations. +- A Kubernetes cluster with Grafana Operator installed, as shown in [Grafana Operator Installation](/docs/grafana-cloud/as-code/infrastructure-as-code/grafana-operator/#installing-the-grafana-operator) +- ArgoCD installed on your Kubernetes cluster. Refer to the [ArgoCD Installation Guide](https://argo-cd.readthedocs.io/en/stable/getting_started/) +- A Git repository to store your dashboard configurations -## Set Up Your Git Repository +## Set up your Git repository -Within the repository, create a directory structure to organize your grafana and dashboard configurations. For this tutorial, lets create a folder named `grafana`. +Create a directory structure in your repository to organize your Grafana and dashboard configurations. For this tutorial, create a folder named `grafana`. -## Grafana Operator Setup +## Set up the Grafana Operator -The Grafana Operator allows us to authenticate with the Grafana instance using the Grafana Custom Resource (CR). +The Grafana Operator allows you to authenticate with the Grafana instance using the Grafana Custom Resource (CR). -1. **Create the Grafana API Token Secret:** +### Create the Grafana API Token Secret -Store the Grafana API Token in a secret with the following content in a file named `grafana-token.yml` in the `grafana` folder in your Git repo: +Store the Grafana API Token in a secret. Create a file named `grafana-token.yml` in the `grafana` folder in your Git repository: ```yaml apiVersion: v1 kind: Secret metadata: name: grafana-cloud-credentials - namespace: + namespace: '' stringData: - GRAFANA_CLOUD_INSTANCE_TOKEN: + GRAFANA_CLOUD_INSTANCE_TOKEN: '' type: Opaque ``` -Replace the following field values: +Replace the placeholders with your values: -- `` with API key from the Grafana instance. To create an API key, refer [Grafana API Key Documentation](/docs/grafana/latest/administration/api-keys/). -- `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +- _``_: API key from your Grafana instance. To create an API key, refer to [Grafana API Key Documentation](/docs/grafana/latest/administration/api-keys/) +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster -2. **Configure the Grafana Custom Resource:** +### Configure the Grafana Custom Resource -Set up connection to your Grafana Cloud instance by creating a file named `grafana-cloud.yml` in the `grafana` folder in your Git repo with the following contents: +Set up the connection to your Grafana Cloud instance. Create a file named `grafana-cloud.yml` in the `grafana` folder in your Git repository: ```yaml apiVersion: grafana.integreatly.org/v1beta1 kind: Grafana metadata: - name: - namespace: + name: '' + namespace: '' labels: - dashboards: + dashboards: '' spec: external: - url: https://.grafana.net/ + url: https://.grafana.net/ apiKey: name: grafana-cloud-credentials key: GRAFANA_CLOUD_INSTANCE_TOKEN ``` -Replace the following field values: +Replace the placeholders with your values: -- `` with API key from the Grafana instance. -- `` with the name of your Grafana Cloud Stack. -- `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +- _``_: Name of your Grafana Cloud Stack +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster -## Add Dashboards to a Git repository +## Add dashboards to your Git repository -In your `grafana` directory, Create a sub-folder called `dashboards`. For this tutorial, we will create 3 seperate dashboards. +In your `grafana` directory, create a sub-folder called `dashboards`. -1. Under `dashboards` folder, Create a file named `simple-dashboard.yaml` with the following content for the first dashboard: +This guide shows you how to creates three separate dashboards. For all dashboard configurations, replace the placeholders with your values: - ```yaml - apiVersion: grafana.integreatly.org/v1beta1 - kind: GrafanaDashboard - metadata: - name: grafanadashboard-sample - namespace: - spec: - resyncPeriod: 30s - instanceSelector: - matchLabels: - dashboards: - json: > - { - "id": null, - "title": "Simple Dashboard", - "tags": [], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "graphTooltip": 1, - "panels": [], - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "time_options": [], - "refresh_intervals": [] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "refresh": "5s", - "schemaVersion": 17, - "version": 0, - "links": [] - } - ``` +- _``_: Name of your Grafana Cloud Stack +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster - Replace the following field values: - - `` with the name of your Grafana Cloud Stack. - - `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +### Create a simple dashboard -1. Under `dashboards` folder, Create a file named `dashboard-from-cm.yaml` with the following content for the second dashboard: +Under the `dashboards` folder, create a file named `simple-dashboard.yaml`: - ```yaml - apiVersion: v1 - kind: ConfigMap - metadata: - name: dashboard-definition - namespace: - spec: - resyncPeriod: 30s - instanceSelector: - matchLabels: - dashboards: - json: > - { - "id": null, - "title": "Simple Dashboard from ConfigMap", - "tags": [], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "graphTooltip": 1, - "panels": [], - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "time_options": [], - "refresh_intervals": [] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "refresh": "5s", - "schemaVersion": 17, - "version": 0, - "links": [] - } - --- - apiVersion: grafana.integreatly.org/v1beta1 - kind: GrafanaDashboard - metadata: - name: grafanadashboard-from-configmap - namespace: - spec: - instanceSelector: - matchLabels: - dashboards: - configMapRef: - name: dashboard-definition - key: json - ``` +```yaml +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: grafanadashboard-sample + namespace: '' +spec: + resyncPeriod: 30s + instanceSelector: + matchLabels: + dashboards: '' + json: > + { + "id": null, + "title": "Simple Dashboard", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "graphTooltip": 1, + "panels": [], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "time_options": [], + "refresh_intervals": [] + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "refresh": "5s", + "schemaVersion": 17, + "version": 0, + "links": [] + } +``` - Replace the following field values: - - `` with the name of your Grafana Cloud Stack. - - `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +### Create a dashboard from ConfigMap -1. Under `dashboards` folder, Create a file named `dashboard-from-id.yaml` with the following content for the third dashboard: +Under the `dashboards` folder, create a file named `dashboard-from-cm.yaml`: - ```yaml - apiVersion: grafana.integreatly.org/v1beta1 - kind: GrafanaDashboard - metadata: - name: node-exporter-latest - namespace: - spec: - instanceSelector: - matchLabels: - dashboards: - grafanaCom: - id: 1860 - ``` +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: dashboard-definition + namespace: +data: + json: > + { + "id": null, + "title": "Simple Dashboard from ConfigMap", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "graphTooltip": 1, + "panels": [], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "time_options": [], + "refresh_intervals": [] + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "refresh": "5s", + "schemaVersion": 17, + "version": 0, + "links": [] + } +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: grafanadashboard-from-configmap + namespace: '' +spec: + instanceSelector: + matchLabels: + dashboards: '' + configMapRef: + name: dashboard-definition + key: json +``` - Replace the following field values: - - `` with the name of your Grafana Cloud Stack. - - `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +### Create a dashboard from Grafana.com -## Configure Argo CD to Sync the Git Repository +Under the `dashboards` folder, create a file named `dashboard-from-id.yaml`: -Once all changes are committed to Git, Log in to the Argo CD user interface or use the CLI. +```yaml +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: node-exporter-latest + namespace: '' +spec: + instanceSelector: + matchLabels: + dashboards: '' + grafanaCom: + id: 1860 +``` -2. Create an Argo CD application to manage the synchronization: +## Configure ArgoCD to sync the Git repository - **Using UI**: - - Navigate to 'New App' and fill out the form with your Git repository details and the path to your `grafana` folder. - - Make sure to tick mark directory Recurse. - - Set the sync policy to `Automatic`. +After you commit all changes to Git, log in to the ArgoCD user interface or use the CLI. - **Using CLI**: - - Prepare an application manifest named `argo-application.yaml` with the configuration pointing to your Git repository: +### Create an ArgoCD application - ```yaml - apiVersion: argoproj.io/v1alpha1 - kind: Application - metadata: - name: Grafana - namespace: - spec: - destination: - name: '' - namespace: '' - server: 'https://kubernetes.default.svc' - source: - path: - repoURL: '' - targetRevision: HEAD - directory: - recurse: true - sources: [] - project: default - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - retry: - limit: 2 - backoff: - duration: 5s - maxDuration: 3m0s - factor: 2 - ``` +**Using the UI:** - Replace the following field values: - - `` with the URL of your GIT Repository. - - `` with the path to the `grafana` folder. - - `` with the namespace where ArgoCD is deployed in Kubernetes Cluster. +1. Navigate to **New App** and complete the form with your Git repository details and the path to your `grafana` folder +2. Enable **Directory Recurse** +3. Set the sync policy to **Automatic** - - Create the application in Argo CD: +**Using the CLI:** - ```shell - kubectl apply -f argo-application.yaml - ``` +Prepare an application manifest named `argo-application.yaml`: -## Verify Sync Status in Argo CD +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: Grafana + namespace: '' +spec: + destination: + name: '' + namespace: '' + server: 'https://kubernetes.default.svc' + source: + path: '' + repoURL: '' + targetRevision: HEAD + directory: + recurse: true + sources: [] + project: default + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + retry: + limit: 2 + backoff: + duration: 5s + maxDuration: 3m0s + factor: 2 +``` -1. Monitor the newly created Argo CD application, ensuring that it successfully syncs your dashboard configuration. +Replace the placeholders with your values: -2. Visit the Argo CD dashboard and check the sync status. If it's successful, your Grafana dashboard should be up to date with the configuration from your Git repository. +- _``_: URL of your Git repository +- _``_: Path to the `grafana` folder in your repository +- _``_: Namespace where ArgoCD is deployed in your Kubernetes cluster -## Updating the Dashboards +Create the application in ArgoCD: + +```sh +kubectl apply -f argo-application.yaml +``` + +## Verify sync status in ArgoCD + +1. Monitor the newly created ArgoCD application to ensure it successfully syncs your dashboard configuration +2. Visit the ArgoCD dashboard and check the sync status. If it's successful, your Grafana dashboards should be up to date with the configuration from your Git repository + +## Update your dashboards To update an existing dashboard: -1. Make changes to the dashboard JSON configuration in your Git repository. -2. Commit and push the changes. -3. Argo CD will detect the update and synchronize the changes to your Cutom Resource. -4. Grafana Operator will then sync changes to the Grafana Instance. +1. Make changes to the dashboard JSON configuration in your Git repository +2. Commit and push the changes +3. ArgoCD detects the update and synchronizes the changes to your Custom Resource +4. Grafana Operator then syncs changes to the Grafana instance -## Validating the Grafana Dashboard Update +### Validate your dashboard updates -Log in to your Grafana dashboard and confirm that the changes have been applied. You should see the dashboard update reflected in the Grafana UI. +Log in to your Grafana dashboard and confirm that the changes are applied. You should see the dashboard updates reflected in the Grafana UI. -## Additional Tips +## Next steps -- You can also install the Grafana Operator's Helm Chart using ArgoCD to manage your setup with GitOps. -- You can follow a similar setup for Grafana Dashboards and Folders. +You've successfully set up a GitOps workflow to manage Grafana dashboards using ArgoCD and the Grafana Operator. Your dashboards are now version-controlled and can be consistently deployed across environments. This approach provides a reliable and auditable way to manage observability dashboards and scale your operations. -## Conclusion +To learn more about managing Grafana using Grafana Operator: -You've set up a GitOps workflow to manage Grafana dashboards using Argo CD and the Grafana Operator. Your dashboards are now version-controlled and can be consistently deployed across environments. This approach provides a reliable and auditable way to manage observability dashboards and scale your operations. +- [Grafana Operator documentation](https://grafana.github.io/grafana-operator/docs/) +- [Grafana dashboard provisioning](/docs/grafana/latest/administration/provisioning/#dashboards) +- [ArgoCD best practices](https://argo-cd.readthedocs.io/en/stable/user-guide/best_practices/) -To learn more about managing Grafana using Grafana Operator, see the [Grafana Operator documentation](https://grafana.github.io/grafana-operator/docs/). +### Additional considerations + +- You can install the Grafana Operator's Helm Chart using ArgoCD to manage your setup with GitOps +- You can follow a similar setup for Grafana Folders and other resources diff --git a/docs/sources/as-code/infrastructure-as-code/grafana-operator/operator-dashboards-folders-datasources.md b/docs/sources/as-code/infrastructure-as-code/grafana-operator/operator-dashboards-folders-datasources.md index 9d8617e12c6..e5bbc1467b8 100644 --- a/docs/sources/as-code/infrastructure-as-code/grafana-operator/operator-dashboards-folders-datasources.md +++ b/docs/sources/as-code/infrastructure-as-code/grafana-operator/operator-dashboards-folders-datasources.md @@ -5,26 +5,27 @@ keywords: - Grafana Cloud - Grafana Operator title: Manage folders, data sources, and dashboards using Grafana Operator +menuTitle: Manage resources with the Grafana Operator weight: 100 canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/grafana-operator/operator-dashboards-folders-datasources/ --- -# Creating and managing folders, data sources, and dashboards using the Grafana Operator +# Manage folders, data sources, and dashboards using the Grafana Operator -Learn how to manage data sources, folders and dashboard, using Grafana Operator. +This guide shows you how to manage data sources, folders, and dashboards using the Grafana Operator. You'll create these resources declaratively using Kubernetes custom resources. ## Prerequisites -Before you begin, you should have the following available: +Before you begin, make sure you have the following: -- An existing Grafana Cloud stack. -- Grafana Operator Installed in your Cluster, as shown in [Grafana Operator Installation](/docs/grafana-cloud/as-code/infrastructure-as-code/grafana-operator/#installing-the-grafana-operator). +- An existing Grafana Cloud stack +- Grafana Operator installed in your cluster, as shown in [Grafana Operator Installation](/docs/grafana-cloud/as-code/infrastructure-as-code/grafana-operator/#installing-the-grafana-operator) -## Grafana Operator Setup +## Set up the Grafana Operator -The Grafana Operator allows us to authenticate with the Grafana instance using the Grafana Custom Resource (CR). +The Grafana Operator allows you to authenticate with your Grafana instance using the Grafana Custom Resource (CR). -1. **Create the Grafana API Token Secret:** +### Create the Grafana API Token Secret Store the Grafana API Token in a secret with the following content in a file named `grafana-token.yml`: @@ -33,61 +34,64 @@ apiVersion: v1 kind: Secret metadata: name: grafana-cloud-credentials - namespace: + namespace: '' stringData: - GRAFANA_CLOUD_INSTANCE_TOKEN: + GRAFANA_CLOUD_INSTANCE_TOKEN: '' type: Opaque ``` -Replace the following field values: +Replace the placeholders with your values: -- `` with API key from the Grafana instance. To create an API key, refer [Grafana API Key Documentation](/docs/grafana/latest/administration/api-keys/). -- `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +- _``_: API key from your Grafana instance. To create an API key, refer to [Grafana API Key Documentation](/docs/grafana/latest/administration/api-keys/) +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster -2. **Configure the Grafana Custom Resource:** +### Configure the Grafana Custom Resource -Set up connection to your Grafana Cloud instance by creating a file named `grafana-cloud.yml` with the following contents: +Set up connection to your Grafana Cloud instance. Create a file named `grafana-cloud.yml`: ```yaml apiVersion: grafana.integreatly.org/v1beta1 kind: Grafana metadata: - name: - namespace: + name: '' + namespace: '' labels: - dashboards: + dashboards: '' spec: external: - url: https://.grafana.net/ + url: https://.grafana.net/ apiKey: name: grafana-cloud-credentials key: GRAFANA_CLOUD_INSTANCE_TOKEN ``` -Replace the following field values: +Replace the placeholders with your values: -- `` with API key from the Grafana instance. -- `` with the name of your Grafana Cloud Stack. -- `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +- _``_: Name of your Grafana Cloud stack +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster ## Add a data source -The following steps use the Prometheus data source. The required arguments vary depending on the data source you select. +{{< admonition type="note" >}} -1. **Create the Data Source Configuration:** +This example uses the Prometheus data source. Note that the required arguments vary depending on the data source you select. -Save a new YAML file `datasource.yml` with the following content: +{{< /admonition >}} + +### Create a data source configuration + +Create and save a new YAML file `datasource.yml` with your data source's configuration: ```yaml apiVersion: grafana.integreatly.org/v1beta1 kind: GrafanaDatasource metadata: - name: - namespace: + name: '' + namespace: '' spec: instanceSelector: matchLabels: - dashboards: + dashboards: '' allowCrossNamespaceImport: true datasource: access: proxy @@ -95,74 +99,76 @@ spec: jsonData: timeInterval: 5s tlsSkipVerify: true - name: + name: '' type: prometheus - url: + url: '' ``` -Replace the following field values: +Replace the placeholders with your values: -- `` with the name of the data source to be added in Grafana. -- `` with URL of your data source. -- `` with the name of your Grafana Cloud Stack. -- `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +- _``_: Name of the data source to be added in Grafana +- _``_: URL of your data source +- _``_: Name of your Grafana Cloud stack +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster -## Add a dashboard to a folder +### Add a dashboard to a folder -Use the following YAML definition to create a simple dashboard in the Grafana instance under a custom folder. If the folder defined under spec.folder fields doesnt not exist, The operator will create it before placing the dashboard inside the folder. +Use the following YAML definition to create a simple dashboard in the Grafana instance under a custom folder. If the folder defined under the `spec.folder` field doesn't exist, the operator creates it before placing the dashboard inside the folder. -1. **Prepare the Dashboard Configuration File:** - -In `dashboard.yml`, define the dashboard and assign it to a folder: +Prepare the dashboard configuration. In `dashboard.yml`, define the dashboard and assign it to a folder: ```yaml apiVersion: grafana.integreatly.org/v1beta1 kind: GrafanaDashboard metadata: - name: - namespace: + name: '' + namespace: '' spec: instanceSelector: matchLabels: - dashboards: - folder: "" + dashboards: '' + folder: '' json: > - { - "title": "as-code dashboard", - “uid” : “ascode” - } + { + "title": "as-code dashboard", + "uid" : "ascode" + } ``` -Replace the following field values: +Replace the placeholders with your values: -- `` with the name of the folder in which you want the Dashboard to be created. -- `` with the name of your Grafana Cloud Stack. -- `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +- _``_: Name of the folder in which you want the dashboard to be created +- _``_: Name of your Grafana Cloud stack +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster -## Apply Kubernetes Manifests +## Apply the Kubernetes manifests In a terminal, run the following commands from the directory where all of the above Kubernetes YAML definitions are located. -1. Create Kubernetes Custom resources for all of the above configurations. +Create Kubernetes Custom resources for all of the configurations: - ```shell - kubectl apply -f grafana-token.yml grafana-cloud.yml datasource.yml dashboard.yml - ``` +```sh +kubectl apply -f grafana-token.yml grafana-cloud.yml datasource.yml dashboard.yml +``` -## Validation +## Validate your configuration -Once you apply the configurations, you should be able to verify the following: +After you apply the configurations, verify that: -- A new data source is visible in Grafana. In the following image a datasource named `InfluxDB` was created. +- A new data source is visible in Grafana. In the following image, a data source named `InfluxDB` was created. ![InfluxDB datasource](/static/img/docs/grafana-cloud/terraform/influxdb_datasource_tf.png) -- A new dashboard and folder in Grafana. In the following image a dashboard named `InfluxDB Cloud Demos` was created inside the `Demos` folder. +- A new dashboard and folder have been created in Grafana. In the following image, a dashboard named `InfluxDB Cloud Demos` was created inside the `Demos` folder. ![InfluxDB dashboard](/static/img/docs/grafana-cloud/grizzly/grizzly-folder-dashboard-datasource.png) -## Conclusion +## Next steps -In this guide, you created a data source, folder, and dashboard using the Grafana Operator. +You've successfully created a data source, folder, and dashboard using the Grafana Operator. Your Grafana resources are now managed declaratively through Kubernetes custom resources. -To learn more about managing Grafana using Grafana Operator, see the [Grafana Operator documentation](https://grafana.github.io/grafana-operator/docs/). +To learn more about managing Grafana: + +- [Grafana Operator documentation](https://grafana.github.io/grafana-operator/docs/) +- [Grafana dashboard provisioning](/docs/grafana/latest/administration/provisioning/#dashboards) +- [Grafana data source provisioning](/docs/grafana/latest/administration/provisioning/#data-sources) diff --git a/docs/sources/as-code/observability-as-code/schema-v2/_index.md b/docs/sources/as-code/observability-as-code/schema-v2/_index.md index 5eb693f0f1d..9be869a8391 100644 --- a/docs/sources/as-code/observability-as-code/schema-v2/_index.md +++ b/docs/sources/as-code/observability-as-code/schema-v2/_index.md @@ -13,7 +13,7 @@ labels: - enterprise - oss title: JSON schema v2 -weight: 200 +weight: 500 canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/ aliases: - ../../observability-as-code/schema-v2/ # /docs/grafana/next/observability-as-code/schema-v2/ From 5d26bc5ee11dd73e3f1ac598bba01993c98d6048 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Thu, 20 Nov 2025 11:18:50 +0100 Subject: [PATCH 006/378] FeatureToggles: Remove feedbackButton unused toggle (#114222) --- packages/grafana-data/src/types/featureToggles.gen.ts | 4 ---- pkg/services/featuremgmt/registry.go | 7 ------- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 ---- pkg/services/featuremgmt/toggles_gen.json | 3 ++- 5 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b9afd13cec0..acb3b1796de 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -748,10 +748,6 @@ export interface FeatureToggles { */ alertingNotificationsStepMode?: boolean; /** - * Enables a button to send feedback from the Grafana UI - */ - feedbackButton?: boolean; - /** * Enable unified storage search UI */ unifiedStorageSearchUI?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 68e88830be4..bc4d4c282ee 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1298,13 +1298,6 @@ var ( FrontendOnly: true, Expression: "true", }, - { - Name: "feedbackButton", - Description: "Enables a button to send feedback from the Grafana UI", - Stage: FeatureStageExperimental, - Owner: grafanaOperatorExperienceSquad, - HideFromDocs: true, - }, { Name: "unifiedStorageSearchUI", Description: "Enable unified storage search UI", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index e0f2bec13bc..8a874135d00 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -169,7 +169,6 @@ alertingEnrichmentPerRule,experimental,@grafana/alerting-squad,false,false,false alertingEnrichmentAssistantInvestigations,experimental,@grafana/alerting-squad,false,false,false alertingAIAnalyzeCentralStateHistory,experimental,@grafana/alerting-squad,false,false,false alertingNotificationsStepMode,GA,@grafana/alerting-squad,false,false,true -feedbackButton,experimental,@grafana/grafana-operator-experience-squad,false,false,false unifiedStorageSearchUI,experimental,@grafana/search-and-storage,false,false,false elasticsearchCrossClusterSearch,GA,@grafana/partner-datasources,false,false,false unifiedHistory,experimental,@grafana/grafana-search-navigate-organise,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 9fb1c573406..f6c70624e8d 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -687,10 +687,6 @@ const ( // Enables simplified step mode in the notifications section FlagAlertingNotificationsStepMode = "alertingNotificationsStepMode" - // FlagFeedbackButton - // Enables a button to send feedback from the Grafana UI - FlagFeedbackButton = "feedbackButton" - // FlagUnifiedStorageSearchUI // Enable unified storage search UI FlagUnifiedStorageSearchUI = "unifiedStorageSearchUI" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index daf3c48c40c..44ed33ad19c 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1731,7 +1731,8 @@ "metadata": { "name": "feedbackButton", "resourceVersion": "1753448760331", - "creationTimestamp": "2024-12-02T17:08:15Z" + "creationTimestamp": "2024-12-02T17:08:15Z", + "deletionTimestamp": "2025-11-20T09:53:52Z" }, "spec": { "description": "Enables a button to send feedback from the Grafana UI", From 3c0d5745fe768369fb4ac9f07de652ad55f0f392 Mon Sep 17 00:00:00 2001 From: Charandas <542168+charandas@users.noreply.github.com> Date: Thu, 20 Nov 2025 02:23:36 -0800 Subject: [PATCH 007/378] chore: remove remaining references to singular namespace (#114208) --- pkg/apiserver/endpoints/filters/path_rewriter_test.go | 8 ++++---- .../apis/provisioning/resources/repository_test.go | 4 ++-- .../apiserver/endpoints/request/namespace_test.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/apiserver/endpoints/filters/path_rewriter_test.go b/pkg/apiserver/endpoints/filters/path_rewriter_test.go index 2340c3ff1f8..d8d8278ccf7 100644 --- a/pkg/apiserver/endpoints/filters/path_rewriter_test.go +++ b/pkg/apiserver/endpoints/filters/path_rewriter_test.go @@ -28,20 +28,20 @@ func Test_WithPathRewriters(t *testing.T) { handler := WithPathRewriters(mockHandler, rewriters) t.Run("should rewrite path", func(t *testing.T) { - req, err := http.NewRequest("GET", "/apis/scope.grafana.app/namespaces/stack-1234/query/blah", nil) + req, err := http.NewRequest("GET", "/apis/scope.grafana.app/namespaces/stacks-1234/query/blah", nil) assert.NoError(t, err) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, "/apis/scope.grafana.app/namespaces/stack-1234/query", rr.Body.String()) + assert.Equal(t, "/apis/scope.grafana.app/namespaces/stacks-1234/query", rr.Body.String()) }) t.Run("should ignore requests that don't match", func(t *testing.T) { - req, err := http.NewRequest("GET", "/apis/scope.grafana.app/namespaces/stack-1234/scopes/1", nil) + req, err := http.NewRequest("GET", "/apis/scope.grafana.app/namespaces/stacks-1234/scopes/1", nil) assert.NoError(t, err) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, "/apis/scope.grafana.app/namespaces/stack-1234/scopes/1", rr.Body.String()) + assert.Equal(t, "/apis/scope.grafana.app/namespaces/stacks-1234/scopes/1", rr.Body.String()) }) } diff --git a/pkg/registry/apis/provisioning/resources/repository_test.go b/pkg/registry/apis/provisioning/resources/repository_test.go index 5240d9d2758..cf845ba7440 100644 --- a/pkg/registry/apis/provisioning/resources/repository_test.go +++ b/pkg/registry/apis/provisioning/resources/repository_test.go @@ -536,7 +536,7 @@ func TestCheckResourceOwnership(t *testing.T) { "name": "test-resource", "annotations": map[string]interface{}{ utils.AnnoKeyManagerKind: "terraform", - utils.AnnoKeyManagerIdentity: "tf-stack-1", + utils.AnnoKeyManagerIdentity: "tf-stacks-1", }, }, }, @@ -546,7 +546,7 @@ func TestCheckResourceOwnership(t *testing.T) { Identity: "repo-1", }, expectError: true, - expectedMessage: "resource 'test-resource' is managed by terraform 'tf-stack-1' and cannot be modified by repo 'repo-1'", + expectedMessage: "resource 'test-resource' is managed by terraform 'tf-stacks-1' and cannot be modified by repo 'repo-1'", }, } diff --git a/pkg/services/apiserver/endpoints/request/namespace_test.go b/pkg/services/apiserver/endpoints/request/namespace_test.go index 6c565d43e19..47c75fa2e77 100644 --- a/pkg/services/apiserver/endpoints/request/namespace_test.go +++ b/pkg/services/apiserver/endpoints/request/namespace_test.go @@ -26,7 +26,7 @@ func TestNamespaceMapper(t *testing.T) { orgId: 123, expected: "org-123", }, - // an invalid use-case, but just documenting that it's handled as stack-0 + // an invalid use-case, but just documenting that it's handled as stacks-0 // this currently prevents the need to have the Mapper return (mapped, err) instead of just mapped. // err checking is avoided for now to keep the usage fluent { From 6e9a33e712606dbf73fff2d2b86bfc7169af0c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Thu, 20 Nov 2025 12:01:21 +0100 Subject: [PATCH 008/378] Dynamic Dashboards: Add more tracking (#114098) --- .../edit-pane/VizPanelEditableElement.tsx | 8 +++++- .../scene/PanelMenuBehavior.tsx | 9 +++++++ .../scene/UnconfiguredPanel.tsx | 2 ++ .../DashboardSceneSerializer.test.ts | 7 ++++- .../serialization/DashboardSceneSerializer.ts | 8 ++++++ .../dashboard-scene/utils/interactions.ts | 26 +++++++++++++++++-- .../dashboard-scene/utils/tracking.test.ts | 5 ++++ .../dashboard-scene/utils/tracking.ts | 11 +++++++- .../SaveDashboard/useDashboardSave.tsx | 8 +++++- .../app/features/dashboard/utils/tracking.ts | 5 +++- 10 files changed, 82 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx index 200357fa881..39ed9beaa52 100644 --- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx @@ -21,6 +21,7 @@ import { BulkActionElement } from '../scene/types/BulkActionElement'; import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem'; import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; +import { DashboardInteractions } from '../utils/interactions'; import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils'; import { MultiSelectedVizPanelsEditableElement } from './MultiSelectedVizPanelsEditableElement'; @@ -95,6 +96,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc public useEditPaneOptions = useEditPaneOptions.bind(this); public onDelete() { + DashboardInteractions.panelActionClicked('duplicate', getPanelIdForVizPanel(this.panel), 'edit_pane'); const layout = dashboardSceneGraph.getLayoutManagerFor(this.panel); layout.removePanel?.(this.panel); } @@ -116,11 +118,13 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc } public onDuplicate() { + DashboardInteractions.panelActionClicked('duplicate', getPanelIdForVizPanel(this.panel), 'edit_pane'); const layout = dashboardSceneGraph.getLayoutManagerFor(this.panel); layout.duplicatePanel?.(this.panel); } public onCopy() { + DashboardInteractions.panelActionClicked('copy', getPanelIdForVizPanel(this.panel), 'edit_pane'); const dashboard = getDashboardSceneFor(this.panel); dashboard.copyPanel(this.panel); } @@ -147,7 +151,9 @@ const OpenPanelEditViz = ({ panel }: OpenPanelEditVizProps) => { + + + )} + {openPane === 'outline' && ( + + togglePane('')} /> + + )} + {openPane === 'add' && ( + + togglePane('')} /> + + )} + + togglePane('add')} + /> + + togglePane('settings')} + /> + togglePane('outline')} + /> + + + + + + + + ); +}; + +export const VerticalTabs: StoryFn = (args) => { + const [openPane, setOpenPane] = useState('queries'); + + const togglePane = (pane: string) => { + setOpenPane(pane); + }; + + const containerStyle = css({ + flexGrow: 1, + height: '600px', + display: 'flex', + flexDirection: 'column', + position: 'relative', + overflow: 'hidden', + gap: '16px', + }); + + const vizWrapper = css({ + height: '30%', + display: 'flex', + }); + + const contextValue = useSidebar({ + position: args.position, + tabsMode: true, + edgeMargin: 0, + }); + + return ( + +

+
{renderBox('Visualization')}
+ + {openPane === 'queries' && ( + + + + )} + {openPane === 'transformations' && ( + + + + )} + + togglePane('queries')} + /> + togglePane('transformations')} + /> + + + +
+ + ); +}; + +function renderBox(label: string) { + return ( + + {label} + + ); +} + +export default meta; diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx new file mode 100644 index 00000000000..187fbc90c4b --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx @@ -0,0 +1,51 @@ +import { render, screen } from '@testing-library/react'; +import React, { act } from 'react'; + +import { Sidebar, useSidebar } from './Sidebar'; + +describe('Sidebar', () => { + it('should render sidebar', async () => { + render(); + + act(() => screen.getByLabelText('Settings').click()); + + // Verify pane is open + expect(screen.getByTestId('sidebar-pane-header-title')).toBeInTheDocument(); + + act(() => screen.getByLabelText('Dock').click()); + + // Verify wrapper pushes content when docked + const wrapper = screen.getByTestId('sidebar-test-wrapper'); + expect(wrapper).toHaveStyle('padding-right: 352px'); + + // Close pane + act(() => screen.getByLabelText('Close').click()); + // Verify pane is closed + expect(screen.queryByTestId('sidebar-pane-header-title')).not.toBeInTheDocument(); + }); +}); + +function TestSetup() { + const [openPane, setOpenPane] = React.useState(''); + const contextValue = useSidebar({ + position: 'right', + hasOpenPane: openPane !== '', + }); + + return ( +
+ + {openPane === 'settings' && ( + + setOpenPane('')} /> + + )} + + setOpenPane('settings')} /> + + + + +
+ ); +} diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx new file mode 100644 index 00000000000..2d9b407a1ad --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx @@ -0,0 +1,167 @@ +import { css, cx } from '@emotion/css'; +import { ReactNode, useContext } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { t } from '@grafana/i18n'; + +import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; + +import { SidebarButton } from './SidebarButton'; +import { SidebarPaneHeader } from './SidebarPaneHeader'; +import { SidebarResizer } from './SidebarResizer'; +import { SIDE_BAR_WIDTH_ICON_ONLY, SIDE_BAR_WIDTH_WITH_TEXT, SidebarContext, SidebarContextValue } from './useSidebar'; + +export interface Props { + children?: ReactNode; + contextValue: SidebarContextValue; +} + +export function SidebarComp({ children, contextValue }: Props) { + const styles = useStyles2(getStyles); + const theme = useTheme2(); + const { isDocked, position, tabsMode, hasOpenPane, edgeMargin, bottomMargin } = contextValue; + + const className = cx({ + [styles.container]: true, + [styles.undockedPaneOpen]: hasOpenPane && !isDocked, + [styles.containerLeft]: position === 'left', + [styles.containerTabsMode]: tabsMode, + }); + + const style = { [position]: theme.spacing(edgeMargin), bottom: theme.spacing(bottomMargin) }; + + return ( + +
+ {!tabsMode && } + {children} +
+
+ ); +} + +export interface SiderbarToolbarProps { + children?: ReactNode; +} + +export function SiderbarToolbar({ children }: SiderbarToolbarProps) { + const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); + + if (!context) { + throw new Error('Sidebar.Toolbar must be used within a Sidebar component'); + } + + return ( +
+ {children} +
+ {context.hasOpenPane && ( + + )} +
+ ); +} + +export function SidebarDivider() { + const styles = useStyles2(getStyles); + + return
; +} + +export interface SidebarOpenPaneProps { + children?: ReactNode; +} + +export function SidebarOpenPane({ children }: SidebarOpenPaneProps) { + const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); + + if (!context) { + throw new Error('Sidebar.OpenPane must be used within a Sidebar component'); + } + + const className = cx(styles.openPane, context.position === 'right' ? styles.openPaneRight : styles.openPaneLeft); + + return ( +
+ {children} +
+ ); +} + +export const getStyles = (theme: GrafanaTheme2) => { + return { + container: css({ + display: 'flex', + position: 'absolute', + flexDirection: 'row', + flex: '1 1 0', + border: `1px solid ${theme.colors.border.weak}`, + background: theme.colors.background.primary, + borderRadius: theme.shape.radius.default, + zIndex: theme.zIndex.navbarFixed, + bottom: 0, + top: 0, + right: 0, + }), + containerTabsMode: css({ + position: 'relative', + }), + containerLeft: css({ + right: 'unset', + flexDirection: 'row-reverse', + left: 0, + borderRadius: theme.shape.radius.default, + }), + undockedPaneOpen: css({ + boxShadow: theme.shadows.z3, + }), + toolbar: css({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: theme.spacing(1, 0), + flexGrow: 0, + gap: theme.spacing(1), + overflow: 'hidden', + width: theme.spacing(SIDE_BAR_WIDTH_WITH_TEXT), + }), + toolbarIconsOnly: css({ + width: theme.spacing(SIDE_BAR_WIDTH_ICON_ONLY), + }), + divider: css({ + height: '1px', + background: theme.colors.border.weak, + width: '100%', + }), + flexGrow: css({ + flexGrow: 1, + }), + openPane: css({ + width: '280px', + flexGrow: 1, + paddingBottom: theme.spacing(2), + }), + openPaneRight: css({ + borderRight: `1px solid ${theme.colors.border.weak}`, + }), + openPaneLeft: css({ + borderLeft: `1px solid ${theme.colors.border.weak}`, + }), + }; +}; + +export const Sidebar = Object.assign(SidebarComp, { + Toolbar: SiderbarToolbar, + Button: SidebarButton, + OpenPane: SidebarOpenPane, + Divider: SidebarDivider, + PaneHeader: SidebarPaneHeader, +}); + +export { type SidebarPosition, type SidebarContextValue, useSidebar } from './useSidebar'; diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx new file mode 100644 index 00000000000..f436d9630f6 --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx @@ -0,0 +1,159 @@ +import { css, cx } from '@emotion/css'; +import { useContext } from 'react'; + +import { GrafanaTheme2, IconName, isIconName } from '@grafana/data'; + +import { useStyles2 } from '../../themes/ThemeContext'; +import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins'; +import { getActiveButtonStyles } from '../Button/Button'; +import { Icon } from '../Icon/Icon'; +import { Tooltip } from '../Tooltip/Tooltip'; + +import { SidebarContext } from './useSidebar'; + +export interface Props { + icon: IconName; + active?: boolean; + onClick?: () => void; + title: string; + tooltip?: string; +} + +export function SidebarButton({ icon, active, onClick, title, tooltip }: Props) { + const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); + + if (!context) { + throw new Error('Sidebar.Button must be used within a Sidebar component'); + } + + const buttonClass = cx( + styles.button, + context.compact && styles.compact, + active && styles.active, + context.position === 'left' && styles.leftButton + ); + + return ( + + + + ); +} + +function renderIcon(icon: IconName | React.ReactNode, compact?: boolean) { + if (!icon) { + return null; + } + + if (isIconName(icon)) { + return ; + } + + return icon; +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + button: css({ + label: 'toolbar-button', + position: 'relative', + display: 'flex', + flexDirection: 'column', + minHeight: theme.spacing(theme.components.height.md), + padding: theme.spacing(0, 1), + width: '100%', + overflow: 'hidden', + // borderRadius: theme.shape.radius.sm, + lineHeight: `${theme.components.height.md * theme.spacing.gridSize - 2}px`, + fontWeight: theme.typography.fontWeightMedium, + color: theme.colors.text.secondary, + background: 'transparent', + border: `none`, + + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: theme.transitions.create(['background-color', 'border-color', 'color'], { + duration: theme.transitions.duration.short, + }), + }, + + '&:focus, &:focus-visible': { + ...getFocusStyles(theme), + zIndex: 1, + }, + + '&:focus:not(:focus-visible)': getMouseFocusStyles(theme), + + '&[disabled], &:disabled': { + cursor: 'not-allowed', + opacity: theme.colors.action.disabledOpacity, + background: theme.colors.action.disabledBackground, + boxShadow: 'none', + + '&:hover': { + color: theme.colors.text.disabled, + background: theme.colors.action.disabledBackground, + boxShadow: 'none', + }, + }, + + '&:hover, &:focus-visible': { + color: theme.colors.text.primary, + background: theme.colors.action.hover, + }, + + '&:active': { + ...getActiveButtonStyles(theme.colors.secondary, 'solid'), + }, + }), + compact: css({ + height: theme.spacing(theme.components.height.md), + padding: theme.spacing(0, 1), + width: theme.spacing(5), + }), + active: css({ + color: theme.colors.text.primary, + background: theme.colors.action.selected, + '&::before': { + display: 'block', + content: '" "', + position: 'absolute', + right: 0, + top: 0, + height: '100%', + width: '2px', + borderRadius: theme.shape.radius.default, + backgroundImage: theme.colors.gradients.brandVertical, + }, + }), + buttonWrapper: css({ + display: 'flex', + flexDirection: 'column', + width: '100%', + whiteSpace: 'nowrap', + }), + iconWrapper: css({}), + title: css({ + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.text.secondary, + textOverflow: 'ellipsis', + overflow: 'hidden', + textAlign: 'center', + whiteSpace: 'nowrap', + }), + titleActive: css({ + color: theme.colors.text.primary, + }), + leftButton: css({ + '&::before': { + right: 'unset', + left: 0, + top: 0, + height: '100%', + }, + }), + }; +}; diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx new file mode 100644 index 00000000000..75393777879 --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx @@ -0,0 +1,55 @@ +import { css } from '@emotion/css'; +import { ReactNode } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { t } from '@grafana/i18n'; + +import { useStyles2 } from '../../themes/ThemeContext'; +import { IconButton } from '../IconButton/IconButton'; +import { Text } from '../Text/Text'; + +export interface Props { + children?: ReactNode; + title: string; + onClose?: () => void; +} + +export function SidebarPaneHeader({ children, onClose, title }: Props) { + const styles = useStyles2(getStyles); + + return ( +
+ {onClose && ( + + )} + + {title} + +
+ {children} +
+ ); +} + +export const getStyles = (theme: GrafanaTheme2) => { + return { + wrapper: css({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(1.5), + height: theme.spacing(6), + gap: theme.spacing(1), + borderBottom: `1px solid ${theme.colors.border.weak}`, + }), + flexGrow: css({ + flexGrow: 1, + }), + }; +}; diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarResizer.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarResizer.tsx new file mode 100644 index 00000000000..7a243f00b54 --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/SidebarResizer.tsx @@ -0,0 +1,93 @@ +import { css } from '@emotion/css'; +import { useCallback, useContext, useRef } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../themes/ThemeContext'; + +import { SidebarContext } from './useSidebar'; + +export function SidebarResizer() { + const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); + const resizerRef = useRef(null); + const dragStart = useRef(null); + + if (!context) { + throw new Error('Sidebar.Resizer must be used within a Sidebar component'); + } + + const { onResize, position } = context; + + const onPointerDown = useCallback( + (e: React.PointerEvent) => { + if (resizerRef.current === null) { + return; + } + + resizerRef.current.setPointerCapture(e.pointerId); + dragStart.current = e.clientX; + }, + [resizerRef] + ); + + const onPointerMove = useCallback( + (e: React.PointerEvent) => { + if (dragStart.current === null) { + return; + } + + const diff = e.clientX - dragStart.current; + dragStart.current = e.clientX; + + onResize(position === 'right' ? -diff : diff); + }, + [dragStart, onResize, position] + ); + + const onPointerUp = useCallback( + (e: React.PointerEvent) => { + dragStart.current = null; + }, + [dragStart] + ); + + return ( +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + right: css({ + position: 'absolute', + width: theme.spacing.gridSize, + left: -theme.spacing.gridSize, + top: theme.shape.radius.default, + bottom: theme.shape.radius.default, + cursor: 'col-resize', + zIndex: 1, + '&:hover': { + borderRight: `1px solid ${theme.colors.primary.border}`, + }, + }), + left: css({ + position: 'absolute', + width: theme.spacing.gridSize, + right: -theme.spacing.gridSize, + top: theme.shape.radius.default, + bottom: theme.shape.radius.default, + cursor: 'col-resize', + zIndex: 1, + '&:hover': { + borderLeft: `1px solid ${theme.colors.primary.border}`, + }, + }), + }; +}; diff --git a/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx b/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx new file mode 100644 index 00000000000..03d04b2f81a --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx @@ -0,0 +1,113 @@ +import { clamp } from 'lodash'; +import React, { useCallback } from 'react'; + +import { useTheme2 } from '../../themes/ThemeContext'; + +export type SidebarPosition = 'left' | 'right'; + +export interface SidebarContextValue { + isDocked: boolean; + position: SidebarPosition; + compact: boolean; + hasOpenPane?: boolean; + tabsMode?: boolean; + outerWrapperProps: React.HTMLAttributes; + paneWidth: number; + bottomMargin: number; + edgeMargin: number; + contentMargin: number; + onDockChange: () => void; + onResize: (diff: number) => void; +} + +export const SidebarContext: React.Context = React.createContext< + SidebarContextValue | undefined +>(undefined); + +export interface UseSideBarOptions { + hasOpenPane?: boolean; + position?: SidebarPosition; + tabsMode?: boolean; + compactDefault?: boolean; + /** defaults to 2 grid units (16px) */ + bottomMargin?: number; + /** defaults to 2 grid units (16px) */ + edgeMargin?: number; + /** defaults to 2 grid units (16px) */ + contentMargin?: number; +} + +export const SIDE_BAR_WIDTH_ICON_ONLY = 5; +export const SIDE_BAR_WIDTH_WITH_TEXT = 8; + +export function useSidebar({ + hasOpenPane, + position = 'right', + tabsMode, + compactDefault = true, + bottomMargin = 2, + edgeMargin = 2, + contentMargin = 2, +}: UseSideBarOptions): SidebarContextValue { + const theme = useTheme2(); + const [isDocked, setIsDocked] = React.useState(false); + const [paneWidth, setPaneWidth] = React.useState(280); + const [compact, setCompact] = React.useState(compactDefault); + // Used to accumulate drag distance to know when to change compact mode + const [_, setCompactDrag] = React.useState(0); + + const onDockChange = useCallback(() => setIsDocked((prev) => !prev), []); + + const prop = position === 'right' ? 'paddingRight' : 'paddingLeft'; + const toolbarWidth = + ((compact ? SIDE_BAR_WIDTH_ICON_ONLY : SIDE_BAR_WIDTH_WITH_TEXT) + edgeMargin + contentMargin) * + theme.spacing.gridSize; + + const outerWrapperProps = { + style: { + [prop]: isDocked && hasOpenPane ? paneWidth + toolbarWidth : toolbarWidth, + }, + }; + + const onResize = useCallback( + (diff: number) => { + setPaneWidth((prevWidth) => { + // If no pane is open we use the resize action to toggle compact mode (button text visibility) + if (!hasOpenPane) { + setCompactDrag((prevDrag) => { + const newDrag = prevDrag + diff; + if (newDrag < -20 && !compact) { + setCompact(true); + return 0; + } else if (newDrag > 20 && compact) { + setCompact(false); + return 0; + } + + return newDrag; + }); + + return prevWidth; + } + + return clamp(prevWidth + diff, 100, 500); + }); + }, + [hasOpenPane, compact] + ); + + return { + isDocked, + onDockChange, + onResize, + outerWrapperProps, + position, + compact, + hasOpenPane, + tabsMode, + paneWidth, + edgeMargin, + bottomMargin, + contentMargin, + }; +} diff --git a/packages/grafana-ui/src/index.ts b/packages/grafana-ui/src/index.ts index b46ce1d33bb..e567e3dd561 100644 --- a/packages/grafana-ui/src/index.ts +++ b/packages/grafana-ui/src/index.ts @@ -466,6 +466,7 @@ export { RunnerPlugin } from './slate-plugins/runner'; export { SelectionShortcutsPlugin } from './slate-plugins/selection_shortcuts'; export { SlatePrism, type Token } from './slate-plugins/slate-prism'; export { SuggestionsPlugin } from './slate-plugins/suggestions'; +export { Sidebar, useSidebar, type SidebarPosition, type SidebarContextValue } from './components/Sidebar/Sidebar'; // @deprecated import from @grafana/schema export { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 4492a771832..28512178dd7 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -8995,6 +8995,11 @@ "series-color-picker-popover": { "y-axis-usage": "Use right y-axis" }, + "sidebar": { + "close": "Close", + "dock": "Dock", + "undock": "Undock" + }, "slider": { "drag-handle-aria-label": "Use arrow keys to change the value" }, From e9116cd23c4fc8d83542e062033c4ada561bd534 Mon Sep 17 00:00:00 2001 From: Jay Clifford <45856600+Jayclifford345@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:27:04 +0000 Subject: [PATCH 013/378] chore(testid): added missing testid to add datasource button (#114169) * added missing testid * added to pages.ts * added e2e to datasource button * fixed lint --- packages/grafana-e2e-selectors/src/selectors/pages.ts | 3 +++ .../datasources/components/DataSourceAddButton.tsx | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 7716ccc1436..96d033d5b14 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -85,6 +85,9 @@ export const versionedPages = { dataSources: { [MIN_GRAFANA_VERSION]: (dataSourceName: string) => `Data source list item ${dataSourceName}`, }, + dataSourceAddButton: { + '12.4.0': 'data-testid data-source-add-button', + }, }, EditDataSource: { url: { diff --git a/public/app/features/datasources/components/DataSourceAddButton.tsx b/public/app/features/datasources/components/DataSourceAddButton.tsx index fa1da6b9953..f765188b50d 100644 --- a/public/app/features/datasources/components/DataSourceAddButton.tsx +++ b/public/app/features/datasources/components/DataSourceAddButton.tsx @@ -1,5 +1,6 @@ import { useCallback } from 'react'; +import { Pages } from '@grafana/e2e-selectors'; import { Trans } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { LinkButton } from '@grafana/ui'; @@ -16,7 +17,12 @@ export function DataSourceAddButton(): JSX.Element | null { }, []); return canCreateDataSource ? ( - + Add new data source ) : null; From b2f022fb5e2bab8a18f51a2bf219a4d8c394a0c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Thu, 20 Nov 2025 12:37:12 +0100 Subject: [PATCH 014/378] Dynamic Dashboards: Make outline open by default (#114146) --- e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts | 2 -- .../dashboards-edit-custom-variables.spec.ts | 1 - e2e-playwright/dashboard-new-layouts/utils.ts | 1 - .../dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx | 2 +- .../features/dashboard-scene/scene/NavToolbarActions.test.tsx | 2 +- .../scene/new-toolbar/actions/EditDashboardSwitch.test.tsx | 2 +- .../new-toolbar/actions/MakeDashboardEditableButton.test.tsx | 2 +- public/app/features/dashboard-scene/utils/tracking.ts | 2 +- 8 files changed, 5 insertions(+), 9 deletions(-) diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts index 49d55f8ca04..ef38379f006 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts @@ -22,8 +22,6 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); - await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click(); - // Should be able to click Variables item in outline to see add variable button await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); await expect( diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts index ce5465c1e33..24c61e5e960 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts @@ -199,7 +199,6 @@ test.describe( .click(); // Open the modal editor in the side pane - await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.node('Variables')).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('foo')).click(); await openModal(dashboardPage, selectors); diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index c59f782b37a..da629ac8141 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -48,7 +48,6 @@ export const flows = { }, async newEditPaneVariableClick(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); - await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx index c2e896fce83..9ca7fbab86e 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx @@ -35,7 +35,7 @@ export function DashboardEditPaneRenderer({ editPane, isEditPaneCollapsed, onTog const isNewElement = selection?.isNewElement() ?? false; const [outlineCollapsed, setOutlineCollapsed] = useLocalStorage( 'grafana.dashboard.edit-pane.outline.collapsed', - true + false ); const [outlinePaneSize = 0.4, setOutlinePaneSize] = useLocalStorage('grafana.dashboard.edit-pane.outline.size', 0.4); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index e42df99d74a..e276bf80e46 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -162,7 +162,7 @@ describe('NavToolbarActions', () => { await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton)); expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ dashboardUid: 'dash-1', - outlineExpanded: false, + outlineExpanded: true, }); }); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditDashboardSwitch.test.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditDashboardSwitch.test.tsx index 583a3da8818..e941c98cadb 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditDashboardSwitch.test.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditDashboardSwitch.test.tsx @@ -58,7 +58,7 @@ describe('EditDashboardSwitch', () => { it('should call DashboardInteractions.editButtonClicked with outlineExpanded:true if grafana.dashboard.edit-pane.outline.collapsed is undefined', async () => { render(); await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton)); - expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: false }); + expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: true }); }); it('should call DashboardInteractions.editButtonClicked with outlineExpanded:true if grafana.dashboard.edit-pane.outline.collapsed is false', async () => { diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.test.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.test.tsx index 05ce93adde1..87788a2b187 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.test.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.test.tsx @@ -59,7 +59,7 @@ describe('MakeDashboardEditableButton', () => { it('should call DashboardInteractions.editButtonClicked with outlineExpanded:true if grafana.dashboard.edit-pane.outline.collapsed is undefined', async () => { render(); await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton)); - expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: false }); + expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: true }); }); it('should call DashboardInteractions.editButtonClicked with outlineExpanded:true if grafana.dashboard.edit-pane.outline.collapsed is false', async () => { diff --git a/public/app/features/dashboard-scene/utils/tracking.ts b/public/app/features/dashboard-scene/utils/tracking.ts index fde95ce2ead..638f5bd781b 100644 --- a/public/app/features/dashboard-scene/utils/tracking.ts +++ b/public/app/features/dashboard-scene/utils/tracking.ts @@ -46,7 +46,7 @@ export const trackDeleteDashboardElement = (element: EditableDashboardElementInf export const trackDashboardSceneEditButtonClicked = (dashboardUid?: string) => { DashboardInteractions.editButtonClicked({ - outlineExpanded: !store.getBool('grafana.dashboard.edit-pane.outline.collapsed', true), + outlineExpanded: !store.getBool('grafana.dashboard.edit-pane.outline.collapsed', false), dashboardUid, }); }; From fbadbf385d597308636e5109a4f01702e196db12 Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Thu, 20 Nov 2025 13:44:15 +0200 Subject: [PATCH 015/378] IAM: Add validation for new tokens with no expiration when token_expiration_day_limit is set (#114166) * do not create sa with no expiration when day limit is set * disable No expiration option if day limit is set * make i18n-extract * run prettier * address feedback --- pkg/services/serviceaccounts/api/token.go | 4 ++ .../serviceaccounts/api/token_test.go | 63 ++++++++++++++++--- .../components/CreateTokenModal.tsx | 51 +++++++++++---- public/locales/en-US/grafana.json | 3 + 4 files changed, 100 insertions(+), 21 deletions(-) diff --git a/pkg/services/serviceaccounts/api/token.go b/pkg/services/serviceaccounts/api/token.go index dcfb82b10f5..620d278ee1e 100644 --- a/pkg/services/serviceaccounts/api/token.go +++ b/pkg/services/serviceaccounts/api/token.go @@ -157,6 +157,10 @@ func (api *ServiceAccountsAPI) CreateToken(c *contextmodel.ReqContext) response. } if api.cfg.SATokenExpirationDayLimit > 0 { + if cmd.SecondsToLive == 0 { + return response.Error(http.StatusBadRequest, "Cannot create token with no expiration date when service_accounts.token_expiration_day_limit is set", nil) + } + dayExpireLimit := time.Now().Add(time.Duration(api.cfg.SATokenExpirationDayLimit) * time.Hour * 24).Truncate(24 * time.Hour) expirationDate := time.Now().Add(time.Duration(cmd.SecondsToLive) * time.Second).Truncate(24 * time.Hour) if expirationDate.After(dayExpireLimit) { diff --git a/pkg/services/serviceaccounts/api/token_test.go b/pkg/services/serviceaccounts/api/token_test.go index 087d786b723..e6218c10638 100644 --- a/pkg/services/serviceaccounts/api/token_test.go +++ b/pkg/services/serviceaccounts/api/token_test.go @@ -60,14 +60,15 @@ func TestServiceAccountsAPI_ListTokens(t *testing.T) { func TestServiceAccountsAPI_CreateToken(t *testing.T) { type TestCase struct { - desc string - id int64 - body string - permissions []accesscontrol.Permission - tokenTTL int64 - expectedErr error - expectedAPIKey *apikey.APIKey - expectedCode int + desc string + id int64 + body string + permissions []accesscontrol.Permission + tokenTTL int64 + tokenExpirationDayLimit int + expectedErr error + expectedAPIKey *apikey.APIKey + expectedCode int } tests := []TestCase{ @@ -105,12 +106,58 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) { permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, expectedCode: http.StatusBadRequest, }, + { + desc: "should not be able to create token for service account if max ttl is configured and exceeds the limit", + id: 1, + body: `{"name": "test", "secondsToLive": 11000}`, + tokenTTL: 10000, + permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, + expectedCode: http.StatusBadRequest, + }, + { + desc: "should be able to create token for service account if max ttl is configured and within the limit", + id: 1, + body: `{"name": "test", "secondsToLive": 5000}`, + tokenTTL: 10000, + permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, + expectedAPIKey: &apikey.APIKey{}, + expectedCode: http.StatusOK, + }, + { + desc: "should not be able to create token for service account if token expiration day limit is configured but not set in body", + id: 1, + body: `{"name": "test"}`, + tokenTTL: -1, + tokenExpirationDayLimit: 30, + permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, + expectedCode: http.StatusBadRequest, + }, + { + desc: "should not be able to create token for service account if token expiration day limit is configured and exceeds the limit", + id: 1, + body: `{"name": "test", "secondsToLive": 340000}`, // 340000 seconds is > 3 days + tokenTTL: -1, + tokenExpirationDayLimit: 3, + permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, + expectedCode: http.StatusBadRequest, + }, + { + desc: "should be able to create token for service account if token expiration day limit is configured and within the limit", + id: 1, + body: `{"name": "test", "secondsToLive": 250000}`, // 250000 seconds is almost 3 days + tokenTTL: -1, + tokenExpirationDayLimit: 3, + permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, + expectedAPIKey: &apikey.APIKey{}, + expectedCode: http.StatusOK, + }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { server := setupTests(t, func(a *ServiceAccountsAPI) { a.cfg.ApiKeyMaxSecondsToLive = tt.tokenTTL + a.cfg.SATokenExpirationDayLimit = tt.tokenExpirationDayLimit a.service = &satests.FakeServiceAccountService{ ExpectedErr: tt.expectedErr, ExpectedAPIKey: tt.expectedAPIKey, diff --git a/public/app/features/serviceaccounts/components/CreateTokenModal.tsx b/public/app/features/serviceaccounts/components/CreateTokenModal.tsx index a0daa852b09..fdad92e254e 100644 --- a/public/app/features/serviceaccounts/components/CreateTokenModal.tsx +++ b/public/app/features/serviceaccounts/components/CreateTokenModal.tsx @@ -16,10 +16,8 @@ import { useStyles2, } from '@grafana/ui'; -const EXPIRATION_OPTIONS = [ - { label: 'No expiration', value: false }, - { label: 'Set expiration date', value: true }, -]; +const NO_EXPIRATION_OPTION = 'no-expiration'; +const CUSTOM_EXPIRATION_OPTION = 'custom-expiration'; export type ServiceAccountToken = { name: string; @@ -44,11 +42,15 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT } else { maxExpirationDate.setDate(8640000000000000); } - const defaultExpirationDate = config.tokenExpirationDayLimit !== undefined && config.tokenExpirationDayLimit > 0; + + const isTokenExpirationDayLimitConfigured = + config.tokenExpirationDayLimit !== undefined && config.tokenExpirationDayLimit > 0; + + const defaultExpirationOption = isTokenExpirationDayLimitConfigured ? CUSTOM_EXPIRATION_OPTION : NO_EXPIRATION_OPTION; const [defaultTokenName, setDefaultTokenName] = useState(''); const [newTokenName, setNewTokenName] = useState(''); - const [isWithExpirationDate, setIsWithExpirationDate] = useState(defaultExpirationDate); + const [expirationOption, setExpirationOption] = useState(defaultExpirationOption); const [newTokenExpirationDate, setNewTokenExpirationDate] = useState(tomorrow); const [isExpirationDateValid, setIsExpirationDateValid] = useState(newTokenExpirationDate !== ''); const styles = useStyles2(getStyles); @@ -69,14 +71,15 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT const onGenerateToken = () => { onCreateToken({ name: newTokenName || defaultTokenName, - secondsToLive: isWithExpirationDate ? getSecondsToLive(newTokenExpirationDate) : undefined, + secondsToLive: + expirationOption === CUSTOM_EXPIRATION_OPTION ? getSecondsToLive(newTokenExpirationDate) : undefined, }); }; const onCloseInternal = () => { setNewTokenName(''); setDefaultTokenName(''); - setIsWithExpirationDate(defaultExpirationDate); + setExpirationOption(defaultExpirationOption); setNewTokenExpirationDate(tomorrow); setIsExpirationDateValid(newTokenExpirationDate !== ''); onClose(); @@ -84,6 +87,24 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT const modalTitle = !token ? 'Add service account token' : 'Service account token created'; + const getExpirationOptions = () => { + const noExpirationDescription = t( + 'serviceaccounts.create-token-modal.description-no-expiration-disabled', + 'Cannot create a token with no expiration date when token expiration day limit is configured' + ); + return [ + { + label: t('serviceaccounts.create-token-modal.label-no-expiration', 'No expiration'), + value: NO_EXPIRATION_OPTION, + description: isTokenExpirationDayLimitConfigured ? noExpirationDescription : undefined, + }, + { + label: t('serviceaccounts.create-token-modal.label-set-expiration-date', 'Set expiration date'), + value: CUSTOM_EXPIRATION_OPTION, + }, + ]; + }; + return ( {!token ? ( @@ -109,13 +130,14 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT - {isWithExpirationDate && ( + {expirationOption === CUSTOM_EXPIRATION_OPTION && ( )} - diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 28512178dd7..d29fe101bd6 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -12543,11 +12543,14 @@ "copy-clipboard": "Copy to clipboard", "copy-to-clipboard-and-close": "Copy to clipboard and close", "description-name-to-easily-identify-the-token": "Name to easily identify the token", + "description-no-expiration-disabled": "Cannot create a token with no expiration date when token expiration day limit is configured", "description-token": "Copy the token now as you will not be able to see it again. Losing a token requires creating a new one.", "generate-token": "Generate token", "label-display-name": "Display name", "label-expiration": "Expiration", "label-expiration-date": "Expiration date", + "label-no-expiration": "No expiration", + "label-set-expiration-date": "Set expiration date", "label-token": "Token" }, "get-actions-cell": { From 7f52586c68355d31a304b466e50d62263265c409 Mon Sep 17 00:00:00 2001 From: Priyansh Gupta <120119805+priyansh3006@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:49:54 +0530 Subject: [PATCH 016/378] Prometheus: Hide 'Kick start your query' button for existing queries (#113980) * Prometheus: Hide 'Kick start your query' button for existing queries * refactor: simplify query check logic in Prometheus editor Remove unnecessary useMemo and buildVisualQueryFromString logic. Just check query.expr directly to determine button visibility. * fix: format code with prettier --- .../components/PromQueryEditorSelector.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx index 8250bd91a68..dc0a5799259 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx @@ -118,16 +118,18 @@ export const PromQueryEditorSelector = memo((props) => { onAddQuery={onAddQuery} /> - + {!query.expr && ( + + )}
Date: Thu, 20 Nov 2025 12:50:38 +0000 Subject: [PATCH 017/378] deps(go): bump github.com/prometheus/common from 0.67.1 to 0.67.2 in /pkg/promlib (#113145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * deps(go): bump github.com/prometheus/common in /pkg/promlib Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.67.1 to 0.67.2. - [Release notes](https://github.com/prometheus/common/releases) - [Changelog](https://github.com/prometheus/common/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/common/compare/v0.67.1...v0.67.2) --- updated-dependencies: - dependency-name: github.com/prometheus/common dependency-version: 0.67.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * deps(go): update github.com/prometheus/common to v0.67.3 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Zoltán Bedi --- apps/advisor/go.mod | 2 +- apps/advisor/go.sum | 4 ++-- apps/alerting/notifications/go.mod | 3 +-- apps/alerting/notifications/go.sum | 4 ++-- apps/alerting/rules/go.mod | 2 +- apps/alerting/rules/go.sum | 4 ++-- apps/annotation/go.mod | 2 +- apps/annotation/go.sum | 4 ++-- apps/collections/go.mod | 2 +- apps/collections/go.sum | 4 ++-- apps/correlations/go.mod | 2 +- apps/correlations/go.sum | 4 ++-- apps/dashboard/go.mod | 2 +- apps/dashboard/go.sum | 4 ++-- apps/example/go.mod | 2 +- apps/example/go.sum | 4 ++-- apps/folder/go.mod | 2 +- apps/folder/go.sum | 4 ++-- apps/investigations/go.mod | 2 +- apps/investigations/go.sum | 4 ++-- apps/logsdrilldown/go.mod | 2 +- apps/logsdrilldown/go.sum | 4 ++-- apps/playlist/go.mod | 2 +- apps/playlist/go.sum | 4 ++-- apps/plugins/go.mod | 2 +- apps/plugins/go.sum | 4 ++-- apps/preferences/go.mod | 2 +- apps/preferences/go.sum | 4 ++-- apps/provisioning/go.mod | 2 +- apps/provisioning/go.sum | 4 ++-- apps/secret/go.mod | 2 +- apps/secret/go.sum | 4 ++-- apps/shorturl/go.mod | 2 +- apps/shorturl/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- pkg/aggregator/go.mod | 3 +-- pkg/aggregator/go.sum | 4 ++-- pkg/apiserver/go.mod | 3 +-- pkg/apiserver/go.sum | 4 ++-- pkg/promlib/go.mod | 2 +- pkg/promlib/go.sum | 4 ++-- 42 files changed, 63 insertions(+), 66 deletions(-) diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 1cffeb06464..9a3fabfd18c 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -232,7 +232,7 @@ require ( github.com/prometheus/alertmanager v0.28.0 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.14.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index c681c3c257d..ef7adf779d1 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -1046,8 +1046,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg= diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 521bb0b586f..c29ea0b00a8 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -32,7 +32,6 @@ require ( github.com/go-openapi/swag/jsonname v0.25.1 // indirect github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -60,7 +59,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 7a53f8de14c..b67c48676b8 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -140,8 +140,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= diff --git a/apps/alerting/rules/go.mod b/apps/alerting/rules/go.mod index c262035d973..17aa567f077 100644 --- a/apps/alerting/rules/go.mod +++ b/apps/alerting/rules/go.mod @@ -5,7 +5,7 @@ go 1.25.3 require ( github.com/grafana/grafana-app-sdk v0.48.2 github.com/grafana/grafana-app-sdk/logging v0.48.1 - github.com/prometheus/common v0.67.2 + github.com/prometheus/common v0.67.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) diff --git a/apps/alerting/rules/go.sum b/apps/alerting/rules/go.sum index 26bd55d3b5d..7eadc30e628 100644 --- a/apps/alerting/rules/go.sum +++ b/apps/alerting/rules/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/annotation/go.mod b/apps/annotation/go.mod index 19ac2c8b88a..988575c7ce1 100644 --- a/apps/annotation/go.mod +++ b/apps/annotation/go.mod @@ -49,7 +49,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/apps/annotation/go.sum b/apps/annotation/go.sum index 26bd55d3b5d..7eadc30e628 100644 --- a/apps/annotation/go.sum +++ b/apps/annotation/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/collections/go.mod b/apps/collections/go.mod index a3b927ec893..281a6579c67 100644 --- a/apps/collections/go.mod +++ b/apps/collections/go.mod @@ -41,7 +41,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect diff --git a/apps/collections/go.sum b/apps/collections/go.sum index 02827e1769d..8d576bfaa8b 100644 --- a/apps/collections/go.sum +++ b/apps/collections/go.sum @@ -85,8 +85,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/apps/correlations/go.mod b/apps/correlations/go.mod index 898b631516d..988f18b89d6 100644 --- a/apps/correlations/go.mod +++ b/apps/correlations/go.mod @@ -49,7 +49,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/apps/correlations/go.sum b/apps/correlations/go.sum index 26bd55d3b5d..7eadc30e628 100644 --- a/apps/correlations/go.sum +++ b/apps/correlations/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 5ed404c7c5d..9e98fe233ec 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -84,7 +84,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.10 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 8d7185e4d78..b4158332679 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -202,8 +202,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d h1:HWfigq7lB31IeJL8iy7jkUmU/PG1Sr8jVGhS749dbUA= diff --git a/apps/example/go.mod b/apps/example/go.mod index a0d1dc37319..acb714107b3 100644 --- a/apps/example/go.mod +++ b/apps/example/go.mod @@ -56,7 +56,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/spf13/pflag v1.0.10 // indirect diff --git a/apps/example/go.sum b/apps/example/go.sum index 5a7f0692b24..00f1a6a2620 100644 --- a/apps/example/go.sum +++ b/apps/example/go.sum @@ -116,8 +116,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/folder/go.mod b/apps/folder/go.mod index 659e4e8a019..1c9249745a9 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -40,7 +40,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect diff --git a/apps/folder/go.sum b/apps/folder/go.sum index 56c5afd751f..944e0d69721 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -85,8 +85,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 51e8ea0c47b..d4f3685a2aa 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -50,7 +50,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 26bd55d3b5d..7eadc30e628 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/logsdrilldown/go.mod b/apps/logsdrilldown/go.mod index 2056bb1f8f5..712d99405d0 100644 --- a/apps/logsdrilldown/go.mod +++ b/apps/logsdrilldown/go.mod @@ -51,7 +51,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/apps/logsdrilldown/go.sum b/apps/logsdrilldown/go.sum index 26bd55d3b5d..7eadc30e628 100644 --- a/apps/logsdrilldown/go.sum +++ b/apps/logsdrilldown/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 246be5289c2..8a254e980d1 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -51,7 +51,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 26bd55d3b5d..7eadc30e628 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index 9a73a11cc62..b24c9260327 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -58,7 +58,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/spf13/pflag v1.0.10 // indirect diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index 55fa4dc09fc..b769fbe3916 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -116,8 +116,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/preferences/go.mod b/apps/preferences/go.mod index 05292d2cdd5..cd702672de6 100644 --- a/apps/preferences/go.mod +++ b/apps/preferences/go.mod @@ -40,7 +40,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect diff --git a/apps/preferences/go.sum b/apps/preferences/go.sum index 02827e1769d..8d576bfaa8b 100644 --- a/apps/preferences/go.sum +++ b/apps/preferences/go.sum @@ -85,8 +85,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index 06fa5976b01..9bb12d5ae4f 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -57,7 +57,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index 9a2b854aa09..78f06781b15 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -113,8 +113,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/apps/secret/go.mod b/apps/secret/go.mod index 09d3e5545f3..2ac03ea3f8b 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -45,7 +45,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect diff --git a/apps/secret/go.sum b/apps/secret/go.sum index 29563e7984d..74024f8df34 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -89,8 +89,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index 6071ce04924..382cf8dd051 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -56,7 +56,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/spf13/pflag v1.0.10 // indirect diff --git a/apps/shorturl/go.sum b/apps/shorturl/go.sum index f10e228919a..e31288da121 100644 --- a/apps/shorturl/go.sum +++ b/apps/shorturl/go.sum @@ -116,8 +116,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/go.mod b/go.mod index 9e558b61411..23756c59ae0 100644 --- a/go.mod +++ b/go.mod @@ -157,7 +157,7 @@ require ( github.com/prometheus/alertmanager v0.28.0 // @grafana/alerting-backend github.com/prometheus/client_golang v1.23.2 // @grafana/alerting-backend github.com/prometheus/client_model v0.6.2 // @grafana/grafana-backend-group - github.com/prometheus/common v0.67.2 // @grafana/alerting-backend + github.com/prometheus/common v0.67.3 // @grafana/alerting-backend github.com/prometheus/prometheus v0.303.1 // @grafana/alerting-backend github.com/prometheus/sigv4 v0.1.2 // @grafana/alerting-backend github.com/puzpuzpuz/xsync/v4 v4.2.0 // @grafana/grafana-backend-group diff --git a/go.sum b/go.sum index 4cc52d323c4..61068fdef0b 100644 --- a/go.sum +++ b/go.sum @@ -2295,8 +2295,8 @@ github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGy github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index c1004c6a52f..e822e4b6a62 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -47,7 +47,6 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.26.1 // indirect @@ -90,7 +89,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/cobra v1.10.1 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 9be90d153dc..8ab04c12ddf 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -208,8 +208,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 8e2ae9ae59b..6947fb20b67 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -39,7 +39,6 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag/jsonname v0.25.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect @@ -64,7 +63,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 7a0b4d77cd7..49b4ab91e93 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -129,8 +129,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 27e09f912a4..c60a20cee8d 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -8,7 +8,7 @@ require ( github.com/grafana/grafana/apps/scope v0.0.0-20251007093103-792853df9134 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_golang v1.23.2 - github.com/prometheus/common v0.67.2 + github.com/prometheus/common v0.67.3 github.com/prometheus/prometheus v0.303.1 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.38.0 diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index f32307ff8b3..f8c29e54262 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -239,8 +239,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/prometheus v0.303.1 h1:He/2jRE6sB23Ew38AIoR1WRR3fCMgPlJA2E0obD2WSY= From 24d970e4b968c4a62da785c49141dd67a63537ff Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 20 Nov 2025 13:58:13 +0100 Subject: [PATCH 018/378] Alerting: Remove stale filtering code (#114231) Remove the old filters file, update import paths --- .../alerting/unified/rule-list/FilterView.tsx | 2 +- .../rule-list/PaginatedDataSourceLoader.tsx | 2 +- .../rule-list/PaginatedGrafanaLoader.tsx | 2 +- .../unified/rule-list/hooks/filters.ts | 377 ------------------ .../hooks/useFilteredRulesIterator.test.ts | 85 ---- .../hooks/useFilteredRulesIterator.ts | 26 +- 6 files changed, 5 insertions(+), 489 deletions(-) delete mode 100644 public/app/features/alerting/unified/rule-list/hooks/filters.ts delete mode 100644 public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.test.ts diff --git a/public/app/features/alerting/unified/rule-list/FilterView.tsx b/public/app/features/alerting/unified/rule-list/FilterView.tsx index 8ac1e1a8c58..fb46822f0c3 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.tsx @@ -16,11 +16,11 @@ import { GrafanaRuleListItem } from './GrafanaRuleListItem'; import LoadMoreHelper from './LoadMoreHelper'; import { UnknownRuleListItem } from './components/AlertRuleListItem'; import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader'; +import { hasClientSideFilters } from './hooks/grafanaFilter'; import { GrafanaRuleWithOrigin, PromRuleWithOrigin, RuleWithOrigin, - hasClientSideFilters, useFilteredRulesIteratorProvider, } from './hooks/useFilteredRulesIterator'; import { FRONTEND_LIST_PAGE_SIZE, getSearchApiGroupPageSize } from './paginationLimits'; diff --git a/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx index 2e2c2fda607..546d500ace2 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx @@ -14,7 +14,7 @@ import { ListGroup } from './components/ListGroup'; import { ListSection } from './components/ListSection'; import { LoadMoreButton } from './components/LoadMoreButton'; import { NoRulesFound } from './components/NoRulesFound'; -import { getDatasourceFilter } from './hooks/filters'; +import { getDatasourceFilter } from './hooks/datasourceFilter'; import { toIndividualRuleGroups, usePrometheusGroupsGenerator } from './hooks/prometheusGroupsGenerator'; import { useLazyLoadPrometheusGroups } from './hooks/useLazyLoadPrometheusGroups'; import { FRONTED_GROUPED_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits'; diff --git a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx index d1835d29757..a39509608b3 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx @@ -18,7 +18,7 @@ import { ListGroup } from './components/ListGroup'; import { ListSection } from './components/ListSection'; import { LoadMoreButton } from './components/LoadMoreButton'; import { NoRulesFound } from './components/NoRulesFound'; -import { getGrafanaFilter } from './hooks/filters'; +import { getGrafanaFilter } from './hooks/grafanaFilter'; import { toIndividualRuleGroups, useGrafanaGroupsGenerator } from './hooks/prometheusGroupsGenerator'; import { useLazyLoadPrometheusGroups } from './hooks/useLazyLoadPrometheusGroups'; import { FRONTED_GROUPED_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits'; diff --git a/public/app/features/alerting/unified/rule-list/hooks/filters.ts b/public/app/features/alerting/unified/rule-list/hooks/filters.ts deleted file mode 100644 index 6ac4d668e9d..00000000000 --- a/public/app/features/alerting/unified/rule-list/hooks/filters.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { attempt, compact, isString } from 'lodash'; -import memoize from 'micro-memoize'; - -import { Matcher } from 'app/plugins/datasource/alertmanager/types'; -import { PromRuleDTO, PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; - -import { GrafanaPromRulesOptions } from '../../api/prometheusApi'; -import { shouldUseBackendFilters } from '../../featureToggles'; -import { RulesFilter } from '../../search/rulesSearchParser'; -import { labelsMatchMatchers } from '../../utils/alertmanager'; -import { Annotation } from '../../utils/constants'; -import { getDatasourceAPIUid } from '../../utils/datasource'; -import { fuzzyMatches } from '../../utils/fuzzySearch'; -import { parseMatcher } from '../../utils/matchers'; -import { isPluginProvidedRule, prometheusRuleType } from '../../utils/rules'; -import { normalizeHealth } from '../components/util'; - -type RuleFilterHandler = (rule: PromRuleDTO, filterState: RulesFilter) => boolean; -type GroupFilterHandler = ( - group: PromRuleGroupDTO, - filterState: Pick -) => boolean; - -type RuleFilterConfig = Record< - Exclude, - RuleFilterHandler | null ->; - -type GroupFilterConfig = Record, GroupFilterHandler | null>; - -export function getGrafanaFilter(filterState: RulesFilter) { - const normalizedFilterState = normalizeFilterState(filterState); - const useBackendFilters = shouldUseBackendFilters(); - - // Build title search for backend filtering - const titleSearch = buildTitleSearch(normalizedFilterState); - - const backendFilter: GrafanaPromRulesOptions = { - state: normalizedFilterState.ruleState ? [normalizedFilterState.ruleState] : [], - health: normalizedFilterState.ruleHealth ? [normalizedFilterState.ruleHealth] : [], - contactPoint: normalizedFilterState.contactPoint ?? undefined, - title: useBackendFilters ? titleSearch : undefined, - type: useBackendFilters ? normalizedFilterState.ruleType : undefined, - dashboardUid: useBackendFilters ? normalizedFilterState.dashboardUid : undefined, - searchGroupName: useBackendFilters ? normalizedFilterState.groupName : undefined, - }; - - const grafanaFilterProcessingConfig: RuleFilterConfig = { - // When backend filtering is enabled, these filters are handled by the backend - freeFormWords: useBackendFilters ? null : freeFormFilter, - ruleName: useBackendFilters ? null : ruleNameFilter, - ruleState: null, - ruleType: useBackendFilters ? null : ruleTypeFilter, - dataSourceNames: dataSourceNamesFilter, - labels: labelsFilter, - ruleHealth: null, - dashboardUid: useBackendFilters ? null : dashboardUidFilter, - plugins: pluginsFilter, - contactPoint: null, - }; - - const grafanaGroupFilterConfig: GroupFilterConfig = { - namespace: namespaceFilter, - groupName: useBackendFilters ? null : groupNameFilter, - }; - - return { - backendFilter, - frontendFilter: { - groupMatches: (group: PromRuleGroupDTO) => groupMatches(group, normalizedFilterState, grafanaGroupFilterConfig), - ruleMatches: (rule: PromRuleDTO) => ruleMatches(rule, normalizedFilterState, grafanaFilterProcessingConfig), - }, - }; -} - -export function getDatasourceFilter(filterState: RulesFilter) { - const normalizedFilterState = normalizeFilterState(filterState); - - const dsRuleFilterConfig: RuleFilterConfig = { - freeFormWords: freeFormFilter, - ruleName: ruleNameFilter, - ruleState: ruleStateFilter, - ruleType: ruleTypeFilter, - dataSourceNames: dataSourceNamesFilter, - labels: labelsFilter, - ruleHealth: ruleHealthFilter, - dashboardUid: dashboardUidFilter, - plugins: pluginsFilter, - contactPoint: contactPointFilter, - }; - - const dsGroupFilterConfig: GroupFilterConfig = { - namespace: namespaceFilter, - groupName: groupNameFilter, - }; - - return { - groupMatches: (group: PromRuleGroupDTO) => groupMatches(group, normalizedFilterState, dsGroupFilterConfig), - ruleMatches: (rule: PromRuleDTO) => ruleMatches(rule, normalizedFilterState, dsRuleFilterConfig), - }; -} - -/** - * @returns True if the group matches the filter, false otherwise. Keeps rules intact - */ -function groupMatches( - group: PromRuleGroupDTO, - filterState: Pick, - filterConfig: GroupFilterConfig -): boolean { - if (filterConfig.namespace && filterConfig.namespace(group, filterState) === false) { - return false; - } - - if (filterConfig.groupName && filterConfig.groupName(group, filterState) === false) { - return false; - } - - return true; -} - -function namespaceFilter(group: PromRuleGroupDTO, filterState: Pick): boolean { - if (filterState.namespace && !fuzzyMatches(group.file, filterState.namespace)) { - return false; - } - - return true; -} - -function groupNameFilter(group: PromRuleGroupDTO, filterState: Pick): boolean { - if (filterState.groupName && !fuzzyMatches(group.name, filterState.groupName)) { - return false; - } - - return true; -} - -/** - * @returns True if the rule matches the filter, false otherwise - */ -function ruleMatches(rule: PromRuleDTO, filterState: RulesFilter, filterConfig: RuleFilterConfig) { - if (filterConfig.freeFormWords && filterConfig.freeFormWords(rule, filterState) === false) { - return false; - } - - if (filterConfig.ruleName && filterConfig.ruleName(rule, filterState) === false) { - return false; - } - - if (filterConfig.labels && filterConfig.labels(rule, filterState) === false) { - return false; - } - - if (filterConfig.ruleType && filterConfig.ruleType(rule, filterState) === false) { - return false; - } - - if (filterConfig.ruleState && filterConfig.ruleState(rule, filterState) === false) { - return false; - } - - if (filterConfig.ruleHealth && filterConfig.ruleHealth(rule, filterState) === false) { - return false; - } - - if (filterConfig.contactPoint && filterConfig.contactPoint(rule, filterState) === false) { - return false; - } - - if (filterConfig.dashboardUid && filterConfig.dashboardUid(rule, filterState) === false) { - return false; - } - - if (filterConfig.plugins && filterConfig.plugins(rule, filterState) === false) { - return false; - } - - if (filterConfig.dataSourceNames && filterConfig.dataSourceNames(rule, filterState) === false) { - return false; - } - - return true; -} - -function freeFormFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean { - if (filterState.freeFormWords.length > 0) { - const nameMatches = fuzzyMatches(rule.name, filterState.freeFormWords.join(' ')); - if (!nameMatches) { - return false; - } - } - - return true; -} - -function ruleNameFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean { - if (filterState.ruleName && !fuzzyMatches(rule.name, filterState.ruleName)) { - return false; - } - - return true; -} - -function labelsFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean { - if (filterState.labels.length > 0) { - const matchers = compact(filterState.labels.map(looseParseMatcher)); - const doRuleLabelsMatchQuery = matchers.length > 0 && labelsMatchMatchers(rule.labels || {}, matchers); - - // Also check alerts if they exist - const doAlertsContainMatchingLabels = - matchers.length > 0 && - prometheusRuleType.alertingRule(rule) && - rule.alerts && - rule.alerts.some((alert) => labelsMatchMatchers(alert.labels || {}, matchers)); - - if (!doRuleLabelsMatchQuery && !doAlertsContainMatchingLabels) { - return false; - } - } - - return true; -} - -function ruleTypeFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean { - if (filterState.ruleType && rule.type !== filterState.ruleType) { - return false; - } - - return true; -} - -function ruleStateFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean { - if (filterState.ruleState) { - if (!prometheusRuleType.alertingRule(rule)) { - return false; - } - if (rule.state !== filterState.ruleState) { - return false; - } - } - - return true; -} - -function ruleHealthFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean { - if (filterState.ruleHealth && normalizeHealth(rule.health) !== filterState.ruleHealth) { - return false; - } - - return true; -} - -function contactPointFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean { - if (filterState.contactPoint) { - if (!prometheusRuleType.grafana.alertingRule(rule)) { - return false; - } - - if (!rule.notificationSettings) { - return false; - } - - if (filterState.contactPoint !== rule.notificationSettings.receiver) { - return false; - } - } - - return true; -} - -function dashboardUidFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean { - if (filterState.dashboardUid) { - if (!prometheusRuleType.alertingRule(rule)) { - return false; - } - - const dashboardAnnotation = rule.annotations?.[Annotation.dashboardUID]; - if (dashboardAnnotation !== filterState.dashboardUid) { - return false; - } - } - - return true; -} - -function pluginsFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean { - // Plugins filter - hide plugin-provided rules when set to 'hide' - if (filterState.plugins === 'hide' && isPluginProvidedRule(rule)) { - return false; - } - - return true; -} - -function dataSourceNamesFilter(rule: PromRuleDTO, filterState: RulesFilter): boolean { - // Note: We can't implement these filters from reduceGroups because they rely on rulerRule property - // which is not available in PromRuleDTO: - // - contactPoint filter - // - dataSourceNames filter - if (filterState.dataSourceNames.length > 0) { - const isGrafanaRule = prometheusRuleType.grafana.rule(rule); - if (isGrafanaRule) { - try { - const filterDatasourceUids = mapDataSourceNamesToUids(filterState.dataSourceNames); - const queriedDatasourceUids = rule.queriedDatasourceUIDs || []; - - const queryIncludesDataSource = queriedDatasourceUids.some((uid) => filterDatasourceUids.includes(uid)); - if (!queryIncludesDataSource) { - return false; - } - } catch (error) { - return false; - } - } - } - - return true; -} - -function looseParseMatcher(matcherQuery: string): Matcher | undefined { - try { - return parseMatcher(matcherQuery); - } catch { - // Try to createa a matcher than matches all values for a given key - return { name: matcherQuery, value: '', isRegex: true, isEqual: true }; - } -} - -// Memoize the function to avoid calling getDatasourceAPIUid for the filter values multiple times -const mapDataSourceNamesToUids = memoize( - (names: string[]): string[] => { - return names.map((name) => attempt(getDatasourceAPIUid, name)).filter(isString); - }, - { maxSize: 1 } -); - -/** - * Build title search parameter for backend filtering - * Combines ruleName and freeFormWords into a single search string - */ -export function buildTitleSearch(filterState: RulesFilter): string | undefined { - const titleParts: string[] = []; - - const ruleName = filterState.ruleName?.trim(); - if (ruleName) { - titleParts.push(ruleName); - } - - const freeFormSegment = filterState.freeFormWords - .map((word) => word.trim()) - .filter(Boolean) - .join(' '); - - if (freeFormSegment) { - titleParts.push(freeFormSegment); - } - - if (titleParts.length === 0) { - return undefined; - } - - return titleParts.join(' '); -} - -/** - * Normalize filter state for case-insensitive matching - * Lowercase free form words, rule name, group name and namespace - */ -function normalizeFilterState(filterState: RulesFilter): RulesFilter { - return { - ...filterState, - freeFormWords: filterState.freeFormWords.map((word) => word.toLowerCase()), - ruleName: filterState.ruleName?.toLowerCase(), - groupName: filterState.groupName?.toLowerCase(), - namespace: filterState.namespace?.toLowerCase(), - }; -} diff --git a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.test.ts b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.test.ts deleted file mode 100644 index 1b226d50c5b..00000000000 --- a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { config } from '@grafana/runtime'; -import { PromRuleType } from 'app/types/unified-alerting-dto'; - -import { RuleSource } from '../../search/rulesSearchParser'; -import { getFilter } from '../../utils/search'; - -import { hasClientSideFilters } from './useFilteredRulesIterator'; - -describe('hasClientSideFilters', () => { - const originalFeatureToggles = config.featureToggles; - - beforeEach(() => { - config.featureToggles = { ...originalFeatureToggles }; - }); - - afterEach(() => { - config.featureToggles = originalFeatureToggles; - }); - - describe('when alertingUIUseBackendFilters is enabled', () => { - beforeEach(() => { - config.featureToggles.alertingUIUseBackendFilters = true; - }); - - it('should return false for backend-supported filters', () => { - expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleName: 'test' }))).toBe(false); - expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); - expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); - expect(hasClientSideFilters(getFilter({ groupName: 'my-group' }))).toBe(false); - }); - - it('should return true for client-side only filters', () => { - expect(hasClientSideFilters(getFilter({ namespace: 'test' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ ruleSource: RuleSource.DataSource }))).toBe(true); - }); - - it('should return false when no filters are applied', () => { - expect(hasClientSideFilters(getFilter({}))).toBe(false); - }); - }); - - describe('when alertingUIUseBackendFilters is disabled', () => { - beforeEach(() => { - config.featureToggles.alertingUIUseBackendFilters = false; - }); - - it('should return true for backend-supported filters when backend filtering is disabled', () => { - expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ ruleName: 'test' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ groupName: 'my-group' }))).toBe(true); - }); - - it('should return true for client-side only filters', () => { - expect(hasClientSideFilters(getFilter({ namespace: 'test' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ ruleSource: RuleSource.DataSource }))).toBe(true); - }); - - it('should return false when no filters are applied', () => { - expect(hasClientSideFilters(getFilter({}))).toBe(false); - }); - }); - - describe('when alertingUIUseBackendFilters is undefined (default)', () => { - beforeEach(() => { - config.featureToggles.alertingUIUseBackendFilters = undefined; - }); - - it('should default to client-side filtering for backward compatibility', () => { - // Default behavior should be client-side filtering - expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true); - expect(hasClientSideFilters(getFilter({ ruleName: 'test' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(true); - expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(true); - expect(hasClientSideFilters(getFilter({ groupName: 'my-group' }))).toBe(true); - }); - }); -}); diff --git a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts index 86f593661ea..407426bb3e1 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts @@ -15,7 +15,6 @@ import { PromRuleGroupDTO, } from 'app/types/unified-alerting-dto'; -import { shouldUseBackendFilters } from '../../featureToggles'; import { RuleSource, RulesFilter } from '../../search/rulesSearchParser'; import { getDataSourceByUid, @@ -25,7 +24,8 @@ import { } from '../../utils/datasource'; import { RulePositionHash, createRulePositionHash } from '../rulePositionHash'; -import { getDatasourceFilter, getGrafanaFilter } from './filters'; +import { getDatasourceFilter } from './datasourceFilter'; +import { getGrafanaFilter } from './grafanaFilter'; import { useGrafanaGroupsGenerator, usePrometheusGroupsGenerator } from './prometheusGroupsGenerator'; export type RuleWithOrigin = PromRuleWithOrigin | GrafanaRuleWithOrigin; @@ -146,28 +146,6 @@ export function useFilteredRulesIteratorProvider() { return getFilteredRulesIterable; } -/** - * Determines if client-side filtering is needed for Grafana-managed rules. - */ -export function hasClientSideFilters(filterState: RulesFilter): boolean { - const useBackendFilters = shouldUseBackendFilters(); - - return ( - // When backend filters are disabled, title search, type filter, dashboard filter, and group name filter need client-side filtering - (!useBackendFilters && - (filterState.freeFormWords.length > 0 || - Boolean(filterState.ruleName) || - Boolean(filterState.ruleType) || - Boolean(filterState.dashboardUid) || - Boolean(filterState.groupName))) || - // Client-side only filters: - Boolean(filterState.namespace) || - filterState.dataSourceNames.length > 0 || - filterState.labels.length > 0 || - filterState.ruleSource === RuleSource.DataSource - ); -} - function mergeIterables(iterables: Array>): AsyncIterableX { if (iterables.length === 0) { return empty(); From f6a1d4a14ecc08c6cbad26c528f2c496e4d1010b Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 20 Nov 2025 14:02:15 +0100 Subject: [PATCH 019/378] Revert Auth: Add None role to Viewer case (#114224) --- pkg/services/apiserver/auth/authorizer/role.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/services/apiserver/auth/authorizer/role.go b/pkg/services/apiserver/auth/authorizer/role.go index 6bc6e9d5474..82e80b38164 100644 --- a/pkg/services/apiserver/auth/authorizer/role.go +++ b/pkg/services/apiserver/auth/authorizer/role.go @@ -4,9 +4,10 @@ import ( "context" "fmt" + "k8s.io/apiserver/pkg/authorization/authorizer" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/org" - "k8s.io/apiserver/pkg/authorization/authorizer" ) var _ authorizer.Authorizer = &roleAuthorizer{} @@ -34,13 +35,15 @@ func (auth roleAuthorizer) Authorize(ctx context.Context, a authorizer.Attribute default: return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } - case org.RoleViewer, org.RoleNone: + case org.RoleViewer: switch a.GetVerb() { case "get", "list", "watch": return authorizer.DecisionAllow, "", nil default: return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } + case org.RoleNone: + return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } return authorizer.DecisionDeny, "", nil } From c9801fc66ec0dff17228da702c7c545cfcb44b16 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 20 Nov 2025 16:33:44 +0300 Subject: [PATCH 020/378] FeatureFlags: Remove unused settings for admin UI (#114137) --- .../dashboardversion/dashverimpl/dashver.go | 4 +- .../featuremgmt/feature_toggle_api/types.go | 6 - pkg/services/featuremgmt/models.go | 4 - pkg/services/featuremgmt/openfeature_test.go | 10 +- pkg/services/featuremgmt/registry.go | 1168 ++++++++--------- pkg/services/featuremgmt/toggles-gitlog.csv | 152 ++- pkg/services/featuremgmt/toggles_gen.csv | 14 +- pkg/services/featuremgmt/toggles_gen.go | 304 ----- pkg/services/featuremgmt/toggles_gen.json | 186 +-- pkg/services/featuremgmt/toggles_gen_test.go | 26 +- 10 files changed, 703 insertions(+), 1171 deletions(-) diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index ef39b815fad..2a4d31f2b4d 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -170,8 +170,8 @@ func (s *Service) RestoreVersion(ctx context.Context, cmd *dashver.RestoreVersio cmd.DashboardUID = u } //nolint:staticcheck // not yet migrated to OpenFeature - if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesDashboards) || - s.features.IsEnabledGlobally(featuremgmt.FlagDashboardNewLayouts) { + if s.features.IsEnabled(ctx, featuremgmt.FlagKubernetesDashboards) || + s.features.IsEnabled(ctx, featuremgmt.FlagDashboardNewLayouts) { s.log.Debug("restoring dashboard version through k8s") res, err := s.restoreVersionThroughK8s(ctx, cmd) if err != nil { diff --git a/pkg/services/featuremgmt/feature_toggle_api/types.go b/pkg/services/featuremgmt/feature_toggle_api/types.go index 6b62447fa33..046f59c28ab 100644 --- a/pkg/services/featuremgmt/feature_toggle_api/types.go +++ b/pkg/services/featuremgmt/feature_toggle_api/types.go @@ -44,12 +44,6 @@ type FeatureSpec struct { // The flag is used at startup, so any change requires a restart RequiresRestart bool `json:"requiresRestart,omitempty"` - // Allow cloud users to set the values in UI - AllowSelfServe bool `json:"allowSelfServe,omitempty"` - - // Do not show the value in the UI - HideFromAdminPage bool `json:"hideFromAdminPage,omitempty"` - // Do not show the value in docs HideFromDocs bool `json:"hideFromDocs,omitempty"` diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go index 557ca7ef8a2..45d3dc810f4 100644 --- a/pkg/services/featuremgmt/models.go +++ b/pkg/services/featuremgmt/models.go @@ -129,10 +129,6 @@ type FeatureFlag struct { Stage FeatureFlagStage `json:"stage,omitempty"` Owner codeowner `json:"-"` // Owner person or team that owns this feature flag - // Recommended properties - control behavior of the feature toggle management page in the UI - AllowSelfServe bool `json:"allowSelfServe,omitempty"` // allow users with the right privileges to toggle this from the UI (GeneralAvailability, PublicPreview, and Deprecated toggles only) - HideFromAdminPage bool `json:"hideFromAdminPage,omitempty"` // GA, Deprecated, and PublicPreview toggles only: don't display this feature in the UI; if this is a GA toggle, add a comment with the reasoning - // CEL-GO expression. Using the value "true" will mean this is on by default Expression string `json:"expression,omitempty"` diff --git a/pkg/services/featuremgmt/openfeature_test.go b/pkg/services/featuremgmt/openfeature_test.go index 2cb5c3542d0..788b53531d4 100644 --- a/pkg/services/featuremgmt/openfeature_test.go +++ b/pkg/services/featuremgmt/openfeature_test.go @@ -6,16 +6,16 @@ import ( "net/url" "testing" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/clientauth/middleware" - "github.com/grafana/grafana/pkg/setting" - - authlib "github.com/grafana/authlib/authn" gofeatureflag "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg" "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + authlib "github.com/grafana/authlib/authn" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/clientauth/middleware" + "github.com/grafana/grafana/pkg/setting" ) func TestCreateProvider(t *testing.T) { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index bc4d4c282ee..e6587428f18 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -18,28 +18,24 @@ var ( // Register each toggle here standardFeatureFlags = []FeatureFlag{ { - Name: "disableEnvelopeEncryption", - Description: "Disable envelope encryption (emergency only)", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaOperatorExperienceSquad, - HideFromAdminPage: true, - AllowSelfServe: false, - Expression: "false", + Name: "disableEnvelopeEncryption", + Description: "Disable envelope encryption (emergency only)", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaOperatorExperienceSquad, + Expression: "false", }, { - Name: "panelTitleSearch", - Description: "Search for dashboards using panel title", - Stage: FeatureStagePublicPreview, - Owner: grafanaSearchAndStorageSquad, - HideFromAdminPage: true, + Name: "panelTitleSearch", + Description: "Search for dashboards using panel title", + Stage: FeatureStagePublicPreview, + Owner: grafanaSearchAndStorageSquad, }, { - Name: "publicDashboardsEmailSharing", - Description: "Enables public dashboard sharing to be restricted to only allowed emails", - Stage: FeatureStagePublicPreview, - Owner: grafanaOperatorExperienceSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "publicDashboardsEmailSharing", + Description: "Enables public dashboard sharing to be restricted to only allowed emails", + Stage: FeatureStagePublicPreview, + Owner: grafanaOperatorExperienceSquad, + HideFromDocs: true, }, { Name: "publicDashboardsScene", @@ -56,12 +52,11 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "featureHighlights", - Description: "Highlight Grafana Enterprise features", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaOperatorExperienceSquad, - AllowSelfServe: true, - Expression: "false", + Name: "featureHighlights", + Description: "Highlight Grafana Enterprise features", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaOperatorExperienceSquad, + Expression: "false", }, { Name: "storage", @@ -70,12 +65,11 @@ var ( Owner: grafanaSearchAndStorageSquad, }, { - Name: "canvasPanelNesting", - Description: "Allow elements nesting", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaDatavizSquad, - HideFromAdminPage: true, + Name: "canvasPanelNesting", + Description: "Allow elements nesting", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDatavizSquad, }, { Name: "logRequestsInstrumentedAsUnknown", @@ -84,19 +78,17 @@ var ( Owner: grafanaBackendGroup, }, { - Name: "grpcServer", - Description: "Run the GRPC server", - Stage: FeatureStagePublicPreview, - Owner: grafanaSearchAndStorageSquad, - HideFromAdminPage: true, + Name: "grpcServer", + Description: "Run the GRPC server", + Stage: FeatureStagePublicPreview, + Owner: grafanaSearchAndStorageSquad, }, { - Name: "cloudWatchCrossAccountQuerying", - Description: "Enables cross-account querying in CloudWatch datasources", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - Owner: awsDatasourcesSquad, - AllowSelfServe: true, + Name: "cloudWatchCrossAccountQuerying", + Description: "Enables cross-account querying in CloudWatch datasources", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default + Owner: awsDatasourcesSquad, }, { Name: "showDashboardValidationWarnings", @@ -117,13 +109,12 @@ var ( Owner: grafanaAlertingSquad, }, { - Name: "logsContextDatasourceUi", - Description: "Allow datasource to provide custom UI for context view", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaObservabilityLogsSquad, - Expression: "true", // turned on by default - AllowSelfServe: true, + Name: "logsContextDatasourceUi", + Description: "Allow datasource to provide custom UI for context view", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Owner: grafanaObservabilityLogsSquad, + Expression: "true", // turned on by default }, { Name: "lokiShardSplitting", @@ -133,13 +124,12 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "lokiQuerySplitting", - Description: "Split large interval queries into subqueries with smaller time intervals", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaObservabilityLogsSquad, - Expression: "true", // turned on by default - AllowSelfServe: true, + Name: "lokiQuerySplitting", + Description: "Split large interval queries into subqueries with smaller time intervals", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Owner: grafanaObservabilityLogsSquad, + Expression: "true", // turned on by default }, { Name: "individualCookiePreferences", @@ -148,22 +138,20 @@ var ( Owner: grafanaBackendGroup, }, { - Name: "influxdbBackendMigration", - Description: "Query InfluxDB InfluxQL without the proxy", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: grafanaPartnerPluginsSquad, - Expression: "true", // enabled by default - AllowSelfServe: false, + Name: "influxdbBackendMigration", + Description: "Query InfluxDB InfluxQL without the proxy", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Owner: grafanaPartnerPluginsSquad, + Expression: "true", // enabled by default }, { - Name: "starsFromAPIServer", - Description: "populate star status from apiserver", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaFrontendSearchNavOrganise, - AllowSelfServe: false, - HideFromDocs: true, + Name: "starsFromAPIServer", + Description: "populate star status from apiserver", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaFrontendSearchNavOrganise, + HideFromDocs: true, }, { Name: "kubernetesStars", @@ -198,26 +186,23 @@ var ( Owner: grafanaDatasourcesCoreServicesSquad, }, { - Name: "unifiedRequestLog", - Description: "Writes error logs to the request logger", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaBackendGroup, - Expression: "true", - HideFromAdminPage: true, + Name: "unifiedRequestLog", + Description: "Writes error logs to the request logger", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaBackendGroup, + Expression: "true", }, { - Name: "renderAuthJWT", - Description: "Uses JWT-based auth for rendering instead of relying on remote cache", - Stage: FeatureStagePublicPreview, - Owner: grafanaOperatorExperienceSquad, - HideFromAdminPage: true, + Name: "renderAuthJWT", + Description: "Uses JWT-based auth for rendering instead of relying on remote cache", + Stage: FeatureStagePublicPreview, + Owner: grafanaOperatorExperienceSquad, }, { - Name: "refactorVariablesTimeRange", - Description: "Refactor time range variables flow to reduce number of API calls made when query variables are chained", - Stage: FeatureStagePublicPreview, - Owner: grafanaDashboardsSquad, - HideFromAdminPage: true, // Non-feature, used to test out a bug fix that impacts the performance of template variables. + Name: "refactorVariablesTimeRange", + Description: "Refactor time range variables flow to reduce number of API calls made when query variables are chained", + Stage: FeatureStagePublicPreview, + Owner: grafanaDashboardsSquad, }, { Name: "faroDatasourceSelector", @@ -229,7 +214,7 @@ var ( { Name: "enableDatagridEditing", Description: "Enables the edit functionality in the datagrid panel", - FrontendOnly: true, + FrontendOnly: false, // The set of plugins returned in frontend settings changes based on this flag Stage: FeatureStagePublicPreview, Owner: grafanaDatavizSquad, }, @@ -358,31 +343,26 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "externalServiceAccounts", - Description: "Automatic service account and token setup for plugins", - HideFromAdminPage: true, - Stage: FeatureStagePublicPreview, - Owner: identityAccessTeam, + Name: "externalServiceAccounts", + Description: "Automatic service account and token setup for plugins", + Stage: FeatureStagePublicPreview, + Owner: identityAccessTeam, }, { - Name: "enableNativeHTTPHistogram", - Description: "Enables native HTTP Histograms", - Stage: FeatureStageExperimental, - FrontendOnly: false, - Owner: grafanaBackendServicesSquad, - HideFromAdminPage: true, - AllowSelfServe: false, - RequiresRestart: true, + Name: "enableNativeHTTPHistogram", + Description: "Enables native HTTP Histograms", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaBackendServicesSquad, + RequiresRestart: true, }, { - Name: "disableClassicHTTPHistogram", - Description: "Disables classic HTTP Histogram (use with enableNativeHTTPHistogram)", - Stage: FeatureStageExperimental, - FrontendOnly: false, - Owner: grafanaBackendServicesSquad, - HideFromAdminPage: true, - AllowSelfServe: false, - RequiresRestart: true, + Name: "disableClassicHTTPHistogram", + Description: "Disables classic HTTP Histogram (use with enableNativeHTTPHistogram)", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaBackendServicesSquad, + RequiresRestart: true, }, { Name: "kubernetesSnapshots", @@ -403,7 +383,7 @@ var ( Description: "Use the kubernetes API in the frontend for dashboards", Stage: FeatureStageGeneralAvailability, Owner: grafanaDashboardsSquad, - FrontendOnly: true, + FrontendOnly: false, // The backend changes permission behavior based on this flag Expression: "true", // enabled by default }, { @@ -534,20 +514,18 @@ var ( Owner: grafanaAlertingSquad, }, { - Name: "alertingProvenanceLockWrites", - Description: "Enables a feature to avoid issues with concurrent writes to the alerting provenance table in MySQL", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "alertingProvenanceLockWrites", + Description: "Enables a feature to avoid issues with concurrent writes to the alerting provenance table in MySQL", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, }, { - Name: "alertingUIUseBackendFilters", - Description: "Enables the UI to use certain backend-side filters", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "alertingUIUseBackendFilters", + Description: "Enables the UI to use certain backend-side filters", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, }, { Name: "alertmanagerRemotePrimary", @@ -591,7 +569,7 @@ var ( Name: "dashboardNewLayouts", Description: "Enables experimental new dashboard layouts", Stage: FeatureStageExperimental, - FrontendOnly: true, + FrontendOnly: false, // The restore backend feature changes behavior based on this flag Owner: grafanaDashboardsSquad, }, { @@ -653,42 +631,35 @@ var ( Owner: grafanaDatavizSquad, }, { - Name: "kubernetesFeatureToggles", - Description: "Use the kubernetes API for feature toggle management in the frontend", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaOperatorExperienceSquad, - AllowSelfServe: false, - HideFromAdminPage: true, + Name: "kubernetesFeatureToggles", + Description: "Use the kubernetes API for feature toggle management in the frontend", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaOperatorExperienceSquad, }, { - Name: "cloudRBACRoles", - Description: "Enabled grafana cloud specific RBAC roles", - Stage: FeatureStagePublicPreview, - Owner: identityAccessTeam, - HideFromDocs: true, - AllowSelfServe: true, - HideFromAdminPage: true, - RequiresRestart: true, + Name: "cloudRBACRoles", + Description: "Enabled grafana cloud specific RBAC roles", + Stage: FeatureStagePublicPreview, + Owner: identityAccessTeam, + HideFromDocs: true, + RequiresRestart: true, }, { - Name: "alertingQueryOptimization", - Description: "Optimizes eligible queries in order to reduce load on datasources", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAlertingSquad, - AllowSelfServe: false, - Expression: "false", + Name: "alertingQueryOptimization", + Description: "Optimizes eligible queries in order to reduce load on datasources", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAlertingSquad, + Expression: "false", }, { - Name: "jitterAlertRulesWithinGroups", - Description: "Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group. Disables sequential evaluation if enabled.", - FrontendOnly: false, - Stage: FeatureStagePublicPreview, - Owner: grafanaAlertingSquad, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: false, - RequiresRestart: true, + Name: "jitterAlertRulesWithinGroups", + Description: "Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group. Disables sequential evaluation if enabled.", + FrontendOnly: false, + Stage: FeatureStagePublicPreview, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + RequiresRestart: true, }, { Name: "onPremToCloudMigrations", @@ -725,41 +696,37 @@ var ( Expression: "true", }, { - Name: "scopeApi", - Description: "In-development feature flag for the scope api using the app platform.", - Stage: FeatureStageExperimental, - Owner: grafanaAppPlatformSquad, - HideFromAdminPage: true, - Expression: "false", + Name: "scopeApi", + Description: "In-development feature flag for the scope api using the app platform.", + Stage: FeatureStageExperimental, + Owner: grafanaAppPlatformSquad, + Expression: "false", }, { - Name: "useScopeSingleNodeEndpoint", - Description: "Use the single node endpoint for the scope api. This is used to fetch the scope parent node.", - Stage: FeatureStageExperimental, - Owner: grafanaOperatorExperienceSquad, - Expression: "false", - FrontendOnly: true, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "useScopeSingleNodeEndpoint", + Description: "Use the single node endpoint for the scope api. This is used to fetch the scope parent node.", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + Expression: "false", + FrontendOnly: true, + HideFromDocs: true, }, { - Name: "useMultipleScopeNodesEndpoint", - Description: "Makes the frontend use the 'names' param for fetching multiple scope nodes at once", - Stage: FeatureStageExperimental, - Owner: grafanaOperatorExperienceSquad, - Expression: "false", - FrontendOnly: true, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "useMultipleScopeNodesEndpoint", + Description: "Makes the frontend use the 'names' param for fetching multiple scope nodes at once", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + Expression: "false", + FrontendOnly: true, + HideFromDocs: true, }, { - Name: "logQLScope", - Description: "In-development feature that will allow injection of labels into loki queries.", - Stage: FeatureStagePrivatePreview, - Owner: grafanaObservabilityLogsSquad, - Expression: "false", - HideFromDocs: true, - HideFromAdminPage: true, + Name: "logQLScope", + Description: "In-development feature that will allow injection of labels into loki queries.", + Stage: FeatureStagePrivatePreview, + Owner: grafanaObservabilityLogsSquad, + Expression: "false", + HideFromDocs: true, }, { Name: "sqlExpressions", @@ -790,58 +757,49 @@ var ( RequiresRestart: true, }, { - Name: "groupByVariable", - Description: "Enable groupBy variable support in scenes dashboards", - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "groupByVariable", + Description: "Enable groupBy variable support in scenes dashboards", + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + HideFromDocs: true, }, { - Name: "scopeFilters", - Description: "Enables the use of scope filters in Grafana", - FrontendOnly: false, - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - RequiresRestart: false, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "scopeFilters", + Description: "Enables the use of scope filters in Grafana", + FrontendOnly: false, + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + RequiresRestart: false, + HideFromDocs: true, }, { - Name: "oauthRequireSubClaim", - Description: "Require that sub claims is present in oauth tokens.", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "oauthRequireSubClaim", + Description: "Require that sub claims is present in oauth tokens.", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "refreshTokenRequired", - Description: "Require that refresh tokens are present in oauth tokens.", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "refreshTokenRequired", + Description: "Require that refresh tokens are present in oauth tokens.", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "newDashboardWithFiltersAndGroupBy", - Description: "Enables filters and group by variables on all new dashboards. Variables are added only if default data source supports filtering.", - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "newDashboardWithFiltersAndGroupBy", + Description: "Enables filters and group by variables on all new dashboards. Variables are added only if default data source supports filtering.", + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + HideFromDocs: true, }, { - Name: "cloudWatchNewLabelParsing", - Description: "Updates CloudWatch label parsing to be more accurate", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - Owner: awsDatasourcesSquad, - FrontendOnly: false, - AllowSelfServe: false, + Name: "cloudWatchNewLabelParsing", + Description: "Updates CloudWatch label parsing to be more accurate", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default + Owner: awsDatasourcesSquad, + FrontendOnly: false, }, { Name: "disableNumericMetricsSortingInExpressions", @@ -852,21 +810,18 @@ var ( RequiresRestart: true, }, { - Name: "grafanaManagedRecordingRules", - Description: "Enables Grafana-managed recording rules.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "grafanaManagedRecordingRules", + Description: "Enables Grafana-managed recording rules.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, }, { - Name: "queryLibrary", - Description: "Enables Saved queries (query library) feature", - Stage: FeatureStagePublicPreview, - Owner: grafanaSharingSquad, - FrontendOnly: false, - AllowSelfServe: false, + Name: "queryLibrary", + Description: "Enables Saved queries (query library) feature", + Stage: FeatureStagePublicPreview, + Owner: grafanaSharingSquad, + FrontendOnly: false, }, { Name: "dashboardLibrary", @@ -897,28 +852,25 @@ var ( FrontendOnly: true, }, { - Name: "alertingDisableSendAlertsExternal", - Description: "Disables the ability to send alerts to an external Alertmanager datasource.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "alertingDisableSendAlertsExternal", + Description: "Disables the ability to send alerts to an external Alertmanager datasource.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, }, { - Name: "preserveDashboardStateWhenNavigating", - Description: "Enables possibility to preserve dashboard variables and time range when navigating between dashboards", - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "preserveDashboardStateWhenNavigating", + Description: "Enables possibility to preserve dashboard variables and time range when navigating between dashboards", + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + HideFromDocs: true, }, { Name: "alertingCentralAlertHistory", Description: "Enables the new central alert history.", Stage: FeatureStageExperimental, Owner: grafanaAlertingSquad, - FrontendOnly: true, + FrontendOnly: false, // changes navtree from backend }, { Name: "pluginProxyPreserveTrailingSlash", @@ -935,59 +887,51 @@ var ( Expression: "true", // enabled by default }, { - Name: "authZGRPCServer", - Description: "Enables the gRPC server for authorization", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "authZGRPCServer", + Description: "Enables the gRPC server for authorization", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { Name: "ssoSettingsLDAP", Description: "Use the new SSO Settings API to configure LDAP", Stage: FeatureStageGeneralAvailability, Owner: identityAccessTeam, - AllowSelfServe: true, RequiresRestart: true, Expression: "true", // enabled by default }, { - Name: "zanzana", - Description: "Use openFGA as authorization engine.", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "zanzana", + Description: "Use openFGA as authorization engine.", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "zanzanaNoLegacyClient", - Description: "Use openFGA as main authorization engine and disable legacy RBAC clietn.", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "zanzanaNoLegacyClient", + Description: "Use openFGA as main authorization engine and disable legacy RBAC clietn.", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "reloadDashboardsOnParamsChange", - Description: "Enables reload of dashboards on scopes, time range and variables changes", - FrontendOnly: false, - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - RequiresRestart: false, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "reloadDashboardsOnParamsChange", + Description: "Enables reload of dashboards on scopes, time range and variables changes", + FrontendOnly: false, + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + RequiresRestart: false, + HideFromDocs: true, }, { - Name: "enableScopesInMetricsExplore", - Description: "Enables the scopes usage in Metrics Explore", - FrontendOnly: false, - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - RequiresRestart: false, - AllowSelfServe: false, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "enableScopesInMetricsExplore", + Description: "Enables the scopes usage in Metrics Explore", + FrontendOnly: false, + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + RequiresRestart: false, + HideFromDocs: true, }, { Name: "cloudWatchRoundUpEndTime", @@ -1024,13 +968,12 @@ var ( Expression: "true", // enabled by default }, { - Name: "vizActionsAuth", - Description: "Allows authenticated API calls in actions", - Stage: FeatureStagePublicPreview, - Owner: grafanaDatavizSquad, - FrontendOnly: true, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "vizActionsAuth", + Description: "Allows authenticated API calls in actions", + Stage: FeatureStagePublicPreview, + Owner: grafanaDatavizSquad, + FrontendOnly: true, + HideFromDocs: true, }, { Name: "alertingPrometheusRulesPrimary", @@ -1061,12 +1004,11 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "appPlatformGrpcClientAuth", - Description: "Enables the gRPC client to authenticate with the App Platform by using ID & access tokens", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "appPlatformGrpcClientAuth", + Description: "Enables the gRPC client to authenticate with the App Platform by using ID & access tokens", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { Name: "groupAttributeSync", @@ -1084,12 +1026,11 @@ var ( Expression: "true", }, { - Name: "improvedExternalSessionHandling", - Description: "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - Owner: identityAccessTeam, - AllowSelfServe: true, + Name: "improvedExternalSessionHandling", + Description: "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default + Owner: identityAccessTeam, }, { Name: "useSessionStorageForRedirection", @@ -1105,28 +1046,25 @@ var ( Owner: identityAccessTeam, }, { - Name: "unifiedStorageSearch", - Description: "Enable unified storage search", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "unifiedStorageSearch", + Description: "Enable unified storage search", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, }, { - Name: "unifiedStorageSearchSprinkles", - Description: "Enable sprinkles on unified storage search", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "unifiedStorageSearchSprinkles", + Description: "Enable sprinkles on unified storage search", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, }, { - Name: "managedDualWriter", - Description: "Pick the dual write mode from database configs", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "managedDualWriter", + Description: "Pick the dual write mode from database configs", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, }, { Name: "pluginsSriChecks", @@ -1169,13 +1107,11 @@ var ( RequiresRestart: true, }, { - Name: "passwordlessMagicLinkAuthentication", - Description: "Enable passwordless login via magic link authentication", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromDocs: true, - HideFromAdminPage: true, - AllowSelfServe: false, + Name: "passwordlessMagicLinkAuthentication", + Description: "Enable passwordless login via magic link authentication", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { Name: "exploreMetricsRelatedLogs", @@ -1228,67 +1164,60 @@ var ( Expression: "true", // Enabled by default for now }, { - Name: "alertingAIGenAlertRules", - Description: "Enable AI-generated alert rules.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingAIGenAlertRules", + Description: "Enable AI-generated alert rules.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingAIFeedback", - Description: "Enable AI-generated feedback from the Grafana UI.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingAIFeedback", + Description: "Enable AI-generated feedback from the Grafana UI.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingAIImproveAlertRules", - Description: "Enable AI-improve alert rules labels and annotations.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingAIImproveAlertRules", + Description: "Enable AI-improve alert rules labels and annotations.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingAIGenTemplates", - Description: "Enable AI-generated alerting templates.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingAIGenTemplates", + Description: "Enable AI-generated alerting templates.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingEnrichmentPerRule", - Description: "Enable enrichment per rule in the alerting UI.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingEnrichmentPerRule", + Description: "Enable enrichment per rule in the alerting UI.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingEnrichmentAssistantInvestigations", - Description: "Enable Assistant Investigations enrichment type.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingEnrichmentAssistantInvestigations", + Description: "Enable Assistant Investigations enrichment type.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingAIAnalyzeCentralStateHistory", - Description: "Enable AI-analyze central state history.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingAIAnalyzeCentralStateHistory", + Description: "Enable AI-analyze central state history.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { Name: "alertingNotificationsStepMode", @@ -1299,12 +1228,11 @@ var ( Expression: "true", }, { - Name: "unifiedStorageSearchUI", - Description: "Enable unified storage search UI", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "unifiedStorageSearchUI", + Description: "Enable unified storage search UI", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, }, { Name: "elasticsearchCrossClusterSearch", @@ -1351,12 +1279,11 @@ var ( Expression: "false", }, { - Name: "improvedExternalSessionHandlingSAML", - Description: "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - Owner: identityAccessTeam, - AllowSelfServe: true, + Name: "improvedExternalSessionHandlingSAML", + Description: "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.", + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default + Owner: identityAccessTeam, }, { Name: "teamHttpHeadersTempo", @@ -1386,12 +1313,11 @@ var ( FrontendOnly: true, }, { - Name: "fetchRulesUsingPost", - Description: "Use a POST request to list rules by passing down the namespaces user has access to", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "fetchRulesUsingPost", + Description: "Use a POST request to list rules by passing down the namespaces user has access to", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, }, { Name: "newLogsPanel", @@ -1402,14 +1328,13 @@ var ( Expression: "true", }, { - Name: "grafanaconThemes", - Description: "Enables the temporary themes for GrafanaCon", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaFrontendPlatformSquad, - HideFromAdminPage: true, - HideFromDocs: true, - RequiresRestart: true, - Expression: "true", + Name: "grafanaconThemes", + Description: "Enables the temporary themes for GrafanaCon", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaFrontendPlatformSquad, + HideFromDocs: true, + RequiresRestart: true, + Expression: "true", }, { Name: "alertingJiraIntegration", @@ -1420,58 +1345,52 @@ var ( HideFromDocs: true, }, { - Name: "alertingUseNewSimplifiedRoutingHashAlgorithm", - Description: "", - Stage: FeatureStagePublicPreview, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - RequiresRestart: true, - Expression: "true", + Name: "alertingUseNewSimplifiedRoutingHashAlgorithm", + Description: "", + Stage: FeatureStagePublicPreview, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + RequiresRestart: true, + Expression: "true", }, { - Name: "useScopesNavigationEndpoint", - Description: "Use the scopes navigation endpoint instead of the dashboardbindings endpoint", - Stage: FeatureStageExperimental, - Owner: grafanaOperatorExperienceSquad, - FrontendOnly: true, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "useScopesNavigationEndpoint", + Description: "Use the scopes navigation endpoint instead of the dashboardbindings endpoint", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + FrontendOnly: true, + HideFromDocs: true, }, { - Name: "scopeSearchAllLevels", - Description: "Enable scope search to include all levels of the scope node tree", - Stage: FeatureStageExperimental, - Owner: grafanaOperatorExperienceSquad, - HideFromDocs: true, - HideFromAdminPage: true, + Name: "scopeSearchAllLevels", + Description: "Enable scope search to include all levels of the scope node tree", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + HideFromDocs: true, }, { - Name: "alertingRuleVersionHistoryRestore", - Description: "Enables the alert rule version history restore feature", - FrontendOnly: true, - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "true", // enabled by default + Name: "alertingRuleVersionHistoryRestore", + Description: "Enables the alert rule version history restore feature", + FrontendOnly: true, + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "true", // enabled by default }, { - Name: "newShareReportDrawer", - Description: "Enables the report creation drawer in a dashboard", - Stage: FeatureStagePublicPreview, - Owner: grafanaOperatorExperienceSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "newShareReportDrawer", + Description: "Enables the report creation drawer in a dashboard", + Stage: FeatureStagePublicPreview, + Owner: grafanaOperatorExperienceSquad, + HideFromDocs: true, }, { - Name: "rendererDisableAppPluginsPreload", - Description: "Disable pre-loading app plugins when the request is coming from the renderer", - Stage: FeatureStageExperimental, - Owner: grafanaOperatorExperienceSquad, - HideFromAdminPage: true, - HideFromDocs: true, - FrontendOnly: true, + Name: "rendererDisableAppPluginsPreload", + Description: "Disable pre-loading app plugins when the request is coming from the renderer", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + HideFromDocs: true, + FrontendOnly: true, }, { Name: "assetSriChecks", @@ -1524,32 +1443,29 @@ var ( Owner: grafanaFrontendPlatformSquad, }, { - Name: "unifiedStorageGrpcConnectionPool", - Description: "Enables the unified storage grpc connection pool", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "unifiedStorageGrpcConnectionPool", + Description: "Enables the unified storage grpc connection pool", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, }, { - Name: "alertingRulePermanentlyDelete", - Description: "Enables UI functionality to permanently delete alert rules", - FrontendOnly: true, - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "true", // enabled by default + Name: "alertingRulePermanentlyDelete", + Description: "Enables UI functionality to permanently delete alert rules", + FrontendOnly: true, + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "true", // enabled by default }, { - Name: "alertingRuleRecoverDeleted", - Description: "Enables the UI functionality to recover and view deleted alert rules", - FrontendOnly: true, - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "true", // enabled by default + Name: "alertingRuleRecoverDeleted", + Description: "Enables the UI functionality to recover and view deleted alert rules", + FrontendOnly: false, // changes navtree from the backend + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "true", // enabled by default }, { Name: "multiTenantTempCredentials", @@ -1596,11 +1512,10 @@ var ( Owner: grafanaOSSBigTent, }, { - Name: "tempoAlerting", - Description: "Enables creating alerts from Tempo data source", - Stage: FeatureStageExperimental, - Owner: grafanaObservabilityTracesAndProfilingSquad, - FrontendOnly: true, + Name: "tempoAlerting", + Description: "Enables creating alerts from Tempo data source", + Stage: FeatureStageExperimental, + Owner: grafanaObservabilityTracesAndProfilingSquad, }, { Name: "pluginsAutoUpdate", @@ -1624,107 +1539,95 @@ var ( Expression: "false", }, { - Name: "alertingBulkActionsInUI", - Description: "Enables the alerting bulk actions in the UI", - FrontendOnly: true, - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "true", // enabled by default + Name: "alertingBulkActionsInUI", + Description: "Enables the alerting bulk actions in the UI", + FrontendOnly: true, + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "true", // enabled by default }, { - Name: "kubernetesAuthzApis", - Description: "Registers AuthZ /apis endpoint", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "kubernetesAuthzApis", + Description: "Registers AuthZ /apis endpoint", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "kubernetesAuthZHandlerRedirect", - Description: "Redirects the traffic from the legacy access control endpoints to the new K8s AuthZ endpoints", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "kubernetesAuthZHandlerRedirect", + Description: "Redirects the traffic from the legacy access control endpoints to the new K8s AuthZ endpoints", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "kubernetesAuthzResourcePermissionApis", - Description: "Registers AuthZ resource permission /apis endpoints", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "kubernetesAuthzResourcePermissionApis", + Description: "Registers AuthZ resource permission /apis endpoints", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "kubernetesAuthzZanzanaSync", - Description: "Enable sync of Zanzana authorization store on AuthZ CRD mutations", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "kubernetesAuthzZanzanaSync", + Description: "Enable sync of Zanzana authorization store on AuthZ CRD mutations", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "kubernetesAuthnMutation", - Description: "Enables create, delete, and update mutations for resources owned by IAM identity", - Stage: FeatureStageExperimental, - Owner: identityAccessTeam, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "kubernetesAuthnMutation", + Description: "Enables create, delete, and update mutations for resources owned by IAM identity", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, }, { - Name: "restoreDashboards", - Description: "Enables restore deleted dashboards feature", - Stage: FeatureStageExperimental, - Owner: grafanaFrontendSearchNavOrganise, - HideFromAdminPage: true, - Expression: "false", + Name: "restoreDashboards", + Description: "Enables restore deleted dashboards feature", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendSearchNavOrganise, + Expression: "false", }, { - Name: "alertEnrichment", - Description: "Enable configuration of alert enrichments in Grafana Cloud.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertEnrichment", + Description: "Enable configuration of alert enrichments in Grafana Cloud.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertEnrichmentMultiStep", - Description: "Allow multiple steps per enrichment.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertEnrichmentMultiStep", + Description: "Allow multiple steps per enrichment.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertEnrichmentConditional", - Description: "Enable conditional alert enrichment steps.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertEnrichmentConditional", + Description: "Enable conditional alert enrichment steps.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingImportAlertmanagerAPI", - Description: "Enables the API to import Alertmanager configuration", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingImportAlertmanagerAPI", + Description: "Enables the API to import Alertmanager configuration", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "alertingImportAlertmanagerUI", - Description: "Enables the UI to see imported Alertmanager configuration", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingImportAlertmanagerUI", + Description: "Enables the UI to see imported Alertmanager configuration", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { Name: "sharingDashboardImage", @@ -1757,34 +1660,31 @@ var ( Expression: "false", }, { - Name: "enableAppChromeExtensions", - Description: "Set this to true to enable all app chrome extensions registered by plugins.", - Stage: FeatureStageExperimental, - Owner: grafanaPluginsPlatformSquad, - HideFromAdminPage: true, - HideFromDocs: true, - FrontendOnly: true, - Expression: "false", // extensions will be disabled by default + Name: "enableAppChromeExtensions", + Description: "Set this to true to enable all app chrome extensions registered by plugins.", + Stage: FeatureStageExperimental, + Owner: grafanaPluginsPlatformSquad, + HideFromDocs: true, + FrontendOnly: true, + Expression: "false", // extensions will be disabled by default }, { - Name: "enableDashboardEmptyExtensions", - Description: "Set this to true to enable all dashboard empty state extensions registered by plugins.", - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - HideFromAdminPage: true, - HideFromDocs: true, - FrontendOnly: true, - Expression: "false", // extensions will be disabled by default + Name: "enableDashboardEmptyExtensions", + Description: "Set this to true to enable all dashboard empty state extensions registered by plugins.", + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + HideFromDocs: true, + FrontendOnly: true, + Expression: "false", // extensions will be disabled by default }, { - Name: "foldersAppPlatformAPI", - Description: "Enables use of app platform API for folders", - Stage: FeatureStageExperimental, - Owner: grafanaFrontendSearchNavOrganise, - HideFromAdminPage: true, - HideFromDocs: true, - FrontendOnly: true, - Expression: "false", + Name: "foldersAppPlatformAPI", + Description: "Enables use of app platform API for folders", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendSearchNavOrganise, + HideFromDocs: true, + FrontendOnly: true, + Expression: "false", }, { Name: "otelLogsFormatting", @@ -1794,21 +1694,19 @@ var ( Owner: grafanaObservabilityLogsSquad, }, { - Name: "alertingNotificationHistory", - Description: "Enables the notification history feature", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, - Expression: "false", + Name: "alertingNotificationHistory", + Description: "Enables the notification history feature", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { - Name: "unifiedStorageSearchDualReaderEnabled", - Description: "Enable dual reader for unified storage search", - Stage: FeatureStageExperimental, - Owner: grafanaSearchAndStorageSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "unifiedStorageSearchDualReaderEnabled", + Description: "Enable dual reader for unified storage search", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, }, { Name: "dashboardLevelTimeMacros", @@ -1818,22 +1716,20 @@ var ( FrontendOnly: true, }, { - Name: "alertmanagerRemoteSecondaryWithRemoteState", - Description: "Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.", - Stage: FeatureStageExperimental, - Owner: grafanaAlertingSquad, - HideFromAdminPage: true, - HideFromDocs: true, + Name: "alertmanagerRemoteSecondaryWithRemoteState", + Description: "Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, }, { - Name: "restrictedPluginApis", - Description: "Enables sharing a list of APIs with a list of plugins", - Stage: FeatureStageExperimental, - Owner: grafanaPluginsPlatformSquad, - HideFromAdminPage: true, - HideFromDocs: true, - FrontendOnly: true, - Expression: "false", + Name: "restrictedPluginApis", + Description: "Enables sharing a list of APIs with a list of plugins", + Stage: FeatureStageExperimental, + Owner: grafanaPluginsPlatformSquad, + HideFromDocs: true, + FrontendOnly: true, + Expression: "false", }, { Name: "favoriteDatasources", @@ -1872,14 +1768,13 @@ var ( Owner: grafanaPathfinderSquad, }, { - Name: "alertingTriage", - Description: "Enables the alerting triage feature", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaAlertingSquad, - HideFromDocs: true, - HideFromAdminPage: true, - Expression: "false", + Name: "alertingTriage", + Description: "Enables the alerting triage feature", + Stage: FeatureStageExperimental, + FrontendOnly: false, // changes navtree in backend + Owner: grafanaAlertingSquad, + HideFromDocs: true, + Expression: "false", }, { Name: "graphiteBackendMode", @@ -1992,25 +1887,22 @@ var ( Expression: "false", }, { - Name: "onlyStoreActionSets", - Description: "When storing dashboard and folder resource permissions, only store action sets and not the full list of underlying permission", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: false, - HideFromDocs: true, - HideFromAdminPage: true, // this should not be a user facing change - Owner: identityAccessTeam, - Expression: "true", + Name: "onlyStoreActionSets", + Description: "When storing dashboard and folder resource permissions, only store action sets and not the full list of underlying permission", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: false, + HideFromDocs: true, + Owner: identityAccessTeam, + Expression: "true", }, { - Name: "panelTimeSettings", - Description: "Enables a new panel time settings drawer", - FrontendOnly: false, - Stage: FeatureStageExperimental, - Owner: grafanaDashboardsSquad, - RequiresRestart: false, - AllowSelfServe: false, - HideFromDocs: false, - HideFromAdminPage: false, + Name: "panelTimeSettings", + Description: "Enables a new panel time settings drawer", + FrontendOnly: false, + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + RequiresRestart: false, + HideFromDocs: false, }, { Name: "dashboardTemplates", @@ -2048,16 +1940,14 @@ var ( Owner: grafanaPluginsPlatformSquad, }, { - Name: "rudderstackUpgrade", - Description: "Enables the new version of rudderstack", - FrontendOnly: true, - Stage: FeatureStageExperimental, - Owner: grafanaFrontendPlatformSquad, - Expression: "false", - RequiresRestart: false, - AllowSelfServe: false, - HideFromDocs: false, - HideFromAdminPage: false, + Name: "rudderstackUpgrade", + Description: "Enables the new version of rudderstack", + FrontendOnly: true, + Stage: FeatureStageExperimental, + Owner: grafanaFrontendPlatformSquad, + Expression: "false", + RequiresRestart: false, + HideFromDocs: false, }, } ) diff --git a/pkg/services/featuremgmt/toggles-gitlog.csv b/pkg/services/featuremgmt/toggles-gitlog.csv index d8fbbaf3924..3f34ca9edc9 100644 --- a/pkg/services/featuremgmt/toggles-gitlog.csv +++ b/pkg/services/featuremgmt/toggles-gitlog.csv @@ -75,7 +75,7 @@ traceqlEditor,2022-08-24T16:57:59Z,2022-12-13T13:27:45Z,c8f2148f7504b393b8f9b32f athenaAsyncQueryDataSupport,2022-09-05T15:39:45Z,2024-03-20T14:14:21Z,34fe7a1119c0f649d3e9df866353438ff2177fdc,Kevin Yu redshiftAsyncQueryDataSupport,2022-09-05T15:39:45Z,2024-03-20T14:14:21Z,34fe7a1119c0f649d3e9df866353438ff2177fdc,Kevin Yu increaseInMemDatabaseQueryCache,2022-09-12T07:50:54Z,2023-02-06T21:01:04Z,72ae4a5aa368d18793f778683e92ae4a3ac7a1db,Carl Bergquist -correlations,2022-09-16T13:14:27Z,,9b4cdfe6526a290e5708a8c0760d50efda607d5c,Piotr Jamróz +correlations,2022-09-16T13:14:27Z,2025-11-13T09:21:46Z,9b4cdfe6526a290e5708a8c0760d50efda607d5c,Piotr Jamróz grpcServer,2022-09-26T20:25:34Z,,55aae797441d2beadc911e390f14c04bb3f101da,Alexander Emelin alertingBigTransactions,2022-10-06T06:22:58Z,2023-04-06T16:06:25Z,b476ae62fba9b94dd272409c521812108750bf50,Joe Blubaugh lokiMonacoEditor,2022-10-06T14:35:30Z,2023-02-06T10:18:01Z,729ce8bb72a5db53fbeb1b5856557652bb175c7a,Matias Chomicki @@ -88,7 +88,7 @@ showDashboardValidationWarnings,2022-10-14T13:51:05Z,,2e16d5499e1cf29e67db5031fe interFont,2022-10-15T14:22:33Z,2022-12-01T11:59:37Z,9f5e691994c9db15f5984b196ab55fd7213b7f72,Torkel Ödegaard accessControlOnCall,2022-10-19T16:10:09Z,2025-02-25T12:44:40Z,717bd4a6c051d1447c9e35385a4ba176dd391c65,Gabriel MABILLE newDBLibrary,2022-10-26T01:20:41Z,2023-11-14T14:51:35Z,a3acfb1a48126119fd4913efbb572de162264e92,Ryan McKinley -nestedFolders,2022-10-26T14:15:14Z,2025-08-06T07:07:23Z,b346ae03105af606521bbefec16722d590378646,Kristin Laemmert +nestedFolders,2022-10-26T14:15:14Z,2025-07-31T22:56:50Z,b346ae03105af606521bbefec16722d590378646,Kristin Laemmert datasourceLogger,2022-11-02T13:51:51Z,2023-02-07T11:49:16Z,06705a49e236dc3ab3b35db51b7bad8625e4866f,Carl Bergquist promQueryBuilder,2022-11-03T17:34:01Z,2022-12-19T13:52:06Z,857e545c5ac71722c8b2d3d27621dfb453dbb8ff,Ryan McKinley elasticsearchBackendMigration,2022-11-10T15:35:15Z,2023-04-12T12:20:43Z,261d620f1c46eb43282cc444ffb4633b77af4283,Ivana Huckova @@ -108,7 +108,7 @@ supportBundles,2022-12-20T10:13:37Z,2023-02-10T09:12:04Z,2c7410c87d81b8a644e738f publicDashboardsEmailSharing,2023-01-03T19:45:15Z,,f0ee3ac80ae2af695c56e709af435c155a2fb33d,owensmallwood alertingNoNormalState,2023-01-13T23:29:29Z,2025-02-03T15:32:50Z,9d57b1c72eb817c5fe1b43ad7bc9706a264b2da0,Yuri Tseretyan azureMultipleResourcePicker,2023-01-20T15:29:23Z,2023-01-30T16:19:03Z,b1efd911c1f7bfabe1b8231b059470c72dce4712,Andres Martinez Gotor -editPanelCSVDragAndDrop,2023-01-24T09:43:44Z,,4167214e3535f41c85004c6aca86fcb9ae8956dd,Zoltán Bedi +editPanelCSVDragAndDrop,2023-01-24T09:43:44Z,2025-11-12T14:47:44Z,4167214e3535f41c85004c6aca86fcb9ae8956dd,Zoltán Bedi topNavCommandPalette,2023-01-24T12:41:09Z,2023-02-24T12:14:53Z,88347caf5fcc99c21c8a07367b9e846947367636,Josh Hunt k8sDashboards,2023-01-25T19:10:16Z,2023-02-09T17:54:00Z,4965cf2edae6eae8e341e6c93c3a1aa56417efb3,Ryan McKinley logsContextDatasourceUi,2023-01-27T14:12:01Z,,7c02d9bb8a864aab35c676f1a44c6c5be9eb6c09,Sven Grossmann @@ -155,11 +155,11 @@ extraThemes,2023-05-10T13:37:04Z,2025-05-20T08:18:08Z,f8cf67347f069310b8cc6a2d9f dataSourcePageHeader,2023-05-23T13:18:00Z,2023-11-06T20:21:15Z,7f84e83ffee53446b4c458c29f7c1385dac09ce3,Taewoo K alertingNotificationsPoliciesMatchingInstances,2023-05-30T13:15:22Z,2023-11-09T17:35:03Z,2f0728ac677c26b291947354058253d2e8a50bb7,Konrad Lalik lokiPredefinedOperations,2023-06-02T10:52:36Z,2025-06-30T14:08:36Z,06003c98c855fcb7c1c718c3f0e5f190b644656a,Ivana Huckova -pluginsFrontendSandbox,2023-06-05T08:51:36Z,,1ed4c0382b3812eecf9722596d8a3ac922f3543d,Esteban Beltran +pluginsFrontendSandbox,2023-06-05T08:51:36Z,2025-11-06T10:06:53Z,1ed4c0382b3812eecf9722596d8a3ac922f3543d,Esteban Beltran refactorVariablesTimeRange,2023-06-06T13:12:09Z,,07dd90b5a8866273813c6b4a5e7087cea529f908,Alexa V -sqlDatasourceDatabaseSelection,2023-06-06T16:28:52Z,2025-08-12T13:22:30Z,c0a1fc2cbdc1ceab4fdcb43270fd3720efe4494a,Jev Forsberg +sqlDatasourceDatabaseSelection,2023-06-06T16:28:52Z,2025-07-31T22:56:50Z,c0a1fc2cbdc1ceab4fdcb43270fd3720efe4494a,Jev Forsberg cloudWatchLogsMonacoEditor,2023-06-12T13:49:52Z,2024-03-18T12:56:57Z,5a831d877ac3305e7dfdb54057069b41b36b40fa,Isabella Siu -recordedQueriesMulti,2023-06-14T12:34:22Z,,db75f20e53e717503fe3bf27217fedc40f4a12e1,Kyle Brandt +recordedQueriesMulti,2023-06-14T12:34:22Z,2025-11-10T20:31:43Z,db75f20e53e717503fe3bf27217fedc40f4a12e1,Kyle Brandt exploreScrollableLogsContainer,2023-06-15T11:25:34Z,2024-03-12T14:53:13Z,cda10fae525d5cf784368791e1aaa1de2ce88f4b,Gareth Dawson alertingLokiRangeToInstant,2023-06-16T17:55:49Z,2023-09-13T11:52:40Z,934ba1aaa1fa4c1b9f1dcc0d6753c67a23c4f466,Jean-Philippe Quéméner lokiExperimentalStreaming,2023-06-19T10:03:51Z,,271cdb4baa6d5b021e013b463350f227173c4e95,Gábor Farkas @@ -178,7 +178,7 @@ lokiLogsDataplane,2023-07-13T07:58:00Z,,e045860fbf449feb101d187730a46bb1cdab787a mlExpressions,2023-07-13T17:37:50Z,,541bfe636daa55705a130df4004aa417c99adc09,Yuri Tseretyan disableTraceQLStreaming,2023-07-14T14:10:46Z,2023-07-26T13:33:16Z,c1709c93013a3fa5797898e81c046a2a53ae7ed4,Andre Pereira grafanaAPIServer,2023-07-14T19:22:10Z,2024-01-23T16:27:28Z,52121b7165b6ffcdf10dd7a62fdf04043ef0c00a,Todd Treece -featureToggleAdminPage,2023-07-18T20:43:32Z,,600f623610b00547400e00fd0b2ee7dc4bab5849,Ibrahim +featureToggleAdminPage,2023-07-18T20:43:32Z,2025-08-29T14:46:39Z,600f623610b00547400e00fd0b2ee7dc4bab5849,Ibrahim lokiFormatQuery,2023-07-21T12:03:56Z,2024-06-05T09:46:28Z,4e42f9b6198d39ffca82c3c3d32a16d54c66090b,Gareth Dawson splitScopes,2023-07-21T14:23:01Z,2024-02-23T15:53:37Z,cfa1a2c55feea58bd7f82e4e03df2ea712ef34a7,Ieva awsAsyncQueryCaching,2023-07-21T15:34:07Z,,56913fbd9578c4666599b2ab46423361d6c9261f,Isabella Siu @@ -215,7 +215,7 @@ enableNativeHTTPHistogram,2023-10-03T18:23:55Z,,0fc403d116b9cbd0b93e497cff0ddded transformationsVariableSupport,2023-10-04T14:28:46Z,2025-02-17T12:07:11Z,40cdb3033697e8d8e43d0ee0342f0d16fac7d633,Oscar Kilhed kubernetesPlaylists,2023-10-05T19:00:36Z,2024-08-13T08:03:28Z,664ebf771e2a1d2f94bc14d3dababb634c4ac40f,Todd Treece grafanaAPIServerWithExperimentalAPIs,2023-10-06T18:55:22Z,,717a9dd6160e352ff7c184bfcddf2410eed9d908,Ryan McKinley -panelMonitoring,2023-10-09T05:19:08Z,,ef82767dabea7d19cd8efac0c36ed590d4d767f4,Victor Marin +panelMonitoring,2023-10-09T05:19:08Z,2025-11-07T19:04:42Z,ef82767dabea7d19cd8efac0c36ed590d4d767f4,Victor Marin navAdminSubsections,2023-10-10T10:50:44Z,2023-11-17T10:04:34Z,f56cc6fdc01dbf2efb802f22650cb2bef0c40179,Ashley Harrison recoveryThreshold,2023-10-10T14:51:50Z,2025-04-24T15:58:17Z,810fbc3327841da6d21f945e75cad9daf040e625,Yuri Tseretyan libraryPanelRBAC,2023-10-11T23:30:50Z,2025-06-27T08:09:02Z,a12cb8cbf3a9b33841b2f2cb1522be11de78c86a,kay delaney @@ -223,7 +223,7 @@ awsDatasourcesNewFormStyling,2023-10-12T08:59:10Z,2024-07-22T12:48:17Z,2771fb940 cachingOptimizeSerializationMemoryUsage,2023-10-12T16:56:49Z,,94ce87571ddfcede0fb7a229a65502b385d5bca3,Michael Mandrus panelTitleSearchInV1,2023-10-13T12:04:24Z,2025-01-21T09:59:32Z,bf2f2540da7a4e4b8d80e1fa4ae3d05868cf7b69,Arati R exploreContentOutline,2023-10-13T16:57:13Z,2024-06-24T15:45:42Z,4ec54bc2c39ba43843c693fdb2a4529b6a4703f2,Haris Rozajac -formatString,2023-10-13T18:17:12Z,2025-11-12T00:00:00Z,889576ac1d9278b1c6e3e278e8195968646a2db0,Sol +formatString,2023-10-13T18:17:12Z,2025-11-17T13:06:30Z,889576ac1d9278b1c6e3e278e8195968646a2db0,Sol pluginsInstrumentationStatusSource,2023-10-17T08:27:45Z,2024-02-21T11:57:40Z,f5076d1868caa14ce44a70e812315541b4199d9f,Giuseppe Guerra teamHttpHeaders,2023-10-17T10:23:54Z,2025-02-20T10:26:46Z,be5ba6813209b5b24e955e0f761032cb5826b578,Eric Leijonmarck costManagementUi,2023-10-17T16:15:51Z,2024-01-08T14:25:11Z,de1ed216f4bbf6f341aa22b144fa66d583a63981,Adam Bannach @@ -237,10 +237,10 @@ alertmanagerRemoteSecondary,2023-10-30T16:27:08Z,,363830883cb1f5de30f7015df5cba4 annotationPermissionUpdate,2023-10-31T13:30:13Z,,c51c51458e4dd103aa0c099aa48b0d41f9375f86,Ieva kubernetesPlaylistsAPI,2023-10-31T17:26:39Z,2023-11-08T19:14:05Z,dd773e74f120ba908cafd596a6875c2d7fa199bf,Ryan McKinley traceToProfiles,2023-11-01T10:14:24Z,2024-01-22T14:21:14Z,c39e9a8f527b79881b95f64fd1c413b4bff42983,Joey -extractFieldsNameDeduplication,2023-11-02T15:47:42Z,,0eda368d32dbd7f57635cabd6b7d6870ca7b58ee,Oscar Kilhed +extractFieldsNameDeduplication,2023-11-02T15:47:42Z,2025-11-12T10:08:13Z,0eda368d32dbd7f57635cabd6b7d6870ca7b58ee,Oscar Kilhed dashboardSceneForViewers,2023-11-02T19:02:25Z,,6e80a3d59b14d9af235431d6acdce95835e2a792,Dominik Prokop panelFilterVariable,2023-11-03T12:15:54Z,,6bf4d0cbc6756ec8e8fb64b3a9d7b76f4c4194f5,Jacob Zelek -addFieldFromCalculationStatFunctions,2023-11-03T14:39:58Z,,61d63d3034d7eed4837c63e1f5cb2f6b7114758d,Victor Marin +addFieldFromCalculationStatFunctions,2023-11-03T14:39:58Z,2025-11-17T15:58:43Z,61d63d3034d7eed4837c63e1f5cb2f6b7114758d,Victor Marin pdfTables,2023-11-06T13:39:22Z,,95b48339f89c7d267bce7d894404d26ccd75d0e3,Agnès Toulet newVizTooltips,2023-11-06T16:35:59Z,2024-04-03T00:32:01Z,6b4b7127544865b78f712d907e6f1719595f4232,Adela Almasan ssoSettingsApi,2023-11-08T09:50:01Z,2025-07-03T08:53:33Z,5285e9503be5702680acb2b52a6bda0632f4603d,Misi @@ -250,10 +250,11 @@ alertingDetailsViewV2,2023-11-09T17:35:03Z,2024-03-14T14:18:01Z,323ee7c38ceb18b8 alertingSimplifiedRouting,2023-11-10T13:14:39Z,2025-05-09T13:30:56Z,68e37c3925080cf64a5e7570eabb042f19ca2dbf,Sonia Aguilar dashboardScene,2023-11-13T08:51:21Z,,4bc322ca1d6ed63d7e79eecb1ed09f3043f9aedb,Torkel Ödegaard datatrails,2023-11-15T11:28:29Z,2024-04-09T18:15:18Z,1f1d348e1700735683dc9b80fce106b8a5bc4cee,Torkel Ödegaard -pluginsSkipHostEnvVars,2023-11-15T17:09:14Z,,cb0a88a02770eaee2561fd434d8339cab268f02e,Giuseppe Guerra +pluginsSkipHostEnvVars,2023-11-15T17:09:14Z,2025-11-13T15:31:57Z,cb0a88a02770eaee2561fd434d8339cab268f02e,Giuseppe Guerra logRowsPopoverMenu,2023-11-16T09:48:10Z,,9cb303c3f701169e8651267744a4b5fe1695a066,Matias Chomicki lokiStructuredMetadata,2023-11-16T16:06:14Z,2025-06-30T14:09:44Z,a01f8c5b42bb7937139b8d52e9723cc37e5f7ae6,Sven Grossmann tracesEmbeddedFlameGraph,2023-11-23T13:36:53Z,2024-01-22T14:21:14Z,4f46fb412ca4e96b0b0ef3f462aead3fc146389e,Joey +regressionTransformation,2023-11-24T14:49:16Z,2025-07-01T13:59:22Z,ab982e7bd36b86c94093bddcc31008f5dc49660e,Oscar Kilhed displayAnonymousStats,2023-11-29T16:58:41Z,2024-02-23T15:53:37Z,59bdff0280d52ca5d8918157d7697b9279b25501,Eric Leijonmarck influxqlStreamingParser,2023-11-29T17:29:35Z,,5845f140758473ab5ffe789bec4077032fd22839,ismail simsek kubernetesSnapshots,2023-12-05T22:31:49Z,,439edebcd605a1b63bf3a9b0ab5c2b83341cd5cd,Ryan McKinley @@ -274,12 +275,12 @@ jitterAlertRules,2024-01-18T18:48:11Z,2024-02-09T21:53:58Z,00a260effab802edc8f72 jitterAlertRulesWithinGroups,2024-01-18T18:48:11Z,,00a260effab802edc8f72df50bfb6447aac343f0,Alexander Weaver onPremToCloudMigrations,2024-01-22T16:09:08Z,,cf13cb9f70c2230f17450667ce59440304fb023c,Michael Mandrus alertingSaveStatePeriodic,2024-01-23T16:03:30Z,,aa25776f813926cb4f1947d4ae5a014f4e7728ff,Jean-Philippe Quéméner -promQLScope,2024-01-29T20:22:17Z,,43d0664340f3e3af219d7b5c747f486a073f5ce3,Kyle Brandt +promQLScope,2024-01-29T20:22:17Z,2025-10-10T14:53:18Z,43d0664340f3e3af219d7b5c747f486a073f5ce3,Kyle Brandt slateAutocomplete,2024-01-31T10:01:20Z,2024-02-01T13:03:11Z,39057552dc2ddfe8ee770c67a71cf55159aca741,Ashley Harrison nodeGraphDotLayout,2024-01-31T16:26:12Z,2025-04-08T14:37:17Z,cb945aa5df3453ab2a851ef7ec8bee244e3eeac4,Andrej Ocenas kubernetesQueryServiceRewrite,2024-01-31T18:36:51Z,2024-04-19T09:26:21Z,e013cd427cb0457177e11f19ebd30bc523b36c76,Ryan McKinley influxdbRunQueriesInParallel,2024-02-01T10:58:24Z,,536153c3362782ae7f79e06755e8c7e0cd3b3acd,ismail simsek -groupToNestedTableTransformation,2024-02-07T14:28:26Z,,756cd3c28b0648f55a6c0158f5faf63b632ebe70,Kyle Cunningham +groupToNestedTableTransformation,2024-02-07T14:28:26Z,2025-11-17T11:57:22Z,756cd3c28b0648f55a6c0158f5faf63b632ebe70,Kyle Cunningham newPDFRendering,2024-02-08T12:09:34Z,,28e66b4ad82ecebb551374325f0be4332412341c,Agnès Toulet autoMigrateGraphPanel,2024-02-08T22:00:48Z,2025-04-04T09:31:35Z,829672759c12b27f849c92c3a2aee4a6b0037920,Nathan Marrs dashboardSceneSolo,2024-02-11T08:08:47Z,,fe6d1460b09b403fc74fd20e95f61513eede2555,Torkel Ödegaard @@ -290,7 +291,7 @@ autoMigrateTablePanel,2024-02-14T16:06:25Z,2025-04-04T09:31:35Z,ce750e06187599da autoMigrateWorldmapPanel,2024-02-14T16:06:25Z,2025-04-04T09:31:35Z,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs groupByVariable,2024-02-14T17:18:04Z,,f016f95298fe490a865612864520f3622f8e804a,Dominik Prokop alertingUpgradeDryrunOnStart,2024-02-16T16:29:54Z,2024-03-14T14:36:35Z,dfaf6d1e2e13b2bd11dc8f0cd4432bfcad819aa9,Matthew Jacobson -expressionParser,2024-02-17T00:59:11Z,2025-08-26T13:21:24Z,f23f50f58d7ab5cb1fd88b42b6c58ec09c1a159d,Ryan McKinley +expressionParser,2024-02-17T00:59:11Z,2025-07-31T22:56:50Z,f23f50f58d7ab5cb1fd88b42b6c58ec09c1a159d,Ryan McKinley sqlExpressions,2024-02-27T21:16:00Z,,70009201d44c2d0ab39cc77081808a69a6c4fd63,Scott Lepper aiGeneratedDashboardChanges,2024-03-05T12:01:31Z,,a7c06d26f14b2a9fa8faa929a6c9a0c355018429,Ivan Ortega Alba scopeFilters,2024-03-05T15:41:19Z,,b3efb4217e48656f24aacdffd5737595d7361afe,Carl Bergquist @@ -303,7 +304,7 @@ usePrometheusFrontendPackage,2024-03-23T00:47:53Z,2024-04-15T21:45:23Z,d08459521 oauthRequireSubClaim,2024-03-25T13:22:24Z,,2f3a01f79fda7f6c465dd64adf5cf5c7227f9d19,Karl Persson authAPIAccessTokenAuth,2024-04-02T15:45:15Z,2025-02-04T15:31:24Z,5340a6e548b1e5fcd3af66695ca34d7e943fd068,Jo newDashboardWithFiltersAndGroupBy,2024-04-04T11:25:21Z,,32b6ef9d153dc7def2c17b81b02cfb1c412eb315,Dominik Prokop -prometheusCodeModeMetricNamesSearch,2024-04-04T20:38:23Z,2025-08-27T13:11:58Z,559fab9dc6a6c882e7d9621ba013c3a53347176a,Nick Richmond +prometheusCodeModeMetricNamesSearch,2024-04-04T20:38:23Z,2025-07-31T22:56:50Z,559fab9dc6a6c882e7d9621ba013c3a53347176a,Nick Richmond cloudWatchNewLabelParsing,2024-04-05T15:57:56Z,,58f32150c262605d188874b88f44a7dece4b9388,Isabella Siu exploreMetrics,2024-04-09T18:15:18Z,2025-04-11T20:45:14Z,66c0fd4dcc3202e11f41b302d27894dc162fb288,Darren Janeczek accessActionSets,2024-04-12T16:19:25Z,2025-03-13T15:18:23Z,56f4664875047d6861ea3facbc94cd921e263950,Ieva @@ -314,8 +315,8 @@ queryServiceRewrite,2024-04-19T09:26:21Z,,5a8384a2455bbd3c0ba5ec67e5f5e3cc4a8369 grafanaManagedRecordingRules,2024-04-22T17:53:16Z,2025-05-19T10:15:49Z,c32953e52cf9fd46a462711bc23fde15a5c3b6bf,Alexander Weaver logsExploreTableDefaultVisualization,2024-05-02T15:28:15Z,,840aeddbd1957117d9b62074c155e87bb4afd4b4,Galen Kistler autofixDSUID,2024-05-03T11:32:07Z,2024-06-20T10:56:39Z,b6f899d953a0924760dc5fd5a3d40669c86d475d,Andres Martinez Gotor -newDashboardSharingComponent,2024-05-03T15:02:18Z,,d1434fad3a68bc1d2b49725be31e45526e6ad349,Juan Cabanas -tlsMemcached,2024-05-09T19:12:08Z,,b009536329d110afd807ef2f27f2b7dcc7d310ba,lean.dev +newDashboardSharingComponent,2024-05-03T15:02:18Z,2025-08-29T14:46:39Z,d1434fad3a68bc1d2b49725be31e45526e6ad349,Juan Cabanas +tlsMemcached,2024-05-09T19:12:08Z,2025-11-12T15:49:28Z,b009536329d110afd807ef2f27f2b7dcc7d310ba,lean.dev notificationBanner,2024-05-13T09:32:34Z,2025-01-10T10:18:43Z,f3953b4955c218cc4678e842faa3d3380fd0f8f7,Alex Khomenko dualWritePlaylistsMode2,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R dualWritePlaylistsMode3,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R @@ -329,7 +330,7 @@ alertingCentralAlertHistory,2024-05-29T15:01:38Z,,289ce6185574df99acea37b86c2e08 pluginProxyPreserveTrailingSlash,2024-06-05T11:36:14Z,,fe3e5917f1bc83ab29b6a57578316e054718aa4a,Marcus Efraimsson kubernetesDashboards,2024-06-05T14:34:23Z,,41e0430f83bf7db50c4caaa1472afa3bf3d5c2dc,Ryan McKinley azureMonitorPrometheusExemplars,2024-06-06T16:53:17Z,,c9778c3332aa93e5dd8bc3b894264e1955f0d593,Andreas Christou -pinNavItems,2024-06-10T11:40:03Z,,84b638fb26cecf856374bb3d09b123061b4b8a6b,Laura Fernández +pinNavItems,2024-06-10T11:40:03Z,2025-11-17T12:12:47Z,84b638fb26cecf856374bb3d09b123061b4b8a6b,Laura Fernández authZGRPCServer,2024-06-13T09:41:35Z,,afcb5a855c26e985e43861bff6fab36b1b008109,Gabriel MABILLE openSearchBackendFlowEnabled,2024-06-17T09:41:50Z,2025-02-03T13:24:49Z,ab2af9b8f75cd13595f4d487c1168e849768a518,Ida Štambuk ssoSettingsLDAP,2024-06-18T11:31:27Z,,d074cc7892b96a1333bd07011baff146ea71e21d,Mihai Doarna @@ -388,7 +389,7 @@ enableExtensionsAdminPage,2024-11-05T15:55:10Z,,c7a7f7dce5d93dc5044c4be140433ee2 exploreMetricsRelatedLogs,2024-11-05T16:28:43Z,,82ac9e2bb67a665c3b56e3a80c47d8aea7f7d044,ismail simsek enableScopesInMetricsExplore,2024-11-06T13:11:33Z,,a80517d6fecf245cb9366b5662ab6d8760200389,Bogdan Matei zipkinBackendMigration,2024-11-07T09:35:53Z,2025-01-28T14:11:10Z,c151021b162d99da2cb0f09485f90a2b5d38c11c,Ivana Huckova -preinstallAutoUpdate,2024-11-07T12:14:25Z,,a415c0b83184ad473b3f8e10c0319c46d9e5c515,Andres Martinez Gotor +preinstallAutoUpdate,2024-11-07T12:14:25Z,2025-11-10T14:06:30Z,a415c0b83184ad473b3f8e10c0319c46d9e5c515,Andres Martinez Gotor enableSCIM,2024-11-07T14:38:46Z,,3d51ca7377eff46b39750a8e0311ead7b75fd359,linoman logQLScope,2024-11-11T11:53:24Z,,c7b6822a5e9cba703e521536947682aaf801203e,Carl Bergquist userStorageAPI,2024-11-12T11:56:41Z,2025-03-27T12:40:00Z,c3494614e39638f6d78b79397cf6032cbcde4b9f,Andres Martinez Gotor @@ -417,7 +418,7 @@ k8SFolderCounts,2024-12-27T17:10:44Z,,df36e77cd31d2ad77e3d708748d040367a0c8c9c,L k8SFolderMove,2024-12-27T17:10:44Z,,df36e77cd31d2ad77e3d708748d040367a0c8c9c,Leonor Oliveira kubernetesRestore,2025-01-03T14:48:47Z,2025-03-20T21:38:32Z,5429512779bd5f25b88ff728ea91efdef7dfafa0,Stephanie Hingtgen improvedExternalSessionHandlingSAML,2025-01-09T17:02:49Z,,c52ec21c75ab72c2f7d28259bac0364edae560d0,Misi -teamHttpHeadersMimir,2025-01-13T10:42:47Z,2025-08-07T09:04:46Z,04acbcdef23f673bd6bbfdbbece29c9769ce155a,Eric Leijonmarck +teamHttpHeadersMimir,2025-01-13T10:42:47Z,2025-07-31T22:56:50Z,04acbcdef23f673bd6bbfdbbece29c9769ce155a,Eric Leijonmarck ABTestFeatureToggleA,2025-01-13T21:13:13Z,2025-05-27T19:18:23Z,009d7f42b3d09b3a6be1f00f07314e2b25af7ebc,Nathan Marrs ABTestFeatureToggleB,2025-01-13T21:13:13Z,2025-05-27T19:18:23Z,009d7f42b3d09b3a6be1f00f07314e2b25af7ebc,Nathan Marrs kubernetesFoldersServiceV2,2025-01-13T21:15:35Z,2025-02-18T23:11:26Z,766d645d827f5e6e0872ae30e5fe23226ae85785,maicon @@ -425,10 +426,10 @@ queryLibraryDashboards,2025-01-14T11:01:15Z,2025-02-14T16:39:22Z,740cd22fe51a354 elasticsearchImprovedParsing,2025-01-15T17:05:54Z,,bab55a4cb84f2ba57838f96a492ab9aa7f307957,Adam Yeats grafanaAdvisor,2025-01-20T10:08:00Z,,c1364d6be6f552203ba786f17a89664304b89247,Andres Martinez Gotor datasourceConnectionsTab,2025-01-21T17:39:48Z,,97d8f68b705f9949493079d1833abfe80e7b48f3,Syerikjan Kh -unifiedStorageSearchPermissionFiltering,2025-01-22T11:38:37Z,2025-08-06T08:04:32Z,dd483fc17fa4a2931848e3574cfc31ea6f6530d9,owensmallwood +unifiedStorageSearchPermissionFiltering,2025-01-22T11:38:37Z,2025-07-31T22:56:50Z,dd483fc17fa4a2931848e3574cfc31ea6f6530d9,owensmallwood alertingSaveStateCompressed,2025-01-27T17:47:33Z,,cb43f4b6962fca18655b3ba634adeb4d59dc89df,Alexander Akhmetov fetchRulesUsingPost,2025-01-29T12:17:44Z,,1444051b65af0de6c412a12132083135c7730414,Fayzal Ghantiwala -templateVariablesUsesCombobox,2025-01-31T09:53:13Z,,7190bfb0ca675fc1b3b5d7ddf7e0ee9d1c9ca3d7,Tobias Skarhed +templateVariablesUsesCombobox,2025-01-31T09:53:13Z,2025-11-13T03:31:18Z,7190bfb0ca675fc1b3b5d7ddf7e0ee9d1c9ca3d7,Tobias Skarhed alertingAlertmanagerExtraDedupStage,2025-01-31T16:12:38Z,2025-03-26T19:15:10Z,0be6e1bb86613b6466e531119fe1cfec67d35aac,Yuri Tseretyan alertingAlertmanagerExtraDedupStageStopPipeline,2025-01-31T16:12:38Z,2025-03-26T19:15:10Z,0be6e1bb86613b6466e531119fe1cfec67d35aac,Yuri Tseretyan exploreMetricsUseExternalAppPlugin,2025-02-03T20:46:54Z,2025-04-11T20:45:14Z,29fa6dfc8de0a758f9f37f000c923f0806c3b629,Nick Richmond @@ -445,24 +446,24 @@ rendererDisableAppPluginsPreload,2025-02-24T14:43:06Z,,608d974585c696253ac629f3c assetSriChecks,2025-03-04T10:56:35Z,,bbfeb8d220cc67c329aa2b5d6ed693ae1cb54325,Jack Westbrook alertRuleRestore,2025-03-05T14:15:26Z,,374380d1f6ab05e98bc00b0b0fdaa076d1d30ffa,Yuri Tseretyan grafanaManagedRecordingRulesDatasources,2025-03-07T13:30:40Z,2025-06-02T08:56:05Z,14ebec527ccf22c15321ac8394a4f9c3909dd4e1,Steve Simpson -inviteUserExperimental,2025-03-07T19:09:59Z,,5e21b9e2d1ebad0dcb5fce5fa1bc48b74cea4b45,Juan Cabanas +inviteUserExperimental,2025-03-07T19:09:59Z,2025-11-14T15:33:26Z,5e21b9e2d1ebad0dcb5fce5fa1bc48b74cea4b45,Juan Cabanas extraLanguages,2025-03-11T10:07:16Z,2025-03-27T12:58:52Z,210c886bb7b913e4ca0b77ff142a72185a82a385,Josh Hunt infinityRunQueriesInParallel,2025-03-14T12:54:04Z,,d72b6649810ffacebe73ef67c6b5bd6e46dfab47,ismail simsek noBackdropBlur,2025-03-14T15:21:35Z,2025-04-08T10:58:19Z,bf172dfd296d15c02473904646c19d2bb663cc3f,Josh Hunt alertingMigrationUI,2025-03-14T16:40:05Z,,ef9dca9ea369997116cd7880c238597fc18ca682,Tom Ratcliffe -unifiedStorageHistoryPruner,2025-03-17T10:36:38Z,,1700a8aa9f2a103f953a385544c2ce200e6fcf9d,Jean-Philippe Quéméner +unifiedStorageHistoryPruner,2025-03-17T10:36:38Z,2025-11-17T19:47:37Z,1700a8aa9f2a103f953a385544c2ce200e6fcf9d,Jean-Philippe Quéméner secretsManagementAppPlatform,2025-03-19T09:25:14Z,,ac4b2a320071323f0d2849b844f7e8c3afbff21d,Matheus Macabu unifiedStorageGrpcConnectionPool,2025-03-21T13:24:54Z,,ba3e8014b3363762369c434c0c67d871a311a033,Jean-Philippe Quéméner -tableNextGen,2025-03-26T03:57:57Z,2025-08-26T21:25:16Z,03d6d8f854ac06701bba2d5d10bedd8a227bea4e,Drew Slobodnjak +tableNextGen,2025-03-26T03:57:57Z,2025-07-31T22:56:50Z,03d6d8f854ac06701bba2d5d10bedd8a227bea4e,Drew Slobodnjak alertingRuleRecoverDeleted,2025-03-27T14:39:26Z,,f9471ac10b65e08343b08d82e1209f1ae7006955,Sonia Aguilar -localizationForPlugins,2025-03-31T04:38:38Z,,18ae5d7f0c599bf417dc68a67fa6852b6cf54600,Hugo Häggmark +localizationForPlugins,2025-03-31T04:38:38Z,2025-08-29T14:46:39Z,18ae5d7f0c599bf417dc68a67fa6852b6cf54600,Hugo Häggmark localeFormatPreference,2025-03-31T13:59:07Z,,4ad0492d3dc8ee81b388384e3e6c329746f46df7,Laura Fernández useScopesNavigationEndpoint,2025-03-31T15:20:00Z,,c321afdeb710db302558ac63a8f0569c4706d003,Tobias Skarhed xrayApplicationSignals,2025-04-01T14:42:02Z,2025-06-13T18:26:14Z,1aea65f6d5aae0ea9d54a6d943b27e1004f4ef85,Isabella Siu queryServiceFromExplore,2025-04-02T10:00:33Z,,135fbf6258d85382a3c80b696b4e410fd6efe809,Gábor Farkas azureMonitorLogsBuilderEditor,2025-04-02T14:15:25Z,,3b73ebb21096f69f4dcb287896e2cbfec65e48d7,Alyssa (Bull) Joyner multiTenantTempCredentials,2025-04-02T20:25:50Z,,6a699b69bace2aecfe3631a5e050cda69180b97f,Isabella Siu -extensionSidebar,2025-04-03T10:16:35Z,2025-09-01T10:14:17Z,f27790268254294be7c09832e9a73676fd9a629c,Sven Grossmann +extensionSidebar,2025-04-03T10:16:35Z,2025-07-31T22:56:50Z,f27790268254294be7c09832e9a73676fd9a629c,Sven Grossmann alertingRulePermanentlyDelete,2025-04-03T11:18:25Z,,3450d243b95acd5bf86fbbf3b183cba4b2eb8301,Sonia Aguilar logsPanelControls,2025-04-07T14:38:55Z,,e2a6f9a84928cfb2f958522b1f50fdf3051f51de,Matias Chomicki metricsFromProfiles,2025-04-09T10:55:28Z,,ceed8243784500557f2686bb478695cc125a0e19,Piotr Jamróz @@ -475,14 +476,14 @@ pluginsAutoUpdate,2025-04-16T11:44:39Z,,c947732e0de99395a6ee4e0161e27c3e884168ac alertingListViewV2PreviewToggle,2025-04-22T08:50:34Z,,512df0091a1df6c7d19243e288ec458250b49813,Konrad Lalik alertRuleUseFiredAtForStartsAt,2025-04-22T11:16:38Z,,3a054d5e00abc212a741fc1aad8cf266a188de1f,Fayzal Ghantiwala alertingBulkActionsInUI,2025-04-24T14:49:59Z,,674fdd1d323ef041dbed93b44ae58afaaf1a8b64,Sonia Aguilar -multiTenantFrontend,2025-04-25T09:24:25Z,,7b492d7e1610da99460cb121ec542c5b755fe6d1,Ryan McKinley +multiTenantFrontend,2025-04-25T09:24:25Z,2025-07-31T22:56:50Z,7b492d7e1610da99460cb121ec542c5b755fe6d1,Ryan McKinley extensionsReadOnlyProxy,2025-05-06T04:55:23Z,2025-07-01T04:10:57Z,bcb2a7e36f7ae5190ab45669f4a0d04660313842,Levente Balogh kubernetesAggregatorCapTokenAuth,2025-05-15T18:14:23Z,,aa2cf8e398ee05353cc488ca22e3c8ee65dd5c53,Charandas alertingImportYAMLUI,2025-05-21T15:59:41Z,,fc5472615fc8b823f1c18c4eae2b6ce3ddcc76f9,Sonia Aguilar teamHttpHeadersTempo,2025-05-22T19:13:31Z,,249e2f3d34f5175e62a17629646994d3116e64f6,Cory Forseth restoreDashboards,2025-05-23T14:35:54Z,,0cb6f9584bf77a7dbdfcc6ce1032db2b4bdc15e2,Alex Khomenko postgresDSUsePGX,2025-05-26T06:54:18Z,2025-06-03T12:45:07Z,1e383b0c1e98734ad4bf974f0435512d10c33246,Zoltán Bedi -skipTokenRotationIfRecent,2025-06-03T06:59:40Z,,86f2bf294044129e802ddeab6f4ac8e9f79dee9e,xavi +skipTokenRotationIfRecent,2025-06-03T06:59:40Z,2025-10-23T08:02:41Z,86f2bf294044129e802ddeab6f4ac8e9f79dee9e,xavi alertEnrichment,2025-06-06T12:16:07Z,,f81031f945b55b644a3ad6a6bcf99d445beb4ad4,Steve Simpson alertingImportAlertmanagerAPI,2025-06-10T08:32:50Z,,f14ed750f53899d72153defc6c787200c22816ca,Alexander Akhmetov preferLibraryPanelTitle,2025-06-17T11:21:21Z,,e90134bb6f2a9de0ce520f4a00427de2d80c6667,Oscar Kilhed @@ -494,42 +495,93 @@ kubernetesLibraryPanels,2025-06-25T22:21:56Z,,79fe8a9902335c7a28af30e467b904a4cc enableAppChromeExtensions,2025-06-30T04:32:08Z,,15293a2ceb083108c0f15490933db523aa915c7d,Hugo Häggmark foldersAppPlatformAPI,2025-07-03T14:15:23Z,,e76f470b444499f70825e9e5eb6b1775ff086c3c,Andrej Ocenas tempoAlerting,2025-07-15T13:36:36Z,,68b9a5f57c30976a1356a0bdb381821e2643384f,Piotr Jamróz -provisioningSecretsService,2025-07-15T13:43:17Z,2025-08-22T16:38:28Z,d39a47a89b9bfc2940f27ca8469e576a659bfc63,Stephanie Hingtgen +provisioningSecretsService,2025-07-15T13:43:17Z,2025-07-31T22:56:50Z,d39a47a89b9bfc2940f27ca8469e576a659bfc63,Stephanie Hingtgen sharingDashboardImage,2025-07-15T21:07:39Z,,b691b3288d5d18366e405ad4e881e7e9d9d6de96,Nathan Marrs -enablePluginImporter,2025-07-16T04:42:28Z,,5b82e056972f959908cd42c9934ba56e8adbf143,Hugo Häggmark +enablePluginImporter,2025-07-16T04:42:28Z,2025-10-23T04:18:23Z,5b82e056972f959908cd42c9934ba56e8adbf143,Hugo Häggmark otelLogsFormatting,2025-07-16T15:42:14Z,,974103c6fa20701c5467c67fb24f173aea9113fb,Matias Chomicki alertingAIAnalyzeCentralStateHistory,2025-07-16T16:42:42Z,,9c15662cf6337b37f2932cf7b20fe5e05d310153,Sonia Aguilar alertingAIGenAlertRules,2025-07-16T16:42:42Z,,9c15662cf6337b37f2932cf7b20fe5e05d310153,Sonia Aguilar alertingAIGenTemplates,2025-07-16T16:42:42Z,,9c15662cf6337b37f2932cf7b20fe5e05d310153,Sonia Aguilar alertingAIImproveAlertRules,2025-07-16T16:42:42Z,,9c15662cf6337b37f2932cf7b20fe5e05d310153,Sonia Aguilar alertingNotificationHistory,2025-07-17T13:26:26Z,,bccc980b902987fc9a2707deca766412b47bbe2a,Vadim Stepanov -pluginAssetProvider,2025-07-17T15:20:35Z,,f6ed9e6ff0e799f3e69532809a7e444efb3c610e,Will Browne +pluginAssetProvider,2025-07-17T15:20:35Z,2025-10-10T09:35:22Z,f6ed9e6ff0e799f3e69532809a7e444efb3c610e,Will Browne unifiedStorageSearchDualReaderEnabled,2025-07-18T12:43:56Z,,2dba473015a6e7e75a5d117bf766a6c24d404d38,maicon kubernetesLibraryPanelConnections,2025-07-21T12:53:46Z,2025-07-30T16:46:19Z,5ec3a2b758dfd0cc63cc42759d6f01a88dfbb6bc,Stephanie Hingtgen -dashboardDsAdHocFiltering,2025-07-23T08:12:25Z,,aedd7b6e3408a04a84ef02c535439fd17aa2e0e6,Sam Jewell +dashboardDsAdHocFiltering,2025-07-23T08:12:25Z,2025-11-10T17:17:49Z,aedd7b6e3408a04a84ef02c535439fd17aa2e0e6,Sam Jewell alertingAIFeedback,2025-07-23T12:38:09Z,,7c872f0e8aa6d05061f546da98d94fe223192fa2,Sonia Aguilar alertingProvenanceLockWrites,2025-07-23T18:16:06Z,,7c43c061a827e4802f40d74f451a15a24db6abd6,Alexander Akhmetov sqlExpressionsColumnAutoComplete,2025-07-23T21:49:58Z,,5bfed408edcc6f74dd8b0780b4f87bccfea02b01,Kristina timeComparison,2025-07-24T20:07:28Z,,219672722662938633ffae3f6e8bfa89f8f29661,Drew Slobodnjak kubernetesAuthnMutation,2025-07-25T15:05:32Z,,5f6fc38430494e3cb642a09e32415ae62a92ad56,Victor Cinaglia alertmanagerRemoteSecondaryWithRemoteState,2025-07-25T15:06:59Z,,dcb965b7dcf1ec47d0847968fa362d2b308328f8,Santiago -adhocFiltersInTooltips,2025-07-29T17:53:43Z,,2174a84b36b9a086018972654c5ec661c19937a1,Sam Jewell +adhocFiltersInTooltips,2025-07-29T17:53:43Z,2025-11-12T10:05:30Z,2174a84b36b9a086018972654c5ec661c19937a1,Sam Jewell scanRowInvalidDashboardParseFallbackEnabled,2025-07-30T14:18:38Z,,98e37f2ca9b4fa1a6cac0c3a2ae355a5ff60dbd9,Mustafa Sencer Özcan dashboardLevelTimeMacros,2025-07-31T09:49:07Z,,e7cfe0c0237394f6c87d01ad4a5e6f29954212fb,Oscar Kilhed useScopeSingleNodeEndpoint,2025-07-31T14:32:41Z,,972e2f31e5f3dfd8d9f8152b3bb1fe7224e5d1e1,Tobias Skarhed -newLogContext,2025-08-01T11:30:17Z,,3f90c85c4eea9657bac14b49c925ba32d208c54e,Matias Chomicki -kubernetesShortURLs,2025-08-04T12:12:12Z,,e88b54e9d3412508342809fd0bb85f836cd50d73,Ezequiel Victorero -newClickhouseConfigPageDesign,2025-08-05T13:37:28Z,,23b801470889c9c99a02f32e819b4a45d5d67189,Alyssa Joyner -favoriteDatasources,2025-08-08T13:28:17Z,,463e544db9812ef6bf02dfe764585b21a00c172c,Andres Martinez Gotor -kubernetesAuthzResourcePermissionApis,2025-08-11T08:54:36Z,,58c4305d64a8e1d33d4f10f0cb2e4e2c318c2fac,Ieva -unifiedStorageSearchAfterWriteExperimentalAPI,2025-08-13T14:05:15Z,,b9b34223a7e8cd40a496c0d4a2c8386f46136ca7,Will Assis -alertingImportAlertmanagerUI,2025-08-13T15:28:43Z,,587f52cf5b8c480cfe63490a1e77907faa1a263a,Alexander Akhmetov -teamFolders,2025-08-13T16:41:00Z,,5564f699ca45fcbffcf67ce2575ff40a34e263cd,Tom Ratcliffe -grafanaAssistantInProfilesDrilldown,2025-08-19T07:54:00Z,,bbf01a638345e42c0f48dfcc743efbfbb7fedaeb,Piotr Jamróz -savedQueries,2025-08-25T21:22:09Z,,649e9aa8ca9e8f7f16e9198b3858aaf12714a6a8,Ezequiel Victorero -alertingEnrichmentPerRule,2025-08-28T08:30:28Z,,98bd10965b4be99d1b1306a2ddf87b1099df74dd,Sonia Aguilar +alertEnrichmentConditional,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +alertEnrichmentMultiStep,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +alertingEnrichmentPerRule,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +alertingImportAlertmanagerUI,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +alertingTriage,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +azureResourcePickerUpdates,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +dskitBackgroundServices,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +favoriteDatasources,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +grafanaAssistantInProfilesDrilldown,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +graphiteBackendMode,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +kubernetesAlertingRules,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +kubernetesAuthzResourcePermissionApis,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +kubernetesShortURLs,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +newClickhouseConfigPageDesign,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +newLogContext,2025-07-31T22:56:50Z,,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +pluginContainers,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +prometheusTypeMigration,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +restrictedPluginApis,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +savedQueries,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +secretsManagementAppPlatformUI,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +teamFolders,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +unifiedStorageSearchAfterWriteExperimentalAPI,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou +vizActionsAuth,2025-07-31T22:56:50Z,2025-08-01T11:30:17Z,ca8324e62a3ed855e9e68c7a5e358a16d48edc8f,Moustafa Baiou queryServiceWithConnections,2025-08-28T19:28:26Z,2025-08-29T12:49:57Z,eda94a6434efc84862f9907c40463c02460c400d,Ryan McKinley -alertingTriage,2025-09-01T09:33:33Z,,31114fb47ced7afbd559afe00a49f40914cd7acb,Konrad Lalik -restrictedPluginApis,2025-09-01T09:57:00Z,,d31e682345c5a4a7b3e055a481996d406329e1fa,Levente Balogh -graphiteBackendMode,2025-09-01T15:13:47Z,,0dc283b303a4b0b992e39b9d72c71f1f3a1ea597,Andreas Christou -azureResourcePickerUpdates,2025-09-02T10:02:01Z,,1a8d25375a6cd8adc3f64558b08ca9fd2ae9782d,Andreas Christou +alertingEnrichmentAssistantInvestigations,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +alertingUseNewSimplifiedRoutingHashAlgorithm,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +cdnPluginsLoadFirst,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +cdnPluginsUrls,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +dashboardLibrary,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +dashboardUndoRedo,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +filterOutBotsFromFrontendLogs,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +kubernetesAuthZHandlerRedirect,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +kubernetesCorrelations,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +kubernetesStars,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +queryCacheRequestDeduplication,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +starsFromAPIServer,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +tempoSearchBackendMigration,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +unifiedStorageUseFullNgram,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +useKubernetesShortURLsAPI,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +useMultipleScopeNodesEndpoint,2025-08-29T14:46:39Z,2025-09-01T09:33:33Z,c3f34efb41749d50b76ff0d765739e8c7b5c4da0,beejeebus +alertingGenerateSimplifiedRoutingWithOldHashes,2025-10-01T16:45:36Z,2025-10-01T19:21:33Z,0da9d4989652218d73ea9aa176ed300dba9aa884,Alexander Akhmetov +unlimitedLayoutsNesting,2025-10-10T12:15:54Z,,b5613b37249f42e18ef62f987202ba43858e5992,Sergej-Vlasov +enableDashboardEmptyExtensions,2025-10-13T07:03:13Z,,85174e3313952f522e1325b17a4e34c898d8617e,Matt Cowley +kubernetesAuthzZanzanaSync,2025-10-13T19:37:13Z,,0e341643292d903bc5fc882aa3332966284958ff,Gabriel MABILLE +grafanaPathfinder,2025-10-14T10:40:40Z,2025-10-22T08:06:21Z,c9f402c764d41a7ff85ef26979f8b4fcbe1b4b81,Jay Clifford +kubernetesLogsDrilldown,2025-10-16T21:31:42Z,,b3f9dad044f65479f99bb174af5725a8863ec915,Liza Detrick +preventPanelChromeOverflow,2025-10-17T14:40:08Z,,4cf11b721aeade73b37551382f5202ecef94d49a,Ashley Harrison +pluginStoreServiceLoading,2025-10-17T20:01:43Z,,69628baa9ddde2851dcccfe6c3c7d8c3e8b3197b,Todd Treece +onlyStoreActionSets,2025-10-20T15:02:56Z,,0a0311a2b230991a55d5b2d393d1cef0326e26f9,Ieva +kubernetesQueryCaching,2025-10-20T16:11:25Z,,17771e0e1df9d70ef8a2f90a7468a1652c462aa1,Lucy Chen +newGauge,2025-10-20T16:33:19Z,,7df537c9fc84ad6d277cee249e87db08bc6a81b7,Torkel Ödegaard +zanzanaNoLegacyClient,2025-10-21T14:03:17Z,,adf1224e82d83277e169646db477462a9a4db8e2,Alexander Zobnin +interactiveLearning,2025-10-22T08:06:21Z,,1abb5aa0f9c77d18775377a7459c091a9adddaa3,Jack Baldry +pluginInstallAPISync,2025-10-24T12:09:26Z,,dc77da11cf4cb3c4e705477cf2fb97e6004daf2f,Todd Treece +dashboardTemplates,2025-10-28T20:05:32Z,,8263803e81f26bdce676c5676b7cc4838cdf9042,Nathan Marrs +panelTimeSettings,2025-10-29T08:06:23Z,,5a031b370f4eeb873ed305c11a0235017fd54307,Torkel Ödegaard +jaegerEnableGrpcEndpoint,2025-10-31T18:19:16Z,,d0ea82633f757dcaa2562e94b2de0b40f9c0e229,Jocelyn Collado-Kuri +timeRangePan,2025-11-05T01:39:46Z,,e067b1de98f564045dfbb726546ef06957eeb3a6,Jesse David Peterson +kubernetesAnnotations,2025-11-06T18:22:20Z,,95ea7584752ddeb2aa224d2d8d9aba2de81f5aa3,Serge Zaitsev +suggestedDashboards,2025-11-07T13:38:59Z,,e5ed003fb219d4b8ffa2c2a44abda4e6ff54d91a,Alexa Vargas +grafanaAdvisorAppInstaller,2025-11-12T14:32:21Z,2025-11-14T11:25:30Z,d83c35fd71f435dc0fdc83b6af43dd7bc3682d22,Andres Martinez Gotor +newPanelPadding,2025-11-12T15:40:46Z,,1f558b1e066ed124c408ed52e95ce60bccfc135b,Torkel Ödegaard +awsDatasourcesHttpProxy,2025-11-12T18:51:23Z,,ec9f39d54a0489ca36a9059ad1b31ab7a58ca20a,Isabella Siu +newVizSuggestions,2025-11-12T19:26:29Z,,a194219365400e6dd5929453b17d212d1efd386c,Adela Almasan +alertingUIUseBackendFilters,2025-11-13T14:52:14Z,,44a92d252b18a571f5858aa7a7519f365d89ad7b,Alexander Akhmetov +transformationsEmptyPlaceholder,2025-11-17T13:57:05Z,,c4f2f3f6f4038a7428157a3629b428c340c08b52,Natalia Bernarte Oses +ttlPluginInstanceManager,2025-11-18T11:17:23Z,,e00eb854f54c972286a39d8ed101f1a423084f06,Will Browne diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 8a874135d00..24f9769ea4f 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -28,7 +28,7 @@ unifiedRequestLog,GA,@grafana/grafana-backend-group,false,false,false renderAuthJWT,preview,@grafana/grafana-operator-experience-squad,false,false,false refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false faroDatasourceSelector,preview,@grafana/app-o11y,false,false,true -enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,true +enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,false extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,true logsExploreTableVisualisation,GA,@grafana/observability-logs,false,false,true awsDatasourcesTempCredentials,GA,@grafana/aws-datasources,false,false,false @@ -51,7 +51,7 @@ enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,f disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesLibraryPanels,experimental,@grafana/grafana-app-platform-squad,false,true,false -kubernetesDashboards,GA,@grafana/dashboards-squad,false,false,true +kubernetesDashboards,GA,@grafana/dashboards-squad,false,false,false kubernetesShortURLs,experimental,@grafana/grafana-app-platform-squad,false,true,false useKubernetesShortURLsAPI,experimental,@grafana/sharing-squad,false,false,true kubernetesAlertingRules,experimental,@grafana/alerting-squad,false,true,false @@ -78,7 +78,7 @@ annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true dashboardScene,GA,@grafana/dashboards-squad,false,false,true -dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,true +dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,false dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true @@ -119,7 +119,7 @@ logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,fa alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false -alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,true +alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,false pluginProxyPreserveTrailingSlash,GA,@grafana/plugins-platform-backend,false,false,false azureMonitorPrometheusExemplars,GA,@grafana/partner-datasources,false,false,false authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false @@ -200,14 +200,14 @@ azureMonitorLogsBuilderEditor,preview,@grafana/partner-datasources,false,false,f localeFormatPreference,preview,@grafana/grafana-frontend-platform,false,false,false unifiedStorageGrpcConnectionPool,experimental,@grafana/search-and-storage,false,false,false alertingRulePermanentlyDelete,GA,@grafana/alerting-squad,false,false,true -alertingRuleRecoverDeleted,GA,@grafana/alerting-squad,false,false,true +alertingRuleRecoverDeleted,GA,@grafana/alerting-squad,false,false,false multiTenantTempCredentials,experimental,@grafana/aws-datasources,false,false,false unifiedNavbars,GA,@grafana/plugins-platform-backend,false,false,true logsPanelControls,preview,@grafana/observability-logs,false,false,true metricsFromProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,true grafanaAssistantInProfilesDrilldown,GA,@grafana/observability-traces-and-profiling,false,false,true postgresDSUsePGX,experimental,@grafana/oss-big-tent,false,false,false -tempoAlerting,experimental,@grafana/observability-traces-and-profiling,false,false,true +tempoAlerting,experimental,@grafana/observability-traces-and-profiling,false,false,false pluginsAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,false @@ -241,7 +241,7 @@ newLogContext,experimental,@grafana/observability-logs,false,false,true newClickhouseConfigPageDesign,privatePreview,@grafana/partner-datasources,false,false,false teamFolders,experimental,@grafana/grafana-search-navigate-organise,false,false,false interactiveLearning,preview,@grafana/pathfinder,false,false,false -alertingTriage,experimental,@grafana/alerting-squad,false,false,true +alertingTriage,experimental,@grafana/alerting-squad,false,false,false graphiteBackendMode,privatePreview,@grafana/partner-datasources,false,false,false azureResourcePickerUpdates,preview,@grafana/partner-datasources,false,false,true prometheusTypeMigration,experimental,@grafana/partner-datasources,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index f6c70624e8d..fdc48861894 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -19,10 +19,6 @@ const ( // Enables public dashboard sharing to be restricted to only allowed emails FlagPublicDashboardsEmailSharing = "publicDashboardsEmailSharing" - // FlagPublicDashboardsScene - // Enables public dashboard rendering using scenes - FlagPublicDashboardsScene = "publicDashboardsScene" - // FlagLokiExperimentalStreaming // Support new streaming approach for loki (prototype, needs special loki build) FlagLokiExperimentalStreaming = "lokiExperimentalStreaming" @@ -35,10 +31,6 @@ const ( // Configurable storage for dashboards, datasources, and resources FlagStorage = "storage" - // FlagCanvasPanelNesting - // Allow elements nesting - FlagCanvasPanelNesting = "canvasPanelNesting" - // FlagLogRequestsInstrumentedAsUnknown // Logs the path for requests that are instrumented as unknown FlagLogRequestsInstrumentedAsUnknown = "logRequestsInstrumentedAsUnknown" @@ -63,30 +55,10 @@ const ( // Rule backtesting API for alerting FlagAlertingBacktesting = "alertingBacktesting" - // FlagLogsContextDatasourceUi - // Allow datasource to provide custom UI for context view - FlagLogsContextDatasourceUi = "logsContextDatasourceUi" - - // FlagLokiShardSplitting - // Use stream shards to split queries into smaller subqueries - FlagLokiShardSplitting = "lokiShardSplitting" - - // FlagLokiQuerySplitting - // Split large interval queries into subqueries with smaller time intervals - FlagLokiQuerySplitting = "lokiQuerySplitting" - // FlagIndividualCookiePreferences // Support overriding cookie preferences per user FlagIndividualCookiePreferences = "individualCookiePreferences" - // FlagInfluxdbBackendMigration - // Query InfluxDB InfluxQL without the proxy - FlagInfluxdbBackendMigration = "influxdbBackendMigration" - - // FlagStarsFromAPIServer - // populate star status from apiserver - FlagStarsFromAPIServer = "starsFromAPIServer" - // FlagKubernetesStars // Routes stars requests from /api to the /apis endpoint FlagKubernetesStars = "kubernetesStars" @@ -119,22 +91,10 @@ const ( // Refactor time range variables flow to reduce number of API calls made when query variables are chained FlagRefactorVariablesTimeRange = "refactorVariablesTimeRange" - // FlagFaroDatasourceSelector - // Enable the data source selector within the Frontend Apps section of the Frontend Observability - FlagFaroDatasourceSelector = "faroDatasourceSelector" - // FlagEnableDatagridEditing // Enables the edit functionality in the datagrid panel FlagEnableDatagridEditing = "enableDatagridEditing" - // FlagExtraThemes - // Enables extra themes - FlagExtraThemes = "extraThemes" - - // FlagLogsExploreTableVisualisation - // A table visualisation for logs in Explore - FlagLogsExploreTableVisualisation = "logsExploreTableVisualisation" - // FlagAwsDatasourcesTempCredentials // Support temporary security credentials in AWS plugins for Grafana Cloud customers FlagAwsDatasourcesTempCredentials = "awsDatasourcesTempCredentials" @@ -175,14 +135,6 @@ const ( // Enable changing the scheduler base interval via configuration option unified_alerting.scheduler_tick_interval FlagConfigurableSchedulerTick = "configurableSchedulerTick" - // FlagDashgpt - // Enable AI powered features in dashboards - FlagDashgpt = "dashgpt" - - // FlagAiGeneratedDashboardChanges - // Enable AI powered features for dashboards to auto-summary changes when saving - FlagAiGeneratedDashboardChanges = "aiGeneratedDashboardChanges" - // FlagReportingRetries // Enables rendering retries for the reporting feature FlagReportingRetries = "reportingRetries" @@ -223,10 +175,6 @@ const ( // Enables k8s short url api and uses it under the hood when handling legacy /api FlagKubernetesShortURLs = "kubernetesShortURLs" - // FlagUseKubernetesShortURLsAPI - // Routes short url requests from /api to the /apis endpoint in the frontend. Depends on kubernetesShortURLs - FlagUseKubernetesShortURLsAPI = "useKubernetesShortURLsAPI" - // FlagKubernetesAlertingRules // Adds support for Kubernetes alerting and recording rules FlagKubernetesAlertingRules = "kubernetesAlertingRules" @@ -275,14 +223,6 @@ const ( // Rewrite requests targeting /ds/query to the query service FlagQueryServiceRewrite = "queryServiceRewrite" - // FlagQueryServiceFromUI - // Routes requests to the new query service - FlagQueryServiceFromUI = "queryServiceFromUI" - - // FlagQueryServiceFromExplore - // Routes explore requests to the new query service - FlagQueryServiceFromExplore = "queryServiceFromExplore" - // FlagCloudWatchBatchQueries // Runs CloudWatch metrics queries as separate batches FlagCloudWatchBatchQueries = "cloudWatchBatchQueries" @@ -311,58 +251,14 @@ const ( // Change the way annotation permissions work by scoping them to folders and dashboards. FlagAnnotationPermissionUpdate = "annotationPermissionUpdate" - // FlagDashboardSceneForViewers - // Enables dashboard rendering using Scenes for viewer roles - FlagDashboardSceneForViewers = "dashboardSceneForViewers" - - // FlagDashboardSceneSolo - // Enables rendering dashboards using scenes for solo panels - FlagDashboardSceneSolo = "dashboardSceneSolo" - - // FlagDashboardScene - // Enables dashboard rendering using scenes for all roles - FlagDashboardScene = "dashboardScene" - // FlagDashboardNewLayouts // Enables experimental new dashboard layouts FlagDashboardNewLayouts = "dashboardNewLayouts" - // FlagDashboardUndoRedo - // Enables undo/redo in dynamic dashboards - FlagDashboardUndoRedo = "dashboardUndoRedo" - - // FlagUnlimitedLayoutsNesting - // Enables unlimited dashboard panel grouping - FlagUnlimitedLayoutsNesting = "unlimitedLayoutsNesting" - - // FlagPanelFilterVariable - // Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard - FlagPanelFilterVariable = "panelFilterVariable" - // FlagPdfTables // Enables generating table data as PDF in reporting FlagPdfTables = "pdfTables" - // FlagCanvasPanelPanZoom - // Allow pan and zoom in canvas panel - FlagCanvasPanelPanZoom = "canvasPanelPanZoom" - - // FlagTimeComparison - // Enables time comparison option in supported panels - FlagTimeComparison = "timeComparison" - - // FlagLogsInfiniteScrolling - // Enables infinite scrolling for the Logs panel in Explore and Dashboards - FlagLogsInfiniteScrolling = "logsInfiniteScrolling" - - // FlagTableSharedCrosshair - // Enables shared crosshair in table panel - FlagTableSharedCrosshair = "tableSharedCrosshair" - - // FlagKubernetesFeatureToggles - // Use the kubernetes API for feature toggle management in the frontend - FlagKubernetesFeatureToggles = "kubernetesFeatureToggles" - // FlagCloudRBACRoles // Enabled grafana cloud specific RBAC roles FlagCloudRBACRoles = "cloudRBACRoles" @@ -399,14 +295,6 @@ const ( // In-development feature flag for the scope api using the app platform. FlagScopeApi = "scopeApi" - // FlagUseScopeSingleNodeEndpoint - // Use the single node endpoint for the scope api. This is used to fetch the scope parent node. - FlagUseScopeSingleNodeEndpoint = "useScopeSingleNodeEndpoint" - - // FlagUseMultipleScopeNodesEndpoint - // Makes the frontend use the 'names' param for fetching multiple scope nodes at once - FlagUseMultipleScopeNodesEndpoint = "useMultipleScopeNodesEndpoint" - // FlagLogQLScope // In-development feature that will allow injection of labels into loki queries. FlagLogQLScope = "logQLScope" @@ -415,10 +303,6 @@ const ( // Enables SQL Expressions, which can execute SQL queries against data source results. FlagSqlExpressions = "sqlExpressions" - // FlagSqlExpressionsColumnAutoComplete - // Enables column autocomplete for SQL Expressions - FlagSqlExpressionsColumnAutoComplete = "sqlExpressionsColumnAutoComplete" - // FlagKubernetesAggregator // Enable grafana's embedded kube-aggregator FlagKubernetesAggregator = "kubernetesAggregator" @@ -471,14 +355,6 @@ const ( // Enable suggested dashboards when creating new dashboards FlagSuggestedDashboards = "suggestedDashboards" - // FlagLogsExploreTableDefaultVisualization - // Sets the logs table as default visualisation in logs explore - FlagLogsExploreTableDefaultVisualization = "logsExploreTableDefaultVisualization" - - // FlagAlertingListViewV2 - // Enables the new alert list view design - FlagAlertingListViewV2 = "alertingListViewV2" - // FlagAlertingDisableSendAlertsExternal // Disables the ability to send alerts to an external Alertmanager datasource. FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal" @@ -543,26 +419,6 @@ const ( // Enables new combobox style UI for the Ad hoc filters variable in scenes architecture FlagNewFiltersUI = "newFiltersUI" - // FlagVizActionsAuth - // Allows authenticated API calls in actions - FlagVizActionsAuth = "vizActionsAuth" - - // FlagAlertingPrometheusRulesPrimary - // Uses Prometheus rules as the primary source of truth for ruler-enabled data sources - FlagAlertingPrometheusRulesPrimary = "alertingPrometheusRulesPrimary" - - // FlagExploreLogsShardSplitting - // Used in Logs Drilldown to split queries into multiple queries based on the number of shards - FlagExploreLogsShardSplitting = "exploreLogsShardSplitting" - - // FlagExploreLogsAggregatedMetrics - // Used in Logs Drilldown to query by aggregated metrics - FlagExploreLogsAggregatedMetrics = "exploreLogsAggregatedMetrics" - - // FlagExploreLogsLimitedTimeRange - // Used in Logs Drilldown to limit the time range - FlagExploreLogsLimitedTimeRange = "exploreLogsLimitedTimeRange" - // FlagAppPlatformGrpcClientAuth // Enables the gRPC client to authenticate with the App Platform by using ID & access tokens FlagAppPlatformGrpcClientAuth = "appPlatformGrpcClientAuth" @@ -571,10 +427,6 @@ const ( // Enable the groupsync extension for managing Group Attribute Sync feature FlagGroupAttributeSync = "groupAttributeSync" - // FlagAlertingQueryAndExpressionsStepMode - // Enables step mode for alerting queries and expressions - FlagAlertingQueryAndExpressionsStepMode = "alertingQueryAndExpressionsStepMode" - // FlagImprovedExternalSessionHandling // Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves. FlagImprovedExternalSessionHandling = "improvedExternalSessionHandling" @@ -611,10 +463,6 @@ const ( // Enables time pickers sync FlagTimeRangeProvider = "timeRangeProvider" - // FlagTimeRangePan - // Enables time range panning functionality - FlagTimeRangePan = "timeRangePan" - // FlagAzureMonitorDisableLogLimit // Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. FlagAzureMonitorDisableLogLimit = "azureMonitorDisableLogLimit" @@ -627,14 +475,6 @@ const ( // Enable passwordless login via magic link authentication FlagPasswordlessMagicLinkAuthentication = "passwordlessMagicLinkAuthentication" - // FlagExploreMetricsRelatedLogs - // Display Related Logs in Grafana Metrics Drilldown - FlagExploreMetricsRelatedLogs = "exploreMetricsRelatedLogs" - - // FlagPrometheusSpecialCharsInLabelValues - // Adds support for quotes and special characters in label values for Prometheus queries - FlagPrometheusSpecialCharsInLabelValues = "prometheusSpecialCharsInLabelValues" - // FlagEnableExtensionsAdminPage // Enables the extension admin page regardless of development mode FlagEnableExtensionsAdminPage = "enableExtensionsAdminPage" @@ -643,14 +483,6 @@ const ( // Enables SCIM support for user and group management FlagEnableSCIM = "enableSCIM" - // FlagCrashDetection - // Enables browser crash detection reporting to Faro. - FlagCrashDetection = "crashDetection" - - // FlagAlertingUIOptimizeReducer - // Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query - FlagAlertingUIOptimizeReducer = "alertingUIOptimizeReducer" - // FlagAzureMonitorEnableUserAuth // Enables user auth for Azure Monitor datasource only FlagAzureMonitorEnableUserAuth = "azureMonitorEnableUserAuth" @@ -683,10 +515,6 @@ const ( // Enable AI-analyze central state history. FlagAlertingAIAnalyzeCentralStateHistory = "alertingAIAnalyzeCentralStateHistory" - // FlagAlertingNotificationsStepMode - // Enables simplified step mode in the notifications section - FlagAlertingNotificationsStepMode = "alertingNotificationsStepMode" - // FlagUnifiedStorageSearchUI // Enable unified storage search UI FlagUnifiedStorageSearchUI = "unifiedStorageSearchUI" @@ -695,10 +523,6 @@ const ( // Enables cross cluster search in the Elasticsearch data source FlagElasticsearchCrossClusterSearch = "elasticsearchCrossClusterSearch" - // FlagUnifiedHistory - // Displays the navigation history so the user can navigate back to previous pages - FlagUnifiedHistory = "unifiedHistory" - // FlagLokiLabelNamesQueryApi // Defaults to using the Loki `/labels` API instead of `/series` FlagLokiLabelNamesQueryApi = "lokiLabelNamesQueryApi" @@ -731,53 +555,25 @@ const ( // Enables less memory intensive Elasticsearch result parsing FlagElasticsearchImprovedParsing = "elasticsearchImprovedParsing" - // FlagDatasourceConnectionsTab - // Shows defined connections for a data source in the plugins detail page - FlagDatasourceConnectionsTab = "datasourceConnectionsTab" - // FlagFetchRulesUsingPost // Use a POST request to list rules by passing down the namespaces user has access to FlagFetchRulesUsingPost = "fetchRulesUsingPost" - // FlagNewLogsPanel - // Enables the new logs panel - FlagNewLogsPanel = "newLogsPanel" - // FlagGrafanaconThemes // Enables the temporary themes for GrafanaCon FlagGrafanaconThemes = "grafanaconThemes" - // FlagAlertingJiraIntegration - // Enables the new Jira integration for contact points in cloud alert managers. - FlagAlertingJiraIntegration = "alertingJiraIntegration" - // FlagAlertingUseNewSimplifiedRoutingHashAlgorithm FlagAlertingUseNewSimplifiedRoutingHashAlgorithm = "alertingUseNewSimplifiedRoutingHashAlgorithm" - // FlagUseScopesNavigationEndpoint - // Use the scopes navigation endpoint instead of the dashboardbindings endpoint - FlagUseScopesNavigationEndpoint = "useScopesNavigationEndpoint" - // FlagScopeSearchAllLevels // Enable scope search to include all levels of the scope node tree FlagScopeSearchAllLevels = "scopeSearchAllLevels" - // FlagAlertingRuleVersionHistoryRestore - // Enables the alert rule version history restore feature - FlagAlertingRuleVersionHistoryRestore = "alertingRuleVersionHistoryRestore" - // FlagNewShareReportDrawer // Enables the report creation drawer in a dashboard FlagNewShareReportDrawer = "newShareReportDrawer" - // FlagRendererDisableAppPluginsPreload - // Disable pre-loading app plugins when the request is coming from the renderer - FlagRendererDisableAppPluginsPreload = "rendererDisableAppPluginsPreload" - - // FlagAssetSriChecks - // Enables SRI checks for Grafana JavaScript assets - FlagAssetSriChecks = "assetSriChecks" - // FlagAlertRuleRestore // Enables the alert rule restore feature FlagAlertRuleRestore = "alertRuleRestore" @@ -786,14 +582,6 @@ const ( // Enables running Infinity queries in parallel FlagInfinityRunQueriesInParallel = "infinityRunQueriesInParallel" - // FlagAlertingMigrationUI - // Enables the alerting migration UI, to migrate data source-managed rules to Grafana-managed rules - FlagAlertingMigrationUI = "alertingMigrationUI" - - // FlagAlertingImportYAMLUI - // Enables a UI feature for importing rules from a Prometheus file to Grafana-managed rules - FlagAlertingImportYAMLUI = "alertingImportYAMLUI" - // FlagAzureMonitorLogsBuilderEditor // Enables the logs builder mode for the Azure Monitor data source FlagAzureMonitorLogsBuilderEditor = "azureMonitorLogsBuilderEditor" @@ -806,10 +594,6 @@ const ( // Enables the unified storage grpc connection pool FlagUnifiedStorageGrpcConnectionPool = "unifiedStorageGrpcConnectionPool" - // FlagAlertingRulePermanentlyDelete - // Enables UI functionality to permanently delete alert rules - FlagAlertingRulePermanentlyDelete = "alertingRulePermanentlyDelete" - // FlagAlertingRuleRecoverDeleted // Enables the UI functionality to recover and view deleted alert rules FlagAlertingRuleRecoverDeleted = "alertingRuleRecoverDeleted" @@ -818,22 +602,6 @@ const ( // use multi-tenant path for awsTempCredentials FlagMultiTenantTempCredentials = "multiTenantTempCredentials" - // FlagUnifiedNavbars - // Enables unified navbars - FlagUnifiedNavbars = "unifiedNavbars" - - // FlagLogsPanelControls - // Enables a control component for the logs panel in Explore - FlagLogsPanelControls = "logsPanelControls" - - // FlagMetricsFromProfiles - // Enables creating metrics from profiles and storing them as recording rules - FlagMetricsFromProfiles = "metricsFromProfiles" - - // FlagGrafanaAssistantInProfilesDrilldown - // Enables integration with Grafana Assistant in Profiles Drilldown - FlagGrafanaAssistantInProfilesDrilldown = "grafanaAssistantInProfilesDrilldown" - // FlagPostgresDSUsePGX // Enables using PGX instead of libpq for PostgreSQL datasource FlagPostgresDSUsePGX = "postgresDSUsePGX" @@ -846,18 +614,10 @@ const ( // Enables auto-updating of users installed plugins FlagPluginsAutoUpdate = "pluginsAutoUpdate" - // FlagAlertingListViewV2PreviewToggle - // Enables the alerting list view v2 preview toggle - FlagAlertingListViewV2PreviewToggle = "alertingListViewV2PreviewToggle" - // FlagAlertRuleUseFiredAtForStartsAt // Use FiredAt for StartsAt when sending alerts to Alertmaanger FlagAlertRuleUseFiredAtForStartsAt = "alertRuleUseFiredAtForStartsAt" - // FlagAlertingBulkActionsInUI - // Enables the alerting bulk actions in the UI - FlagAlertingBulkActionsInUI = "alertingBulkActionsInUI" - // FlagKubernetesAuthzApis // Registers AuthZ /apis endpoint FlagKubernetesAuthzApis = "kubernetesAuthzApis" @@ -902,10 +662,6 @@ const ( // Enables the UI to see imported Alertmanager configuration FlagAlertingImportAlertmanagerUI = "alertingImportAlertmanagerUI" - // FlagSharingDashboardImage - // Enables image sharing functionality for dashboards - FlagSharingDashboardImage = "sharingDashboardImage" - // FlagPreferLibraryPanelTitle // Prefer library panel title over viz panel title. FlagPreferLibraryPanelTitle = "preferLibraryPanelTitle" @@ -918,22 +674,6 @@ const ( // Enables new design for the InfluxDB data source configuration page FlagNewInfluxDSConfigPageDesign = "newInfluxDSConfigPageDesign" - // FlagEnableAppChromeExtensions - // Set this to true to enable all app chrome extensions registered by plugins. - FlagEnableAppChromeExtensions = "enableAppChromeExtensions" - - // FlagEnableDashboardEmptyExtensions - // Set this to true to enable all dashboard empty state extensions registered by plugins. - FlagEnableDashboardEmptyExtensions = "enableDashboardEmptyExtensions" - - // FlagFoldersAppPlatformAPI - // Enables use of app platform API for folders - FlagFoldersAppPlatformAPI = "foldersAppPlatformAPI" - - // FlagOtelLogsFormatting - // Applies OTel formatting templates to displayed logs - FlagOtelLogsFormatting = "otelLogsFormatting" - // FlagAlertingNotificationHistory // Enables the notification history feature FlagAlertingNotificationHistory = "alertingNotificationHistory" @@ -942,26 +682,10 @@ const ( // Enable dual reader for unified storage search FlagUnifiedStorageSearchDualReaderEnabled = "unifiedStorageSearchDualReaderEnabled" - // FlagDashboardLevelTimeMacros - // Supports __from and __to macros that always use the dashboard level time range - FlagDashboardLevelTimeMacros = "dashboardLevelTimeMacros" - // FlagAlertmanagerRemoteSecondaryWithRemoteState // Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications. FlagAlertmanagerRemoteSecondaryWithRemoteState = "alertmanagerRemoteSecondaryWithRemoteState" - // FlagRestrictedPluginApis - // Enables sharing a list of APIs with a list of plugins - FlagRestrictedPluginApis = "restrictedPluginApis" - - // FlagFavoriteDatasources - // Enable favorite datasources - FlagFavoriteDatasources = "favoriteDatasources" - - // FlagNewLogContext - // New Log Context component - FlagNewLogContext = "newLogContext" - // FlagNewClickhouseConfigPageDesign // Enables new design for the Clickhouse data source configuration page FlagNewClickhouseConfigPageDesign = "newClickhouseConfigPageDesign" @@ -982,10 +706,6 @@ const ( // Enables the Graphite data source full backend mode FlagGraphiteBackendMode = "graphiteBackendMode" - // FlagAzureResourcePickerUpdates - // Enables the updated Azure Monitor resource picker - FlagAzureResourcePickerUpdates = "azureResourcePickerUpdates" - // FlagPrometheusTypeMigration // Checks for deprecated Prometheus authentication methods (SigV4 and Azure), installs the relevant data source, and migrates the Prometheus data sources FlagPrometheusTypeMigration = "prometheusTypeMigration" @@ -1010,18 +730,6 @@ const ( // Enable syncing plugin installations to the installs API FlagPluginInstallAPISync = "pluginInstallAPISync" - // FlagNewGauge - // Enable new gauge visualization - FlagNewGauge = "newGauge" - - // FlagNewVizSuggestions - // Enable new visualization suggestions - FlagNewVizSuggestions = "newVizSuggestions" - - // FlagPreventPanelChromeOverflow - // Restrict PanelChrome contents with overflow: hidden; - FlagPreventPanelChromeOverflow = "preventPanelChromeOverflow" - // FlagJaegerEnableGrpcEndpoint // Enable querying trace data through Jaeger's gRPC endpoint (HTTP) FlagJaegerEnableGrpcEndpoint = "jaegerEnableGrpcEndpoint" @@ -1053,16 +761,4 @@ const ( // FlagAwsDatasourcesHttpProxy // Enables http proxy settings for aws datasources FlagAwsDatasourcesHttpProxy = "awsDatasourcesHttpProxy" - - // FlagTransformationsEmptyPlaceholder - // Show transformation quick-start cards in empty transformations state - FlagTransformationsEmptyPlaceholder = "transformationsEmptyPlaceholder" - - // FlagTtlPluginInstanceManager - // Enable TTL plugin instance manager - FlagTtlPluginInstanceManager = "ttlPluginInstanceManager" - - // FlagRudderstackUpgrade - // Enables the new version of rudderstack - FlagRudderstackUpgrade = "rudderstackUpgrade" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 44ed33ad19c..48eba662c0a 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -62,7 +62,6 @@ "description": "Enable configuration of alert enrichments in Grafana Cloud.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -77,7 +76,6 @@ "description": "Enable conditional alert enrichment steps.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -92,7 +90,6 @@ "description": "Allow multiple steps per enrichment.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -133,7 +130,6 @@ "description": "Enable AI-analyze central state history.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -148,7 +144,6 @@ "description": "Enable AI-generated feedback from the Grafana UI.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -163,7 +158,6 @@ "description": "Enable AI-generated alert rules.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -178,7 +172,6 @@ "description": "Enable AI-generated alerting templates.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -193,7 +186,6 @@ "description": "Enable AI-improve alert rules labels and annotations.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -221,7 +213,6 @@ "stage": "GA", "codeowner": "@grafana/alerting-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -229,14 +220,16 @@ { "metadata": { "name": "alertingCentralAlertHistory", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-05-29T15:01:38Z" + "resourceVersion": "1763541314825", + "creationTimestamp": "2024-05-29T15:01:38Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" + } }, "spec": { "description": "Enables the new central alert history.", "stage": "experimental", - "codeowner": "@grafana/alerting-squad", - "frontend": true + "codeowner": "@grafana/alerting-squad" } }, { @@ -249,7 +242,6 @@ "description": "Disables the ability to send alerts to an external Alertmanager datasource.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -263,7 +255,6 @@ "description": "Enable Assistant Investigations enrichment type.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -279,7 +270,6 @@ "description": "Enable Assistant Investigations enrichment type in the UI.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -294,7 +284,6 @@ "description": "Enable enrichment per rule in the alerting UI.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -322,7 +311,6 @@ "description": "Enables the API to import Alertmanager configuration", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -337,7 +325,6 @@ "description": "Enables the UI to see imported Alertmanager configuration", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -420,7 +407,6 @@ "description": "Enables the notification history feature", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -462,7 +448,6 @@ "description": "Enables a feature to avoid issues with concurrent writes to the alerting provenance table in MySQL", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -504,7 +489,6 @@ "description": "Enable rule notification message section extension.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -520,7 +504,6 @@ "stage": "GA", "codeowner": "@grafana/alerting-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -528,15 +511,16 @@ { "metadata": { "name": "alertingRuleRecoverDeleted", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-03-27T14:39:26Z" + "resourceVersion": "1763541314825", + "creationTimestamp": "2025-03-27T14:39:26Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" + } }, "spec": { "description": "Enables the UI functionality to recover and view deleted alert rules", "stage": "GA", "codeowner": "@grafana/alerting-squad", - "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -552,7 +536,6 @@ "stage": "GA", "codeowner": "@grafana/alerting-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -588,15 +571,16 @@ { "metadata": { "name": "alertingTriage", - "resourceVersion": "1756386724059", - "creationTimestamp": "2025-09-01T09:33:33Z" + "resourceVersion": "1763541314825", + "creationTimestamp": "2025-09-01T09:33:33Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" + } }, "spec": { "description": "Enables the alerting triage feature", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -625,7 +609,6 @@ "description": "Enables the UI to use certain backend-side filters", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -644,7 +627,6 @@ "stage": "preview", "codeowner": "@grafana/alerting-squad", "requiresRestart": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -664,7 +646,6 @@ "stage": "deprecated", "codeowner": "@grafana/alerting-squad", "requiresRestart": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -706,7 +687,6 @@ "description": "Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -733,7 +713,6 @@ "description": "Enables the gRPC client to authenticate with the App Platform by using ID \u0026 access tokens", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -760,7 +739,6 @@ "description": "Enables the gRPC server for authorization", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -895,8 +873,7 @@ "description": "Allow elements nesting", "stage": "experimental", "codeowner": "@grafana/dataviz-squad", - "frontend": true, - "hideFromAdminPage": true + "frontend": true } }, { @@ -963,8 +940,6 @@ "stage": "preview", "codeowner": "@grafana/identity-access-team", "requiresRestart": true, - "allowSelfServe": true, - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -990,7 +965,6 @@ "description": "Enables cross-account querying in CloudWatch datasources", "stage": "GA", "codeowner": "@grafana/aws-datasources", - "allowSelfServe": true, "expression": "true" } }, @@ -1048,7 +1022,6 @@ "description": "Correlations page", "stage": "GA", "codeowner": "@grafana/datapro", - "allowSelfServe": true, "expression": "true" } }, @@ -1138,14 +1111,16 @@ { "metadata": { "name": "dashboardNewLayouts", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-10-23T08:55:45Z" + "resourceVersion": "1763533458962", + "creationTimestamp": "2024-10-23T08:55:45Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-19 06:24:18.962973 +0000 UTC" + } }, "spec": { "description": "Enables experimental new dashboard layouts", "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "frontend": true + "codeowner": "@grafana/dashboards-squad" } }, { @@ -1266,7 +1241,6 @@ "stage": "GA", "codeowner": "@grafana/observability-metrics", "frontend": true, - "allowSelfServe": true, "expression": "true" } }, @@ -1319,8 +1293,7 @@ "description": "Disables classic HTTP Histogram (use with enableNativeHTTPHistogram)", "stage": "experimental", "codeowner": "@grafana/grafana-backend-services-squad", - "requiresRestart": true, - "hideFromAdminPage": true + "requiresRestart": true } }, { @@ -1333,7 +1306,6 @@ "description": "Disable envelope encryption (emergency only)", "stage": "GA", "codeowner": "@grafana/grafana-operator-experience-squad", - "hideFromAdminPage": true, "expression": "false" } }, @@ -1380,7 +1352,6 @@ "stage": "experimental", "codeowner": "@grafana/plugins-platform-backend", "requiresRestart": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -1441,7 +1412,6 @@ "stage": "experimental", "codeowner": "@grafana/plugins-platform-backend", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -1457,7 +1427,6 @@ "stage": "experimental", "codeowner": "@grafana/dashboards-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -1465,14 +1434,16 @@ { "metadata": { "name": "enableDatagridEditing", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-04-24T14:46:31Z" + "resourceVersion": "1763539789149", + "creationTimestamp": "2023-04-24T14:46:31Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-19 08:09:49.149669 +0000 UTC" + } }, "spec": { "description": "Enables the edit functionality in the datagrid panel", "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "codeowner": "@grafana/dataviz-squad" } }, { @@ -1498,8 +1469,7 @@ "description": "Enables native HTTP Histograms", "stage": "experimental", "codeowner": "@grafana/grafana-backend-services-squad", - "requiresRestart": true, - "hideFromAdminPage": true + "requiresRestart": true } }, { @@ -1514,7 +1484,6 @@ "stage": "experimental", "codeowner": "@grafana/plugins-platform-backend", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -1541,7 +1510,6 @@ "description": "Enables the scopes usage in Metrics Explore", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -1634,8 +1602,7 @@ "spec": { "description": "Automatic service account and token setup for plugins", "stage": "preview", - "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true + "codeowner": "@grafana/identity-access-team" } }, { @@ -1705,7 +1672,6 @@ "description": "Highlight Grafana Enterprise features", "stage": "GA", "codeowner": "@grafana/grafana-operator-experience-squad", - "allowSelfServe": true, "expression": "false" } }, @@ -1751,7 +1717,6 @@ "description": "Use a POST request to list rules by passing down the namespaces user has access to", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -1781,7 +1746,6 @@ "stage": "experimental", "codeowner": "@grafana/grafana-search-navigate-organise", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -1886,7 +1850,6 @@ "description": "Enables Grafana-managed recording rules.", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -1917,7 +1880,6 @@ "stage": "GA", "codeowner": "@grafana/grafana-frontend-platform", "requiresRestart": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -1958,7 +1920,6 @@ "description": "Enable groupBy variable support in scenes dashboards", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -1989,8 +1950,7 @@ "spec": { "description": "Run the GRPC server", "stage": "preview", - "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true + "codeowner": "@grafana/search-and-storage" } }, { @@ -2003,7 +1963,6 @@ "description": "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.", "stage": "GA", "codeowner": "@grafana/identity-access-team", - "allowSelfServe": true, "expression": "true" } }, @@ -2017,7 +1976,6 @@ "description": "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.", "stage": "GA", "codeowner": "@grafana/identity-access-team", - "allowSelfServe": true, "expression": "true" } }, @@ -2121,7 +2079,6 @@ "stage": "experimental", "codeowner": "@grafana/sharing-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2239,7 +2196,6 @@ "description": "Redirects the traffic from the legacy access control endpoints to the new K8s AuthZ endpoints", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2256,7 +2212,6 @@ "description": "Enables create, delete, and update mutations for resources owned by IAM identity", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2270,7 +2225,6 @@ "description": "Registers AuthZ /apis endpoint", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2285,7 +2239,6 @@ "description": "Enables K8s AuthZ endpoints", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2299,7 +2252,6 @@ "description": "Registers AuthZ resource permission /apis endpoints", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2316,7 +2268,6 @@ "description": "Enable sync of Zanzana authorization store on AuthZ CRD mutations", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2336,17 +2287,16 @@ { "metadata": { "name": "kubernetesDashboards", - "resourceVersion": "1755157224830", + "resourceVersion": "1763533458962", "creationTimestamp": "2024-06-05T14:34:23Z", "annotations": { - "grafana.app/updatedTimestamp": "2025-08-14 07:40:24.830741 +0000 UTC" + "grafana.app/updatedTimestamp": "2025-11-19 06:24:18.962973 +0000 UTC" } }, "spec": { "description": "Use the kubernetes API in the frontend for dashboards", "stage": "GA", "codeowner": "@grafana/dashboards-squad", - "frontend": true, "expression": "true" } }, @@ -2360,8 +2310,7 @@ "description": "Use the kubernetes API for feature toggle management in the frontend", "stage": "experimental", "codeowner": "@grafana/grafana-operator-experience-squad", - "frontend": true, - "hideFromAdminPage": true + "frontend": true } }, { @@ -2459,7 +2408,6 @@ "description": "Populate Zanzana on AuthZ CRDs creation or update", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2498,7 +2446,6 @@ "description": "In-development feature that will allow injection of labels into loki queries.", "stage": "privatePreview", "codeowner": "@grafana/observability-logs", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -2541,7 +2488,6 @@ "stage": "GA", "codeowner": "@grafana/observability-logs", "frontend": true, - "allowSelfServe": true, "expression": "true" } }, @@ -2648,7 +2594,6 @@ "stage": "GA", "codeowner": "@grafana/observability-logs", "frontend": true, - "allowSelfServe": true, "expression": "true" } }, @@ -2687,7 +2632,6 @@ "description": "Pick the dual write mode from database configs", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2792,7 +2736,6 @@ "description": "Enables filters and group by variables on all new dashboards. Variables are added only if default data source supports filtering.", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2903,7 +2846,6 @@ "description": "Enables the report creation drawer in a dashboard", "stage": "preview", "codeowner": "@grafana/grafana-operator-experience-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2931,7 +2873,6 @@ "description": "Require that sub claims is present in oauth tokens.", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2961,7 +2902,6 @@ "description": "When storing dashboard and folder resource permissions, only store action sets and not the full list of underlying permission", "stage": "GA", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -3043,8 +2983,7 @@ "spec": { "description": "Search for dashboards using panel title", "stage": "preview", - "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true + "codeowner": "@grafana/search-and-storage" } }, { @@ -3057,7 +2996,6 @@ "description": "Enable passwordless login via magic link authentication", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3127,7 +3065,6 @@ "stage": "experimental", "codeowner": "@grafana/plugins-platform-backend", "requiresRestart": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -3289,7 +3226,6 @@ "description": "Enables possibility to preserve dashboard variables and time range when navigating between dashboards", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3318,7 +3254,6 @@ "description": "In-development feature that will allow injection of labels into prometheus queries.", "stage": "GA", "codeowner": "@grafana/oss-big-tent", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -3404,7 +3339,6 @@ "description": "Enables public dashboard sharing to be restricted to only allowed emails", "stage": "preview", "codeowner": "@grafana/grafana-operator-experience-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3540,8 +3474,7 @@ "spec": { "description": "Refactor time range variables flow to reduce number of API calls made when query variables are chained", "stage": "preview", - "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true + "codeowner": "@grafana/dashboards-squad" } }, { @@ -3554,7 +3487,6 @@ "description": "Require that refresh tokens are present in oauth tokens.", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3585,7 +3517,6 @@ "description": "Enables reload of dashboards on scopes, time range and variables changes", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3598,8 +3529,7 @@ "spec": { "description": "Uses JWT-based auth for rendering instead of relying on remote cache", "stage": "preview", - "codeowner": "@grafana/grafana-operator-experience-squad", - "hideFromAdminPage": true + "codeowner": "@grafana/grafana-operator-experience-squad" } }, { @@ -3613,7 +3543,6 @@ "stage": "experimental", "codeowner": "@grafana/grafana-operator-experience-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3643,7 +3572,6 @@ "description": "Enables restore deleted dashboards feature", "stage": "experimental", "codeowner": "@grafana/grafana-search-navigate-organise", - "hideFromAdminPage": true, "expression": "false" } }, @@ -3661,7 +3589,6 @@ "stage": "experimental", "codeowner": "@grafana/plugins-platform-backend", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -3730,7 +3657,6 @@ "description": "In-development feature flag for the scope api using the app platform.", "stage": "experimental", "codeowner": "@grafana/grafana-app-platform-squad", - "hideFromAdminPage": true, "expression": "false" } }, @@ -3744,7 +3670,6 @@ "description": "Enables the use of scope filters in Grafana", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3761,7 +3686,6 @@ "description": "Enable scope search to include all levels of the scope node tree", "stage": "experimental", "codeowner": "@grafana/grafana-operator-experience-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -3829,7 +3753,6 @@ "description": "Skip token rotation if it was already rotated less than 5 seconds ago", "stage": "GA", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -3845,8 +3768,7 @@ "description": "Enables previous SQL data source dataset dropdown behavior", "stage": "preview", "codeowner": "@grafana/oss-big-tent", - "frontend": true, - "hideFromAdminPage": true + "frontend": true } }, { @@ -3903,7 +3825,6 @@ "stage": "GA", "codeowner": "@grafana/identity-access-team", "requiresRestart": true, - "allowSelfServe": true, "expression": "true" } }, @@ -4012,7 +3933,6 @@ "description": "Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams", "stage": "GA", "codeowner": "@grafana/identity-access-team", - "allowSelfServe": true, "expression": "true" } }, @@ -4059,14 +3979,16 @@ { "metadata": { "name": "tempoAlerting", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-07-15T13:36:36Z" + "resourceVersion": "1763532089512", + "creationTimestamp": "2025-07-15T13:36:36Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-19 06:01:29.512182 +0000 UTC" + } }, "spec": { "description": "Enables creating alerts from Tempo data source", "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", - "frontend": true + "codeowner": "@grafana/observability-traces-and-profiling" } }, { @@ -4170,7 +4092,6 @@ "stage": "GA", "codeowner": "@grafana/observability-metrics", "frontend": true, - "allowSelfServe": true, "expression": "true" } }, @@ -4227,7 +4148,6 @@ "description": "Writes error logs to the request logger", "stage": "GA", "codeowner": "@grafana/grafana-backend-group", - "hideFromAdminPage": true, "expression": "true" } }, @@ -4253,7 +4173,6 @@ "description": "Enables the unified storage grpc connection pool", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4268,7 +4187,6 @@ "description": "Enables the unified storage history pruner", "stage": "GA", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "true" } @@ -4283,7 +4201,6 @@ "description": "Enable unified storage search", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4302,7 +4219,6 @@ "stage": "experimental", "codeowner": "@grafana/search-and-storage", "requiresRestart": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -4317,7 +4233,6 @@ "description": "Enable dual reader for unified storage search", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4331,7 +4246,6 @@ "description": "Enable sprinkles on unified storage search", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4345,7 +4259,6 @@ "description": "Enable unified storage search UI", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4360,7 +4273,6 @@ "description": "Use full n-gram indexing instead of edge n-gram for unified storage search", "stage": "experimental", "codeowner": "@grafana/search-and-storage", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4404,7 +4316,6 @@ "stage": "experimental", "codeowner": "@grafana/grafana-operator-experience-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -4420,7 +4331,6 @@ "stage": "experimental", "codeowner": "@grafana/grafana-operator-experience-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true, "expression": "false" } @@ -4439,7 +4349,6 @@ "stage": "experimental", "codeowner": "@grafana/grafana-operator-experience-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4470,7 +4379,6 @@ "stage": "preview", "codeowner": "@grafana/dataviz-squad", "frontend": true, - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4484,7 +4392,6 @@ "description": "Use openFGA as authorization engine.", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -4501,7 +4408,6 @@ "description": "Use openFGA as main authorization engine and disable legacy RBAC clietn.", "stage": "experimental", "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true, "hideFromDocs": true } } diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index d6a7c66802d..57e308de4ec 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -50,17 +50,14 @@ func TestFeatureToggleFiles(t *testing.T) { lookup := map[string]featuretoggleapi.FeatureSpec{} for _, flag := range standardFeatureFlags { lookup[flag.Name] = featuretoggleapi.FeatureSpec{ - Description: flag.Description, - Stage: flag.Stage.String(), - Owner: string(flag.Owner), - RequiresDevMode: flag.RequiresDevMode, - FrontendOnly: flag.FrontendOnly, - RequiresRestart: flag.RequiresRestart, - AllowSelfServe: flag.AllowSelfServe, - HideFromAdminPage: flag.HideFromAdminPage, - HideFromDocs: flag.HideFromDocs, - Expression: flag.Expression, - // EnabledVersion: ???, + Description: flag.Description, + Stage: flag.Stage.String(), + Owner: string(flag.Owner), + RequiresDevMode: flag.RequiresDevMode, + FrontendOnly: flag.FrontendOnly, + RequiresRestart: flag.RequiresRestart, + HideFromDocs: flag.HideFromDocs, + Expression: flag.Expression, } // Replace them all @@ -187,9 +184,6 @@ func verifyFlagsConfiguration(t *testing.T) { if flag.Name != strings.TrimSpace(flag.Name) { t.Errorf("flag Name should not start/end with spaces. See: %s", flag.Name) } - if flag.AllowSelfServe && (flag.Stage != FeatureStageGeneralAvailability && flag.Stage != FeatureStagePublicPreview && flag.Stage != FeatureStageDeprecated) { - t.Errorf("only allow self-serving GA, PublicPreview and Deprecated toggles") - } if flag.Owner == "" { t.Errorf("feature %s does not have an owner. please fill the FeatureFlag.Owner property", flag.Name) } @@ -340,6 +334,10 @@ package featuremgmt const (`) for _, flag := range standardFeatureFlags { + if flag.FrontendOnly { + continue // no need to have the golang constant for frontend only flags + } + data.CamelCase = strcase.ToCamel(flag.Name) data.Flag = flag data.Ext = "" From a4cbbe10c0d36a89334707f4f3f8e4526391ade6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Thu, 20 Nov 2025 14:55:45 +0100 Subject: [PATCH 021/378] Provisioning: Add retry logic for nanogit client operations (#114216) * chore(deps): update nanogit to v0.3.0 in go.mod and go.sum files * Add retry logic for nanogit client operations - Configure retry logic in withGitContext to ensure all Git operations have retry support - Use nanogit's ExponentialBackoffRetrier with 8 attempts (~10s retry window) - Retry transient network errors and HTTP-specific server errors (5xx for GET/DELETE, 429 for all) - Rename logger function to withGitContext to better reflect its responsibilities * fix: resolve staticcheck S1008 linting issue in retry_client.go Simplify return statement to use errors.As directly instead of if-return pattern * Revert "fix: resolve staticcheck S1008 linting issue in retry_client.go" This reverts commit bd367b5629bd49d9d15db708352b9ed1805376ab. --- apps/provisioning/go.mod | 2 +- apps/provisioning/go.sum | 4 +- .../pkg/repository/git/repository.go | 80 ++++++++++++++++--- .../pkg/repository/git/repository_test.go | 10 +-- go.mod | 2 +- go.sum | 4 +- go.work.sum | 4 +- 7 files changed, 81 insertions(+), 25 deletions(-) diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index 9bb12d5ae4f..a72aa77fe68 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -10,7 +10,7 @@ require ( github.com/grafana/grafana-app-sdk/logging v0.48.1 github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 - github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b + github.com/grafana/nanogit v0.3.0 github.com/migueleliasweb/go-github-mock v1.1.0 github.com/stretchr/testify v1.11.1 golang.org/x/oauth2 v0.33.0 diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index 78f06781b15..92a4d53c282 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -70,8 +70,8 @@ github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f h1:f+Z github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f/go.mod h1:RA8mP8KVIwKXBx3Ssqa/uEBABib5LvUWYPVMxrNvnP0= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= -github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b h1:rFjoqJFb2KxJ29K9ltuWRSsdA46SbN0GCxoQc36h5kg= -github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= +github.com/grafana/nanogit v0.3.0 h1:XNEef+4Vi+465ZITJs/g/xgnDRJbWhhJ7iQrAnWZ0oQ= +github.com/grafana/nanogit v0.3.0/go.mod h1:6s6CCTpyMOHPpcUZaLGI+rgBEKdmxVbhqSGgCK13j7Y= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/apps/provisioning/pkg/repository/git/repository.go b/apps/provisioning/pkg/repository/git/repository.go index 4d002e4f831..82d5ca6bd1d 100644 --- a/apps/provisioning/pkg/repository/git/repository.go +++ b/apps/provisioning/pkg/repository/git/repository.go @@ -25,6 +25,7 @@ import ( "github.com/grafana/nanogit/options" "github.com/grafana/nanogit/protocol" "github.com/grafana/nanogit/protocol/hash" + "github.com/grafana/nanogit/retry" ) type RepositoryConfig struct { @@ -144,7 +145,7 @@ func isValidGitURL(gitURL string) bool { // Test implements provisioning.Repository. func (r *gitRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { - ctx, _ = r.logger(ctx, "") + ctx, _ = r.withGitContext(ctx, "") t := string(r.config.Spec.Type) @@ -219,7 +220,7 @@ func (r *gitRepository) Test(ctx context.Context) (*provisioning.TestResults, er // Read implements provisioning.Repository. func (r *gitRepository) Read(ctx context.Context, filePath, ref string) (*repository.FileInfo, error) { - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) finalPath := safepath.Join(r.gitConfig.Path, filePath) // Resolve ref to commit hash @@ -271,7 +272,7 @@ func (r *gitRepository) Read(ctx context.Context, filePath, ref string) (*reposi } func (r *gitRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) // Resolve ref to commit hash refHash, err := r.resolveRefToHash(ctx, ref) @@ -319,7 +320,7 @@ func (r *gitRepository) Create(ctx context.Context, path, ref string, data []byt if ref == "" { ref = r.gitConfig.Branch } - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) branchRef, err := r.ensureBranchExists(ctx, ref) if err != nil { return err @@ -364,7 +365,7 @@ func (r *gitRepository) Update(ctx context.Context, path, ref string, data []byt if ref == "" { ref = r.gitConfig.Branch } - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) // Check if trying to update a directory if safepath.IsDir(path) { @@ -411,7 +412,7 @@ func (r *gitRepository) Write(ctx context.Context, path string, ref string, data ref = r.gitConfig.Branch } - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) info, err := r.Read(ctx, path, ref) if err != nil && !(errors.Is(err, repository.ErrFileNotFound)) { return fmt.Errorf("check if file exists before writing: %w", err) @@ -431,7 +432,7 @@ func (r *gitRepository) Delete(ctx context.Context, path, ref, comment string) e if ref == "" { ref = r.gitConfig.Branch } - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) branchRef, err := r.ensureBranchExists(ctx, ref) if err != nil { @@ -454,7 +455,7 @@ func (r *gitRepository) Move(ctx context.Context, oldPath, newPath, ref, comment if ref == "" { ref = r.gitConfig.Branch } - ctx, _ = r.logger(ctx, ref) + ctx, _ = r.withGitContext(ctx, ref) branchRef, err := r.ensureBranchExists(ctx, ref) if err != nil { @@ -545,6 +546,7 @@ func (r *gitRepository) History(_ context.Context, _ string, _ string) ([]provis } func (r *gitRepository) ListRefs(ctx context.Context) ([]provisioning.RefItem, error) { + ctx, _ = r.withGitContext(ctx, "") refs, err := r.client.ListRefs(ctx) if err != nil { return nil, fmt.Errorf("list refs: %w", err) @@ -566,7 +568,7 @@ func (r *gitRepository) ListRefs(ctx context.Context) ([]provisioning.RefItem, e } func (r *gitRepository) LatestRef(ctx context.Context) (string, error) { - ctx, _ = r.logger(ctx, "") + ctx, _ = r.withGitContext(ctx, "") branchRef, err := r.client.GetRef(ctx, fmt.Sprintf("refs/heads/%s", r.gitConfig.Branch)) if err != nil { return "", fmt.Errorf("get branch ref: %w", err) @@ -583,7 +585,7 @@ func (r *gitRepository) CompareFiles(ctx context.Context, base, ref string) ([]r return nil, fmt.Errorf("ref cannot be empty") } - ctx, logger := r.logger(ctx, ref) + ctx, logger := r.withGitContext(ctx, ref) // Resolve base ref to hash var baseHash hash.Hash @@ -671,11 +673,15 @@ func (r *gitRepository) CompareFiles(ctx context.Context, base, ref string) ([]r } func (r *gitRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) { + ctx = ensureRetryContext(ctx) + ctx, _ = r.withGitContext(ctx, "") return NewStagedGitRepository(ctx, r, opts) } // resolveRefToHash resolves a ref (branch name or commit hash) to a commit hash func (r *gitRepository) resolveRefToHash(ctx context.Context, ref string) (hash.Hash, error) { + ctx, _ = r.withGitContext(ctx, ref) + // Use default branch if ref is empty if ref == "" { ref = r.gitConfig.Branch @@ -706,6 +712,8 @@ func (r *gitRepository) resolveRefToHash(ctx context.Context, ref string) (hash. // ensureBranchExists checks if a branch exists and creates it if it doesn't, // returning the branch reference to avoid duplicate GetRef calls func (r *gitRepository) ensureBranchExists(ctx context.Context, branchName string) (nanogit.Ref, error) { + ctx, _ = r.withGitContext(ctx, branchName) + if !IsValidGitBranchName(branchName) { return nanogit.Ref{}, &apierrors.StatusError{ ErrStatus: metav1.Status{ @@ -802,7 +810,57 @@ func (r *gitRepository) commitAndPush(ctx context.Context, writer nanogit.Staged return nil } -func (r *gitRepository) logger(ctx context.Context, ref string) (context.Context, logging.Logger) { +// defaultGitRetrier returns a default retrier configuration for Git operations. +// +// Retry attempts will happen when: +// - Network errors occur: connection timeouts, temporary network failures, or connection errors +// - HTTP 5xx server errors: For GET and DELETE operations (idempotent) +// - HTTP 429 Too Many Requests: For all operations (rate limiting is temporary) +// +// The retry behavior: +// - Total attempts: 8 (1 initial attempt + 7 retries) +// - Initial delay: 100ms before the first retry +// - Exponential backoff: delay doubles after each failed attempt (100ms → 200ms → 400ms → 800ms → 1.6s → 3.2s → 5s) +// - Maximum delay: capped at 5 seconds +// - Jitter: enabled to prevent thundering herd problems +// - Total retry window: approximately 10 seconds from first attempt to last retry +// +// All attempts will fail when: +// - The Git server is completely unavailable or unreachable +// - Network connectivity issues persist beyond the retry window (~10 seconds) +// - The server returns transient errors consistently for the entire retry duration +// - Context cancellation occurs before retries complete +// +// Non-transient errors (e.g., 4xx client errors except 429, authentication failures) are not retried and returned immediately. +func defaultGitRetrier() *retry.ExponentialBackoffRetrier { + return retry.NewExponentialBackoffRetrier(). + WithMaxAttempts(8). // 1 initial + 7 retries = 8 total attempts (~10s total retry window) + WithInitialDelay(100 * time.Millisecond). + WithMaxDelay(5 * time.Second). + WithMultiplier(2.0). + WithJitter() +} + +// ensureRetryContext ensures that retry logic is configured in the context. +// This function should be called at the beginning of all methods that make client calls +// to guarantee retry logic is always present, regardless of context state. +func ensureRetryContext(ctx context.Context) context.Context { + // Only add retrier if one doesn't already exist in the context + if retry.FromContext(ctx).MaxAttempts() <= 1 { + ctx = retry.ToContext(ctx, defaultGitRetrier()) + } + return ctx +} + +// withGitContext sets up the context with logging, git repository metadata, and retry logic. +// This function should be called at the beginning of all public methods to ensure: +// - Proper logging context with git repository details +// - Retry logic is configured for all Git operations +// - Context is properly prepared for nanogit client calls +func (r *gitRepository) withGitContext(ctx context.Context, ref string) (context.Context, logging.Logger) { + // Ensure retry logic is configured first, before any early returns + ctx = ensureRetryContext(ctx) + logger := logging.FromContext(ctx) type containsGit int diff --git a/apps/provisioning/pkg/repository/git/repository_test.go b/apps/provisioning/pkg/repository/git/repository_test.go index d3946b7b6d4..4958ab372ab 100644 --- a/apps/provisioning/pkg/repository/git/repository_test.go +++ b/apps/provisioning/pkg/repository/git/repository_test.go @@ -2163,7 +2163,7 @@ func TestGitRepository_commitAndPush(t *testing.T) { } } -func TestGitRepository_logger(t *testing.T) { +func TestGitRepository_withGitContext(t *testing.T) { gitRepo := &gitRepository{ config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -2179,7 +2179,7 @@ func TestGitRepository_logger(t *testing.T) { t.Run("creates new logger context", func(t *testing.T) { ctx := context.Background() - newCtx, logger := gitRepo.logger(ctx, "feature-branch") + newCtx, logger := gitRepo.withGitContext(ctx, "feature-branch") require.NotNil(t, newCtx) require.NotNil(t, logger) @@ -2188,7 +2188,7 @@ func TestGitRepository_logger(t *testing.T) { t.Run("uses default branch when ref is empty", func(t *testing.T) { ctx := context.Background() - newCtx, logger := gitRepo.logger(ctx, "") + newCtx, logger := gitRepo.withGitContext(ctx, "") require.NotNil(t, newCtx) require.NotNil(t, logger) @@ -2198,10 +2198,10 @@ func TestGitRepository_logger(t *testing.T) { ctx := context.Background() // First call creates the logger context - ctx1, logger1 := gitRepo.logger(ctx, "branch1") + ctx1, logger1 := gitRepo.withGitContext(ctx, "branch1") // Second call should return the existing logger context - ctx2, logger2 := gitRepo.logger(ctx1, "branch2") + ctx2, logger2 := gitRepo.withGitContext(ctx1, "branch2") // When logger context already exists, it should return the same context require.Equal(t, ctx1, ctx2) diff --git a/go.mod b/go.mod index 23756c59ae0..d8df0cd6bd7 100644 --- a/go.mod +++ b/go.mod @@ -106,7 +106,7 @@ require ( github.com/grafana/grafana-plugin-sdk-go v0.283.0 // @grafana/plugins-platform-backend github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 // @grafana/alerting-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs - github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b // indirect; @grafana/grafana-git-ui-sync-team + github.com/grafana/nanogit v0.3.0 // indirect; @grafana/grafana-git-ui-sync-team github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // @grafana/observability-traces-and-profiling github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae // @grafana/observability-traces-and-profiling diff --git a/go.sum b/go.sum index 61068fdef0b..1c294bc1fe2 100644 --- a/go.sum +++ b/go.sum @@ -1661,8 +1661,8 @@ github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLm github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000/go.mod h1:/ZklAgE1i4f3Z8uriXwESmCr1VLF8lBGaJspuaGuf78= github.com/grafana/loki/v3 v3.2.1 h1:VB7u+KHfvL5aHAxgoVBvz5wVhsdGuqKC7uuOFOOe7jw= github.com/grafana/loki/v3 v3.2.1/go.mod h1:WvdLl6wOS+yahaeQY+xhD2m2XzkHDfKr5FZaX7D/X2Y= -github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b h1:rFjoqJFb2KxJ29K9ltuWRSsdA46SbN0GCxoQc36h5kg= -github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= +github.com/grafana/nanogit v0.3.0 h1:XNEef+4Vi+465ZITJs/g/xgnDRJbWhhJ7iQrAnWZ0oQ= +github.com/grafana/nanogit v0.3.0/go.mod h1:6s6CCTpyMOHPpcUZaLGI+rgBEKdmxVbhqSGgCK13j7Y= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U= diff --git a/go.work.sum b/go.work.sum index 9dc15683534..bbfe799f72c 100644 --- a/go.work.sum +++ b/go.work.sum @@ -937,7 +937,6 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= @@ -1323,7 +1322,6 @@ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkq github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8= github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ= @@ -1374,6 +1372,7 @@ github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiy github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/schollz/progressbar/v3 v3.14.6 h1:GyjwcWBAf+GFDMLziwerKvpuS7ZF+mNTAXIB2aspiZs= github.com/schollz/progressbar/v3 v3.14.6/go.mod h1:Nrzpuw3Nl0srLY0VlTvC4V6RL50pcEymjy6qyJAaLa0= +github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= github.com/segmentio/parquet-go v0.0.0-20220811205829-7efc157d28af/go.mod h1:PxYdAI6cGd+s1j4hZDQbz3VFgobF5fDA0weLeNWKTE4= @@ -1920,7 +1919,6 @@ golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= From 3999d108f7f21a072990400e8cfcf7c4dd6bcdfc Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Thu, 20 Nov 2025 14:00:33 +0000 Subject: [PATCH 022/378] Folders: Make `listFolders` call correct API and fix tags sorting (#114181) --- .../browse-dashboards/api/services.ts | 19 ++++++++++--------- public/app/features/search/service/unified.ts | 4 +++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/public/app/features/browse-dashboards/api/services.ts b/public/app/features/browse-dashboards/api/services.ts index 98be615583b..2bb1ec6d39e 100644 --- a/public/app/features/browse-dashboards/api/services.ts +++ b/public/app/features/browse-dashboards/api/services.ts @@ -1,8 +1,7 @@ -import { getBackendSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { GENERAL_FOLDER_UID } from 'app/features/search/constants'; import { getGrafanaSearcher } from 'app/features/search/service/searcher'; -import { NestedFolderDTO } from 'app/features/search/service/types'; +import { DashboardQueryResult } from 'app/features/search/service/types'; import { queryResultToViewItem } from 'app/features/search/service/utils'; import { DashboardViewItem } from 'app/features/search/types'; import { AccessControlAction } from 'app/types/accessControl'; @@ -17,22 +16,24 @@ export async function listFolders( page = 1, pageSize = PAGE_SIZE ): Promise { - const backendSrv = getBackendSrv(); + const searcher = getGrafanaSearcher(); - // TODO: what to do here for unified search? - let folders: NestedFolderDTO[] = []; + let folders: DashboardQueryResult[] = []; if (contextSrv.hasPermission(AccessControlAction.FoldersRead)) { - folders = await backendSrv.get('/api/folders', { - parentUid: parentUID, - page, + const foldersResults = await searcher.search({ + kind: ['folder'], + location: parentUID || 'general', + from: (page - 1) * pageSize, // our pages are 1-indexed, so we need to -1 to convert that to correct value to skip limit: pageSize, + offset: (page - 1) * pageSize, }); + folders = foldersResults.view.toArray(); } return folders.map((item) => ({ kind: 'folder', uid: item.uid, - title: item.title, + title: item.name, parentTitle, parentUID, managedBy: item.managedBy, diff --git a/public/app/features/search/service/unified.ts b/public/app/features/search/service/unified.ts index 4ab551997a3..8d1af58f9d9 100644 --- a/public/app/features/search/service/unified.ts +++ b/public/app/features/search/service/unified.ts @@ -386,7 +386,9 @@ export function toDashboardResults(rsp: SearchAPIResponse, sort: string): DataFr ...hit, uid: hit.name, url: toURL(hit.resource, hit.name, hit.title), - tags: hit.tags || [], + // Sort tags so we aren't reliant on the backend having done this for us + // Sorting order can be different between APIs/search implementations + tags: (hit.tags || []).sort(), folder: hit.folder || 'general', location, name: hit.title, // 🤯 FIXME hit.name is k8s name, eg grafana dashboards UID From 7d179120b6387c679d66b54fbbe41d7d15cc25c6 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 20 Nov 2025 14:03:24 +0000 Subject: [PATCH 023/378] Plugins: Replace CDN class with FS CDN type (#113968) replace cdn class with fs type --- apps/plugins/kinds/plugin.cue | 4 +- .../apis/plugins/v0alpha1/plugin_spec_gen.go | 1 - apps/plugins/pkg/apis/plugins_manifest.go | 2 +- apps/plugins/pkg/app/install/registrar.go | 1 - .../plugins/pkg/app/install/registrar_test.go | 13 --- pkg/api/frontendsettings_test.go | 14 +++- pkg/plugins/ifaces.go | 17 +++- pkg/plugins/localfiles.go | 6 +- .../angularinspector/angularinspector.go | 2 +- .../angularinspector/angularinspector_test.go | 7 +- pkg/plugins/manager/loader/loader.go | 34 +------- .../manager/pipeline/validation/steps.go | 7 +- pkg/plugins/manager/pluginfakes/fakes.go | 12 ++- .../manager/signature/manifest_test.go | 2 +- pkg/plugins/pluginassets/pluginassets_test.go | 9 --- pkg/plugins/plugins.go | 3 +- pkg/plugins/test_utils.go | 2 +- .../pluginsintegration/loader/loader_test.go | 8 +- .../pluginsintegration/pipeline/steps.go | 2 +- .../pluginassets/pluginassets.go | 10 +-- .../pluginassets/pluginassets_test.go | 80 ++++++++++++------- 21 files changed, 122 insertions(+), 114 deletions(-) diff --git a/apps/plugins/kinds/plugin.cue b/apps/plugins/kinds/plugin.cue index 4f381082969..50c475cd430 100644 --- a/apps/plugins/kinds/plugin.cue +++ b/apps/plugins/kinds/plugin.cue @@ -13,7 +13,7 @@ pluginV0Alpha1: { id: string version: string url?: string - class: "core" | "external" | "cdn" + class: "core" | "external" } } routes: { @@ -233,4 +233,4 @@ pluginV0Alpha1: { title?: string description?: string }] -} \ No newline at end of file +} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go index d46a0a4aed1..9ae14bcbdc5 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go @@ -21,5 +21,4 @@ type PluginSpecClass string const ( PluginSpecClassCore PluginSpecClass = "core" PluginSpecClassExternal PluginSpecClass = "external" - PluginSpecClassCdn PluginSpecClass = "cdn" ) diff --git a/apps/plugins/pkg/apis/plugins_manifest.go b/apps/plugins/pkg/apis/plugins_manifest.go index 428971aa0b5..09c57eaa186 100644 --- a/apps/plugins/pkg/apis/plugins_manifest.go +++ b/apps/plugins/pkg/apis/plugins_manifest.go @@ -20,7 +20,7 @@ import ( ) var ( - rawSchemaPluginv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Plugin":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"getMetaDependencies":{"type":"object","required":["grafanaDependency"],"properties":{"extensions":{"type":"object","properties":{"exposedComponents":{"description":"+listType=set","type":"array","items":{"type":"string"}}},"additionalProperties":false},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id","type","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["app","datasource","panel"]}},"additionalProperties":false}}},"additionalProperties":false},"getMetaEnterpriseFeatures":{"type":"object","properties":{"healthDiagnosticsErrors":{"description":"Allow additional properties","type":"boolean","default":false}},"additionalProperties":false},"getMetaExtensions":{"type":"object","properties":{"addedComponents":{"description":"+listType=atomic","type":"array","items":{"type":"object","required":["targets","title"],"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","type":"array","items":{"type":"string"}},"title":{"type":"string"}},"additionalProperties":false}},"addedLinks":{"description":"+listType=atomic","type":"array","items":{"type":"object","required":["targets","title"],"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","type":"array","items":{"type":"string"}},"title":{"type":"string"}},"additionalProperties":false}},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"additionalProperties":false}},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"getMetaIAM":{"type":"object","properties":{"permissions":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"scope":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"getMetaInclude":{"type":"object","properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"type":"string","enum":["Admin","Editor","Viewer"]},"type":{"type":"string","enum":["dashboard","page","panel","datasource"]},"uid":{"type":"string"}},"additionalProperties":false},"getMetaInfo":{"type":"object","required":["keywords","logos","updated","version"],"properties":{"author":{"description":"Optional fields","type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","type":"array","items":{"type":"string"}},"links":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false}},"logos":{"type":"object","required":["small","large"],"properties":{"large":{"type":"string"},"small":{"type":"string"}},"additionalProperties":false},"screenshots":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"path":{"type":"string"}},"additionalProperties":false}},"updated":{"type":"string","format":"date-time"},"version":{"type":"string"}},"additionalProperties":false},"getMetaQueryOptions":{"type":"object","properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"additionalProperties":false},"getMetaRole":{"type":"object","properties":{"grants":{"description":"+listType=set","type":"array","items":{"type":"string"}},"role":{"type":"object","properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"scope":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false}},"additionalProperties":false},"getMetaRoute":{"type":"object","properties":{"body":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"headers":{"description":"+listType=atomic","type":"array","items":{"type":"string"}},"jwtTokenAuth":{"type":"object","properties":{"params":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"scopes":{"description":"+listType=set","type":"array","items":{"type":"string"}},"url":{"type":"string"}},"additionalProperties":false},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"type":"object","properties":{"params":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"scopes":{"description":"+listType=set","type":"array","items":{"type":"string"}},"url":{"type":"string"}},"additionalProperties":false},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"content":{"type":"string"},"name":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external","cdn"],"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaPluginv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Plugin":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"getMetaDependencies":{"type":"object","required":["grafanaDependency"],"properties":{"extensions":{"type":"object","properties":{"exposedComponents":{"description":"+listType=set","type":"array","items":{"type":"string"}}},"additionalProperties":false},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id","type","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["app","datasource","panel"]}},"additionalProperties":false}}},"additionalProperties":false},"getMetaEnterpriseFeatures":{"type":"object","properties":{"healthDiagnosticsErrors":{"description":"Allow additional properties","type":"boolean","default":false}},"additionalProperties":false},"getMetaExtensions":{"type":"object","properties":{"addedComponents":{"description":"+listType=atomic","type":"array","items":{"type":"object","required":["targets","title"],"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","type":"array","items":{"type":"string"}},"title":{"type":"string"}},"additionalProperties":false}},"addedLinks":{"description":"+listType=atomic","type":"array","items":{"type":"object","required":["targets","title"],"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","type":"array","items":{"type":"string"}},"title":{"type":"string"}},"additionalProperties":false}},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"additionalProperties":false}},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"getMetaIAM":{"type":"object","properties":{"permissions":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"scope":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"getMetaInclude":{"type":"object","properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"type":"string","enum":["Admin","Editor","Viewer"]},"type":{"type":"string","enum":["dashboard","page","panel","datasource"]},"uid":{"type":"string"}},"additionalProperties":false},"getMetaInfo":{"type":"object","required":["keywords","logos","updated","version"],"properties":{"author":{"description":"Optional fields","type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","type":"array","items":{"type":"string"}},"links":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false}},"logos":{"type":"object","required":["small","large"],"properties":{"large":{"type":"string"},"small":{"type":"string"}},"additionalProperties":false},"screenshots":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"path":{"type":"string"}},"additionalProperties":false}},"updated":{"type":"string","format":"date-time"},"version":{"type":"string"}},"additionalProperties":false},"getMetaQueryOptions":{"type":"object","properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"additionalProperties":false},"getMetaRole":{"type":"object","properties":{"grants":{"description":"+listType=set","type":"array","items":{"type":"string"}},"role":{"type":"object","properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"scope":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false}},"additionalProperties":false},"getMetaRoute":{"type":"object","properties":{"body":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"headers":{"description":"+listType=atomic","type":"array","items":{"type":"string"}},"jwtTokenAuth":{"type":"object","properties":{"params":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"scopes":{"description":"+listType=set","type":"array","items":{"type":"string"}},"url":{"type":"string"}},"additionalProperties":false},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"type":"object","properties":{"params":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"scopes":{"description":"+listType=set","type":"array","items":{"type":"string"}},"url":{"type":"string"}},"additionalProperties":false},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"content":{"type":"string"},"name":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external"],"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) versionSchemaPluginv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaPluginv0alpha1, &versionSchemaPluginv0alpha1) ) diff --git a/apps/plugins/pkg/app/install/registrar.go b/apps/plugins/pkg/app/install/registrar.go index fe747eb2806..3977fd88cf0 100644 --- a/apps/plugins/pkg/app/install/registrar.go +++ b/apps/plugins/pkg/app/install/registrar.go @@ -23,7 +23,6 @@ type Class = string const ( ClassCore Class = "core" ClassExternal Class = "external" - ClassCDN Class = "cdn" ) type Source = string diff --git a/apps/plugins/pkg/app/install/registrar_test.go b/apps/plugins/pkg/app/install/registrar_test.go index 36359896ae6..f1e0adbc508 100644 --- a/apps/plugins/pkg/app/install/registrar_test.go +++ b/apps/plugins/pkg/app/install/registrar_test.go @@ -446,19 +446,6 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) { require.Equal(t, pluginsv0alpha1.PluginSpecClass(ClassCore), p.Spec.Class) }, }, - { - name: "cdn class is mapped correctly", - install: PluginInstall{ - ID: "plugin-cdn", - Version: "3.0.0", - Class: ClassCDN, - Source: SourcePluginStore, - }, - namespace: "org-3", - validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) { - require.Equal(t, pluginsv0alpha1.PluginSpecClass(ClassCDN), p.Spec.Class) - }, - }, { name: "source annotation is set correctly", install: PluginInstall{ diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index cb09065dc85..ba67837be5d 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/login/social/socialimpl" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginscdn" @@ -265,6 +266,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) { Type: plugins.TypeApp, Preload: true, }, + FS: &pluginfakes.FakePluginFS{}, }, }, } @@ -302,6 +304,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) { Type: plugins.TypeApp, Preload: true, }, + FS: &pluginfakes.FakePluginFS{}, }, }, } @@ -339,6 +342,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) { Preload: true, }, Angular: plugins.AngularMeta{Detected: true}, + FS: &pluginfakes.FakePluginFS{}, }, }, } @@ -404,12 +408,12 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) { }, }, { - desc: "app plugin with CDN class", + desc: "app plugin with CDN fs", pluginStore: func() pluginstore.Store { return &pluginstore.FakePluginStore{ PluginList: []pluginstore.Plugin{ { - Class: plugins.ClassCDN, + Class: plugins.ClassExternal, Module: fmt.Sprintf("/%s/module.js", "test-app"), JSONData: plugins.JSONData{ ID: "test-app", @@ -417,6 +421,9 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) { Type: plugins.TypeApp, Preload: true, }, + FS: &pluginfakes.FakePluginFS{TypeFunc: func() plugins.FSType { + return plugins.FSTypeCDN + }}, }, }, } @@ -545,6 +552,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_translations(t *testing.T) { "en-US": "public/plugins/test-app/locales/en-US/test-app.json", "pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json", }, + FS: &pluginfakes.FakePluginFS{}, }, }, } @@ -594,6 +602,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_translations(t *testing.T) { "en-US": "public/plugins/test-app/locales/en-US/test-app.json", "pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json", }, + FS: &pluginfakes.FakePluginFS{}, }, }, } @@ -633,6 +642,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_translations(t *testing.T) { "en-US": "public/plugins/test-app/locales/en-US/test-app.json", "pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json", }, + FS: &pluginfakes.FakePluginFS{}, }, }, } diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index e50f059e815..9b719b8b180 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -72,12 +72,27 @@ type UpdateInfo struct { type FS interface { fs.FS - Type() string + Type() FSType Base() string Files() ([]string, error) Rel(string) (string, error) } +type FSType string + +const ( + FSTypeCDN FSType = "cdn" + FSTypeLocal FSType = "local" +) + +func (f FSType) CDN() bool { + return f == FSTypeCDN +} + +func (f FSType) Local() bool { + return f == FSTypeLocal +} + type FSRemover interface { Remove() error } diff --git a/pkg/plugins/localfiles.go b/pkg/plugins/localfiles.go index a6584641a55..073ab0e844d 100644 --- a/pkg/plugins/localfiles.go +++ b/pkg/plugins/localfiles.go @@ -30,8 +30,8 @@ func NewLocalFS(basePath string) LocalFS { return LocalFS{basePath: basePath} } -func (f LocalFS) Type() string { - return "local" +func (f LocalFS) Type() FSType { + return FSTypeLocal } // fileIsAllowed takes an absolute path to a file and an os.FileInfo for that file, and it checks if access to that @@ -219,7 +219,7 @@ func NewStaticFS(fs FS) (StaticFS, error) { }, nil } -func (f StaticFS) Type() string { +func (f StaticFS) Type() FSType { return f.FS.Type() } diff --git a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go index 98840189389..0ee0f9d9ee9 100644 --- a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go +++ b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector.go @@ -36,7 +36,7 @@ func NewPatternListInspector(detectorsProvider angulardetector.DetectorsProvider func (i *PatternsListInspector) Inspect(ctx context.Context, p *plugins.Plugin) (isAngular bool, err error) { // CDN plugins are ignored because they should not be using Angular - if p.Class == plugins.ClassCDN { + if p.FS.Type().CDN() { return false, nil } diff --git a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go index 47c5af990b9..1e29443d97d 100644 --- a/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go +++ b/pkg/plugins/manager/loader/angular/angularinspector/angularinspector_test.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector" + "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" ) type fakeDetector struct { @@ -81,7 +82,11 @@ func TestPatternsListInspector(t *testing.T) { { name: "CDN plugins return false without calling detectors", plugin: &plugins.Plugin{ - Class: plugins.ClassCDN, + FS: &pluginfakes.FakePluginFS{ + TypeFunc: func() plugins.FSType { + return plugins.FSTypeCDN + }, + }, }, fakeDetectors: []*fakeDetector{ {returns: true}, diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index e6bbc257f23..d4943cee865 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -18,8 +18,6 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" ) -const concurrencyLimit = 32 - type Loader struct { cfg *pluginsCfg.PluginManagementCfg discovery discovery.Discoverer @@ -87,37 +85,13 @@ func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins st = time.Now() validatedPlugins := []*plugins.Plugin{} - type validateResult struct { - bootstrappedPlugin *plugins.Plugin - err error - } - validateResults := make(chan validateResult, len(bootstrappedPlugins)) - - var limitSize int - if src.PluginClass(ctx) == plugins.ClassCDN { - limitSize = min(len(bootstrappedPlugins), concurrencyLimit) - } else { - limitSize = 1 - } - limit := make(chan struct{}, limitSize) for _, bootstrappedPlugin := range bootstrappedPlugins { - limit <- struct{}{} - go func(p *plugins.Plugin) { - err := l.validation.Validate(ctx, p) - validateResults <- validateResult{ - bootstrappedPlugin: bootstrappedPlugin, - err: err, - } - <-limit - }(bootstrappedPlugin) - } - for i := 0; i < len(bootstrappedPlugins); i++ { - r := <-validateResults - if r.err != nil { - l.recordError(ctx, r.bootstrappedPlugin, r.err) + err := l.validation.Validate(ctx, bootstrappedPlugin) + if err != nil { + l.recordError(ctx, bootstrappedPlugin, err) continue } - validatedPlugins = append(validatedPlugins, r.bootstrappedPlugin) + validatedPlugins = append(validatedPlugins, bootstrappedPlugin) } l.log.Debug("Validated", "class", src.PluginClass(ctx), "duration", time.Since(st), "total", len(validatedPlugins)) diff --git a/pkg/plugins/manager/pipeline/validation/steps.go b/pkg/plugins/manager/pipeline/validation/steps.go index bffac88fc0b..02b8b119884 100644 --- a/pkg/plugins/manager/pipeline/validation/steps.go +++ b/pkg/plugins/manager/pipeline/validation/steps.go @@ -57,7 +57,7 @@ func newModuleJSValidator() *ModuleJSValidator { func (v *ModuleJSValidator) Validate(_ context.Context, p *plugins.Plugin) error { // CDN plugins are ignored because the module.js is guaranteed to exist - if p.Class == plugins.ClassCDN { + if p.FS.Type().CDN() { return nil } @@ -96,6 +96,11 @@ func newAngularDetector(cfg *config.PluginManagementCfg, angularInspector angula } func (a *AngularDetector) Validate(ctx context.Context, p *plugins.Plugin) error { + // CDN plugins are ignored because they should not be using Angular + if p.FS.Type().CDN() { + return nil + } + if p.IsExternalPlugin() { var err error diff --git a/pkg/plugins/manager/pluginfakes/fakes.go b/pkg/plugins/manager/pluginfakes/fakes.go index 614c95075ad..e449fd9bb2e 100644 --- a/pkg/plugins/manager/pluginfakes/fakes.go +++ b/pkg/plugins/manager/pluginfakes/fakes.go @@ -433,6 +433,7 @@ func (f *FakeActionSetRegistry) RegisterActionSets(_ context.Context, _ string, type FakePluginFS struct { OpenFunc func(name string) (fs.File, error) RemoveFunc func() error + TypeFunc func() plugins.FSType RelFunc func(string) (string, error) base string @@ -444,10 +445,6 @@ func NewFakePluginFS(base string) *FakePluginFS { } } -func (f *FakePluginFS) Type() string { - return "fake" -} - func (f *FakePluginFS) Open(name string) (fs.File, error) { if f.OpenFunc != nil { return f.OpenFunc(name) @@ -462,6 +459,13 @@ func (f *FakePluginFS) Rel(_ string) (string, error) { return "", nil } +func (f *FakePluginFS) Type() plugins.FSType { + if f.TypeFunc != nil { + return f.TypeFunc() + } + return "fake" +} + func (f *FakePluginFS) Base() string { return f.base } diff --git a/pkg/plugins/manager/signature/manifest_test.go b/pkg/plugins/manager/signature/manifest_test.go index 5b8125d15fa..d399268b464 100644 --- a/pkg/plugins/manager/signature/manifest_test.go +++ b/pkg/plugins/manager/signature/manifest_test.go @@ -345,7 +345,7 @@ func newPathSeparatorOverrideFS(sep string, ufs plugins.FS) (fsPathSeparatorFile }, nil } -func (f fsPathSeparatorFiles) Type() string { +func (f fsPathSeparatorFiles) Type() plugins.FSType { return f.FS.Type() } diff --git a/pkg/plugins/pluginassets/pluginassets_test.go b/pkg/plugins/pluginassets/pluginassets_test.go index 3dede1ce9f0..4f0520d115b 100644 --- a/pkg/plugins/pluginassets/pluginassets_test.go +++ b/pkg/plugins/pluginassets/pluginassets_test.go @@ -42,15 +42,6 @@ func TestLocalProvider_Module(t *testing.T) { }, expected: "public/plugins/external-plugin/module.js", }, - { - name: "CDN plugin should use standard path", - plugin: PluginInfo{ - JsonData: plugins.JSONData{ID: "cdn-plugin"}, - Class: plugins.ClassCDN, - FS: plugins.NewLocalFS("/cdn/plugins/cdn-plugin"), - }, - expected: "public/plugins/cdn-plugin/module.js", - }, } for _, tt := range tests { diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 2c394dcc632..2d8241008d9 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -496,7 +496,7 @@ func (p *Plugin) IsCorePlugin() bool { } func (p *Plugin) IsExternalPlugin() bool { - return !p.IsCorePlugin() + return p.Class == ClassExternal } type Class string @@ -504,7 +504,6 @@ type Class string const ( ClassCore Class = "core" ClassExternal Class = "external" - ClassCDN Class = "cdn" ) func (c Class) String() string { diff --git a/pkg/plugins/test_utils.go b/pkg/plugins/test_utils.go index 15545bcedbd..f2a1dae3dc1 100644 --- a/pkg/plugins/test_utils.go +++ b/pkg/plugins/test_utils.go @@ -47,7 +47,7 @@ func (f inMemoryFS) Open(fn string) (fs.File, error) { return &inMemoryFile{path: fn, reader: bytes.NewReader(f.files[fn])}, nil } -func (f inMemoryFS) Type() string { +func (f inMemoryFS) Type() FSType { return "in-memory" } diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index 2ac557edaaa..60f5ac757a1 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -1076,9 +1076,9 @@ func TestLoader_AngularClass(t *testing.T) { expAngularDetectionRun: true, }, { - name: "other-class plugin should run angular detection", - class: "CDN", // (enterprise-only class) - expAngularDetectionRun: true, + name: "other class plugin should skip angular detection", + class: "foo", + expAngularDetectionRun: false, }, } { t.Run(tc.name, func(t *testing.T) { @@ -1086,7 +1086,7 @@ func TestLoader_AngularClass(t *testing.T) { PluginClassFunc: func(ctx context.Context) plugins.Class { return tc.class }, - DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, []string{filepath.Join(testDataDir(t), "valid-v2-signature")}).Discover, + DiscoverFunc: sources.NewLocalSource(tc.class, []string{filepath.Join(testDataDir(t), "valid-v2-signature")}).Discover, } // if angularDetected = true, it means that the detection has run l := newLoaderWithOpts(t, &config.PluginManagementCfg{}, loaderDepOpts{ diff --git a/pkg/services/pluginsintegration/pipeline/steps.go b/pkg/services/pluginsintegration/pipeline/steps.go index 7ed269332de..43f1dbb06e3 100644 --- a/pkg/services/pluginsintegration/pipeline/steps.go +++ b/pkg/services/pluginsintegration/pipeline/steps.go @@ -149,7 +149,7 @@ func ReportFSMetrics(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, err return p, nil } - metrics.SetPluginFSInformation(p.ID, p.FS.Type()) + metrics.SetPluginFSInformation(p.ID, string(p.FS.Type())) return p, nil } diff --git a/pkg/services/pluginsintegration/pluginassets/pluginassets.go b/pkg/services/pluginsintegration/pluginassets/pluginassets.go index cecf8ea78dc..4d9a7ec1a53 100644 --- a/pkg/services/pluginsintegration/pluginassets/pluginassets.go +++ b/pkg/services/pluginsintegration/pluginassets/pluginassets.go @@ -70,12 +70,12 @@ func (s *Service) LoadingStrategy(_ context.Context, p pluginstore.Plugin) plugi // Since the parent plugin is not explicitly configured as script loading compatible, // If the plugin is either loaded from the CDN (via its parent) or contains Angular, we should use fetch - if s.cdnEnabled(p.Parent.ID, p.Class) || p.Angular.Detected { + if s.cdnEnabled(p.Parent.ID, p.FS) || p.Angular.Detected { return plugins.LoadingStrategyFetch } } - if !s.cdnEnabled(p.ID, p.Class) && !p.Angular.Detected { + if !s.cdnEnabled(p.ID, p.FS) && !p.Angular.Detected { return plugins.LoadingStrategyScript } @@ -142,7 +142,7 @@ func (s *Service) moduleHash(ctx context.Context, p pluginstore.Plugin, childFSB // CDN plugins have the version as part of the URL, which acts as a cache-buster. // Needed due to: https://github.com/grafana/plugin-tools/pull/1426 // FS plugins build before this change will have SRI mismatch issues. - if !s.cdnEnabled(p.ID, p.Class) { + if !s.cdnEnabled(p.ID, p.FS) { return "", nil } @@ -185,8 +185,8 @@ func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool { return false } -func (s *Service) cdnEnabled(pluginID string, class plugins.Class) bool { - return s.cdn.PluginSupported(pluginID) || class == plugins.ClassCDN +func (s *Service) cdnEnabled(pluginID string, fs plugins.FS) bool { + return s.cdn.PluginSupported(pluginID) || fs.Type().CDN() } // convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks. diff --git a/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go b/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go index a747e7e4e89..28f9d015fff 100644 --- a/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go +++ b/pkg/services/pluginsintegration/pluginassets/pluginassets_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/pluginscdn" @@ -58,7 +59,7 @@ func TestService_Calculate(t *testing.T) { pluginSettings: newPluginSettings(pluginID, map[string]string{ CreatePluginVersionCfgKey: futureVersion, }), - plugin: newPlugin(pluginID, withAngular(false)), + plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())), expected: plugins.LoadingStrategyScript, }, { @@ -66,16 +67,16 @@ func TestService_Calculate(t *testing.T) { pluginSettings: newPluginSettings(pluginID, map[string]string{ // NOTE: cdn key is not set }), - plugin: newPlugin(pluginID, withAngular(false)), + plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())), expected: plugins.LoadingStrategyScript, }, { - name: "Expected LoadingStrategyScript when create-plugin version is not compatible, plugin is not angular, is not configured as CDN enabled and does not have the CDN class", + name: "Expected LoadingStrategyScript when create-plugin version is not compatible, plugin is not angular, is not configured as CDN enabled and does not have a CDN fs", pluginSettings: newPluginSettings(pluginID, map[string]string{ CreatePluginVersionCfgKey: incompatVersion, // NOTE: cdn key is not set }), - plugin: newPlugin(pluginID, withAngular(false), withClass(plugins.ClassExternal)), + plugin: newPlugin(pluginID, withAngular(false), withClass(plugins.ClassExternal), withFS(plugins.NewFakeFS())), expected: plugins.LoadingStrategyScript, }, { @@ -107,14 +108,14 @@ func TestService_Calculate(t *testing.T) { { name: "Expected LoadingStrategyFetch when parent create-plugin version is not set, is not configured as CDN enabled and plugin is angular", pluginSettings: setting.PluginSettings{}, - plugin: newPlugin(pluginID, withAngular(true), func(p pluginstore.Plugin) pluginstore.Plugin { + plugin: newPlugin(pluginID, withAngular(true), withFS(plugins.NewFakeFS()), func(p pluginstore.Plugin) pluginstore.Plugin { p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"} return p }), expected: plugins.LoadingStrategyFetch, }, { - name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular, is configured as CDN enabled and does not have the CDN class", + name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular, and plugin is configured as CDN enabled", pluginSettings: newPluginSettings(pluginID, map[string]string{ "cdn": "true", CreatePluginVersionCfgKey: incompatVersion, @@ -127,7 +128,7 @@ func TestService_Calculate(t *testing.T) { pluginSettings: newPluginSettings(pluginID, map[string]string{ CreatePluginVersionCfgKey: incompatVersion, }), - plugin: newPlugin(pluginID, withAngular(true)), + plugin: newPlugin(pluginID, withAngular(true), withFS(plugins.NewFakeFS())), expected: plugins.LoadingStrategyFetch, }, { @@ -140,19 +141,25 @@ func TestService_Calculate(t *testing.T) { expected: plugins.LoadingStrategyFetch, }, { - name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular and has the CDN class", + name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular and has a CDN fs", pluginSettings: newPluginSettings(pluginID, map[string]string{ CreatePluginVersionCfgKey: incompatVersion, }), - plugin: newPlugin(pluginID, withAngular(false), withClass(plugins.ClassCDN)), + plugin: newPlugin(pluginID, withAngular(false), withFS( + &pluginfakes.FakePluginFS{ + TypeFunc: func() plugins.FSType { + return plugins.FSTypeCDN + }, + }, + )), expected: plugins.LoadingStrategyFetch, }, { - name: "Expected LoadingStrategyScript when plugin setting create-plugin version is badly formatted, plugin is not configured as CDN enabled and does not have the CDN class", + name: "Expected LoadingStrategyScript when plugin setting create-plugin version is badly formatted, plugin is not configured as CDN enabled and does not have a CDN fs", pluginSettings: newPluginSettings(pluginID, map[string]string{ CreatePluginVersionCfgKey: "invalidSemver", }), - plugin: newPlugin(pluginID, withAngular(false)), + plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())), expected: plugins.LoadingStrategyScript, }, } @@ -183,9 +190,9 @@ func TestService_ModuleHash(t *testing.T) { features *config.Features store []pluginstore.Plugin - // Can be used to configure plugin's class - // cdn class = loaded from CDN with no files on disk - // external class = files on disk but served from CDN only if cdn=true + // Can be used to configure plugin's fs + // fs cdn type = loaded from CDN with no files on disk + // fs local type = files on disk but served from CDN only if cdn=true plugin pluginstore.Plugin // When true, set cdn=true in config @@ -204,7 +211,7 @@ func TestService_ModuleHash(t *testing.T) { pluginID, withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - withClass(plugins.ClassCDN), + withClass(plugins.ClassExternal), ), cdn: true, features: &config.Features{SriChecksEnabled: true}, @@ -236,7 +243,6 @@ func TestService_ModuleHash(t *testing.T) { pluginID, withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - withClass(plugins.ClassCDN), ), cdn: true, features: &config.Features{SriChecksEnabled: false}, @@ -261,7 +267,6 @@ func TestService_ModuleHash(t *testing.T) { parentPluginID, withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), - withClass(plugins.ClassCDN), ), }, plugin: newPlugin( @@ -269,7 +274,6 @@ func TestService_ModuleHash(t *testing.T) { withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "datasource"))), withParent(parentPluginID), - withClass(plugins.ClassCDN), ), cdn: true, features: &config.Features{SriChecksEnabled: true}, @@ -284,7 +288,6 @@ func TestService_ModuleHash(t *testing.T) { parentPluginID, withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), - withClass(plugins.ClassCDN), ), }, plugin: newPlugin( @@ -292,7 +295,6 @@ func TestService_ModuleHash(t *testing.T) { withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one"))), withParent(parentPluginID), - withClass(plugins.ClassCDN), ), cdn: true, features: &config.Features{SriChecksEnabled: true}, @@ -308,14 +310,12 @@ func TestService_ModuleHash(t *testing.T) { "grand-parent-app", withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested"))), - withClass(plugins.ClassCDN), ), newPlugin( "parent-datasource", withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource"))), withParent("grand-parent-app"), - withClass(plugins.ClassCDN), ), }, plugin: newPlugin( @@ -323,7 +323,6 @@ func TestService_ModuleHash(t *testing.T) { withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one"))), withParent("parent-datasource"), - withClass(plugins.ClassCDN), ), cdn: true, features: &config.Features{SriChecksEnabled: true}, @@ -378,11 +377,17 @@ func TestService_ModuleHash(t *testing.T) { t.Run(tc.name, func(t *testing.T) { var pluginSettings setting.PluginSettings if tc.cdn { - pluginSettings = newPluginSettings(pluginID, map[string]string{ - "cdn": "true", - }) - } else { - require.NotEqual(t, plugins.ClassCDN, tc.plugin.Class, "plugin should not have the CDN class because CDN is disabled") + pluginSettings = setting.PluginSettings{ + pluginID: { + "cdn": "true", + }, + parentPluginID: map[string]string{ + "cdn": "true", + }, + "grand-parent-app": map[string]string{ + "cdn": "true", + }, + } } features := tc.features if features == nil { @@ -439,8 +444,24 @@ func TestService_ModuleHash_Cache(t *testing.T) { withInfo(plugins.Info{Version: "1.0.0"}), withSignatureStatus(plugins.SignatureStatusValid), withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))), - withClass(plugins.ClassCDN), ) + + pCfg = &config.PluginManagementCfg{ + PluginsCDNURLTemplate: "https://cdn.grafana.com", + PluginSettings: setting.PluginSettings{ + pluginID: { + "cdn": "true", + }, + }, + Features: config.Features{SriChecksEnabled: true}, + } + svc = ProvideService( + pCfg, + pluginscdn.ProvideService(pCfg), + signature.ProvideService(pCfg, statickey.New()), + pluginstore.NewFakePluginStore(), + ) + k := svc.moduleHashCacheKey(pV1) _, ok := svc.moduleHashCache.Load(k) @@ -461,7 +482,6 @@ func TestService_ModuleHash_Cache(t *testing.T) { withSignatureStatus(plugins.SignatureStatusValid), // different fs for different hash withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))), - withClass(plugins.ClassCDN), ) mhV2 := svc.ModuleHash(context.Background(), pV2) require.NotEqual(t, mhV2, mhV1, "different version should have different hash") From 41276676eb62912a58cc05684e29986f5731563c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Thu, 20 Nov 2025 15:12:07 +0100 Subject: [PATCH 024/378] Provisioning: add retry logic for transient errors in Kubernetes client (#114215) * feat: add retry logic for transient errors in Kubernetes client Add retry wrapper for dynamic.ResourceInterface that automatically retries transient errors using Kubernetes' wait.ExponentialBackoff utility. - Implements retry logic with exponential backoff for all Kubernetes API operations - Detects transient errors: ServiceUnavailable, ServerTimeout, TooManyRequests, InternalError, Timeout, and network errors - Uses wait.ExponentialBackoff from k8s.io/apimachinery/pkg/util/wait - Respects context cancellation - Includes comprehensive unit tests Part of https://github.com/grafana/git-ui-sync-project/issues/634 * docs: add detailed documentation for defaultRetryBackoff Document when retry attempts will happen, what errors trigger retries, and the retry behavior (attempts, delays, exponential backoff, jitter). * feat: add logging and increase retry attempts for Kubernetes client - Add context logger to track retry attempts (Info for retries, Warn for exhaustion) - Increase retry attempts from 5 to 8 steps (~10 seconds total retry window) - Document when all retry attempts will fail: * API server completely unavailable/unreachable * Network connectivity issues persist beyond retry window * Consistent transient errors for entire retry duration * Context cancellation before retries complete * chore: update retry client documentation * fix: resolve linting issues in retry client - Replace type assertions with errors.As for wrapped errors - Remove deprecated Temporary() check (deprecated since Go 1.18) - Update tests to remove temporary error test case * fix: resolve staticcheck S1008 linting issue in retry_client.go Simplify return statement to use errors.As directly instead of if-return pattern --- .../apis/provisioning/resources/client.go | 6 +- .../provisioning/resources/retry_client.go | 296 ++++++++++ .../resources/retry_client_test.go | 507 ++++++++++++++++++ 3 files changed, 807 insertions(+), 2 deletions(-) create mode 100644 pkg/registry/apis/provisioning/resources/retry_client.go create mode 100644 pkg/registry/apis/provisioning/resources/retry_client_test.go diff --git a/pkg/registry/apis/provisioning/resources/client.go b/pkg/registry/apis/provisioning/resources/client.go index f12bcf7b053..388e5311f72 100644 --- a/pkg/registry/apis/provisioning/resources/client.go +++ b/pkg/registry/apis/provisioning/resources/client.go @@ -221,10 +221,11 @@ func (c *resourceClients) ForKind(ctx context.Context, gvk schema.GroupVersionKi return nil, schema.GroupVersionResource{}, err } } + baseClient := dynamic.Resource(gvr).Namespace(c.namespace) info = &clientInfo{ gvk: gvk, gvr: gvr, - client: dynamic.Resource(gvr).Namespace(c.namespace), + client: newRetryResourceInterface(baseClient, defaultRetryBackoff()), } c.byKind[gvk] = info c.byResource[gvr] = info @@ -274,10 +275,11 @@ func (c *resourceClients) ForResource(ctx context.Context, gvr schema.GroupVersi return nil, schema.GroupVersionKind{}, fmt.Errorf("getting kind for resource for %s: %w", gvr.String(), err) } } + baseClient := dynamic.Resource(gvr).Namespace(c.namespace) info = &clientInfo{ gvk: gvk, gvr: gvr, - client: dynamic.Resource(gvr).Namespace(c.namespace), + client: newRetryResourceInterface(baseClient, defaultRetryBackoff()), } c.byKind[gvk] = info c.byResource[gvr] = info diff --git a/pkg/registry/apis/provisioning/resources/retry_client.go b/pkg/registry/apis/provisioning/resources/retry_client.go new file mode 100644 index 00000000000..cc09443e018 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/retry_client.go @@ -0,0 +1,296 @@ +package resources + +import ( + "context" + "errors" + "net" + "time" + + "github.com/grafana/grafana-app-sdk/logging" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" +) + +// defaultRetryBackoff returns a default backoff configuration for retries. +// +// Retry attempts will happen when: +// - The Kubernetes API returns transient errors: ServiceUnavailable (503), ServerTimeout (504), +// TooManyRequests (429), InternalError (500), or Timeout errors +// - Network errors occur: connection timeouts, temporary network failures, or connection errors +// +// The retry behavior: +// - Total attempts: 8 (1 initial attempt + 7 retries) +// - Initial delay: 100ms before the first retry +// - Exponential backoff: delay doubles after each failed attempt (100ms → 200ms → 400ms → 800ms → 1.6s → 3.2s → 5s) +// - Maximum delay: capped at 5 seconds +// - Jitter: 10% randomization to prevent thundering herd problems +// - Total retry window: approximately 10 seconds from first attempt to last retry +// +// All attempts will fail when: +// - The Kubernetes API server is completely unavailable or unreachable +// - Network connectivity issues persist beyond the retry window (~10 seconds) +// - The API server returns transient errors consistently for the entire retry duration +// - Context cancellation occurs before retries complete +// +// Non-transient errors (e.g., NotFound, BadRequest, Forbidden) are not retried and returned immediately. +func defaultRetryBackoff() wait.Backoff { + return wait.Backoff{ + Duration: 100 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 8, // 1 initial attempt + 7 retries = 8 total attempts (~10s total retry window) + Cap: 5 * time.Second, + } +} + +// retryResourceInterface wraps a dynamic.ResourceInterface with retry logic for transient errors +type retryResourceInterface struct { + client dynamic.ResourceInterface + backoff wait.Backoff +} + +// newRetryResourceInterface creates a new ResourceInterface wrapper with retry logic +func newRetryResourceInterface(client dynamic.ResourceInterface, backoff wait.Backoff) dynamic.ResourceInterface { + return &retryResourceInterface{ + client: client, + backoff: backoff, + } +} + +// isTransientError determines if an error is transient and should be retried +func isTransientError(err error) bool { + if err == nil { + return false + } + + // Check for Kubernetes API transient errors + if apierrors.IsServiceUnavailable(err) { + return true + } + if apierrors.IsServerTimeout(err) { + return true + } + if apierrors.IsTooManyRequests(err) { + return true + } + if apierrors.IsInternalError(err) { + return true + } + if apierrors.IsTimeout(err) { + return true + } + + // Check for network errors + var netErr net.Error + if errors.As(err, &netErr) { + if netErr.Timeout() { + return true + } + } + + // Check for connection errors + var opErr *net.OpError + return errors.As(err, &opErr) +} + +// retryWithBackoff executes a function with exponential backoff retry logic using wait.ExponentialBackoff +func (r *retryResourceInterface) retryWithBackoff(ctx context.Context, fn func() error) error { + var lastErr error + attempt := 0 + logger := logging.FromContext(ctx) + + err := wait.ExponentialBackoff(r.backoff, func() (bool, error) { + attempt++ + + // Check if context is cancelled + if ctx.Err() != nil { + logger.Debug("Retry cancelled due to context cancellation", "attempt", attempt) + return false, ctx.Err() + } + + err := fn() + if err == nil { + if attempt > 1 { + logger.Debug("Operation succeeded after retry", "attempt", attempt) + } + return true, nil // success, stop retrying + } + + // If not a transient error, return immediately without retrying + if !isTransientError(err) { + logger.Debug("Non-transient error, not retrying", "attempt", attempt, "error", err) + return false, err + } + + // Transient error, retry + lastErr = err + logger.Info("Transient error encountered, retrying", "attempt", attempt, "max_attempts", r.backoff.Steps, "error", err) + return false, nil + }) + + // If wait.ExponentialBackoff returned an error, it means we exhausted retries + if err != nil { + if lastErr != nil { + logger.Warn("All retry attempts exhausted", "total_attempts", attempt, "error", lastErr) + return lastErr + } + logger.Warn("All retry attempts exhausted", "total_attempts", attempt, "error", err) + return err + } + + return nil +} + +// Create implements dynamic.ResourceInterface +func (r *retryResourceInterface) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.Create(ctx, obj, options, subresources...) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// Update implements dynamic.ResourceInterface +func (r *retryResourceInterface) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.Update(ctx, obj, options, subresources...) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// UpdateStatus implements dynamic.ResourceInterface +func (r *retryResourceInterface) UpdateStatus(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.UpdateStatus(ctx, obj, options) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// Delete implements dynamic.ResourceInterface +func (r *retryResourceInterface) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error { + return r.retryWithBackoff(ctx, func() error { + return r.client.Delete(ctx, name, options, subresources...) + }) +} + +// DeleteCollection implements dynamic.ResourceInterface +func (r *retryResourceInterface) DeleteCollection(ctx context.Context, options metav1.DeleteOptions, listOptions metav1.ListOptions) error { + return r.retryWithBackoff(ctx, func() error { + return r.client.DeleteCollection(ctx, options, listOptions) + }) +} + +// Get implements dynamic.ResourceInterface +func (r *retryResourceInterface) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.Get(ctx, name, options, subresources...) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// List implements dynamic.ResourceInterface +func (r *retryResourceInterface) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) { + var result *unstructured.UnstructuredList + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.List(ctx, opts) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// Watch implements dynamic.ResourceInterface +func (r *retryResourceInterface) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + // Watch operations are long-lived and shouldn't be retried in the same way + // Return the watch interface directly + return r.client.Watch(ctx, opts) +} + +// Patch implements dynamic.ResourceInterface +func (r *retryResourceInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.Patch(ctx, name, pt, data, options, subresources...) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// Apply implements dynamic.ResourceInterface +func (r *retryResourceInterface) Apply(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions, subresources ...string) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.Apply(ctx, name, obj, options, subresources...) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} + +// ApplyStatus implements dynamic.ResourceInterface +func (r *retryResourceInterface) ApplyStatus(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions) (*unstructured.Unstructured, error) { + var result *unstructured.Unstructured + var err error + + retryErr := r.retryWithBackoff(ctx, func() error { + result, err = r.client.ApplyStatus(ctx, name, obj, options) + return err + }) + + if retryErr != nil { + return nil, retryErr + } + return result, nil +} diff --git a/pkg/registry/apis/provisioning/resources/retry_client_test.go b/pkg/registry/apis/provisioning/resources/retry_client_test.go new file mode 100644 index 00000000000..dab40bcb001 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/retry_client_test.go @@ -0,0 +1,507 @@ +package resources + +import ( + "context" + "errors" + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/watch" +) + +func TestIsTransientError(t *testing.T) { + tests := []struct { + name string + err error + expected bool + }{ + { + name: "nil error", + err: nil, + expected: false, + }, + { + name: "service unavailable", + err: apierrors.NewServiceUnavailable("service unavailable"), + expected: true, + }, + { + name: "server timeout", + err: apierrors.NewServerTimeout(schema.GroupResource{}, "operation", 0), + expected: true, + }, + { + name: "too many requests", + err: apierrors.NewTooManyRequests("too many requests", 0), + expected: true, + }, + { + name: "internal error", + err: apierrors.NewInternalError(errors.New("internal error")), + expected: true, + }, + { + name: "network timeout error", + err: &net.DNSError{Err: "timeout", IsTimeout: true}, + expected: true, + }, + // Note: Temporary() is deprecated in Go 1.18+, so we no longer check for temporary errors + // Timeout errors are still checked and will be retried + { + name: "network op error", + err: &net.OpError{Op: "read", Err: errors.New("connection refused")}, + expected: true, + }, + { + name: "not found error", + err: apierrors.NewNotFound(schema.GroupResource{}, "resource"), + expected: false, + }, + { + name: "bad request error", + err: apierrors.NewBadRequest("bad request"), + expected: false, + }, + { + name: "forbidden error", + err: apierrors.NewForbidden(schema.GroupResource{}, "resource", errors.New("forbidden")), + expected: false, + }, + { + name: "generic error", + err: errors.New("generic error"), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isTransientError(tt.err) + assert.Equal(t, tt.expected, result, "isTransientError(%v) = %v, want %v", tt.err, result, tt.expected) + }) + } +} + +func TestRetryResourceInterface_Create(t *testing.T) { + tests := []struct { + name string + setupMock func(*MockDynamicResourceInterface) + backoff wait.Backoff + expectedCalls int + expectError bool + }{ + { + name: "success on first attempt", + setupMock: func(m *MockDynamicResourceInterface) { + m.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{}, nil).Once() + }, + backoff: defaultRetryBackoff(), + expectedCalls: 1, + expectError: false, + }, + { + name: "success after transient errors", + setupMock: func(m *MockDynamicResourceInterface) { + m.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Twice() + m.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + }, + backoff: wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }, + expectedCalls: 3, + expectError: false, + }, + { + name: "max retries exceeded", + setupMock: func(m *MockDynamicResourceInterface) { + m.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")) + }, + backoff: wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 3, // Only 3 steps = 2 retries + 1 initial + Cap: 100 * time.Millisecond, + }, + expectedCalls: 3, + expectError: true, + }, + { + name: "non-transient error - no retry", + setupMock: func(m *MockDynamicResourceInterface) { + m.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewBadRequest("bad request")).Once() + }, + backoff: defaultRetryBackoff(), + expectedCalls: 1, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + tt.setupMock(mockClient) + + retryClient := newRetryResourceInterface(mockClient, tt.backoff) + obj := &unstructured.Unstructured{} + _, err := retryClient.Create(context.Background(), obj, metav1.CreateOptions{}) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + mockClient.AssertNumberOfCalls(t, "Create", tt.expectedCalls) + }) + } +} + +func TestRetryResourceInterface_Update(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Once() + mockClient.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + obj := &unstructured.Unstructured{} + result, err := retryClient.Update(context.Background(), obj, metav1.UpdateOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "Update", 2) +} + +func TestRetryResourceInterface_Get(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServerTimeout(schema.GroupResource{}, "operation", 0)).Twice() + mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + result, err := retryClient.Get(context.Background(), "test-resource", metav1.GetOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "Get", 3) +} + +func TestRetryResourceInterface_Delete(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(apierrors.NewTooManyRequests("too many requests", 0)).Once() + mockClient.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + err := retryClient.Delete(context.Background(), "test-resource", metav1.DeleteOptions{}) + + assert.NoError(t, err) + mockClient.AssertNumberOfCalls(t, "Delete", 2) +} + +func TestRetryResourceInterface_List(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("List", mock.Anything, mock.Anything). + Return(nil, apierrors.NewInternalError(errors.New("internal error"))).Once() + mockClient.On("List", mock.Anything, mock.Anything). + Return(&unstructured.UnstructuredList{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + result, err := retryClient.List(context.Background(), metav1.ListOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "List", 2) +} + +func TestRetryResourceInterface_Patch(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, &net.OpError{Op: "read", Err: errors.New("connection refused")}).Once() + mockClient.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + result, err := retryClient.Patch(context.Background(), "test-resource", types.MergePatchType, []byte(`{}`), metav1.PatchOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "Patch", 2) +} + +func TestRetryResourceInterface_Apply(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("Apply", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Once() + mockClient.On("Apply", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + obj := &unstructured.Unstructured{} + result, err := retryClient.Apply(context.Background(), "test-resource", obj, metav1.ApplyOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "Apply", 2) +} + +func TestRetryResourceInterface_UpdateStatus(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("UpdateStatus", mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Once() + mockClient.On("UpdateStatus", mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + obj := &unstructured.Unstructured{} + result, err := retryClient.UpdateStatus(context.Background(), obj, metav1.UpdateOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "UpdateStatus", 2) +} + +func TestRetryResourceInterface_ApplyStatus(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("ApplyStatus", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Once() + mockClient.On("ApplyStatus", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&unstructured.Unstructured{}, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + obj := &unstructured.Unstructured{} + result, err := retryClient.ApplyStatus(context.Background(), "test-resource", obj, metav1.ApplyOptions{}) + + assert.NoError(t, err) + assert.NotNil(t, result) + mockClient.AssertNumberOfCalls(t, "ApplyStatus", 2) +} + +func TestRetryResourceInterface_DeleteCollection(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything). + Return(apierrors.NewServiceUnavailable("service unavailable")).Once() + mockClient.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything). + Return(nil).Once() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 10 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 100 * time.Millisecond, + }) + + err := retryClient.DeleteCollection(context.Background(), metav1.DeleteOptions{}, metav1.ListOptions{}) + + assert.NoError(t, err) + mockClient.AssertNumberOfCalls(t, "DeleteCollection", 2) +} + +func TestRetryResourceInterface_Watch(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockWatch := &mockWatch{} + mockClient.On("Watch", mock.Anything, mock.Anything).Return(mockWatch, nil).Once() + + retryClient := newRetryResourceInterface(mockClient, defaultRetryBackoff()) + + watch, err := retryClient.Watch(context.Background(), metav1.ListOptions{}) + + assert.NoError(t, err) + assert.Equal(t, mockWatch, watch) + // Watch should not retry, so only one call + mockClient.AssertNumberOfCalls(t, "Watch", 1) +} + +func TestRetryResourceInterface_ContextCancellation(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + mockClient.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Maybe() + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 100 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 1 * time.Second, + }) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately + + obj := &unstructured.Unstructured{} + _, err := retryClient.Create(ctx, obj, metav1.CreateOptions{}) + + assert.Error(t, err) + assert.Equal(t, context.Canceled, err) + // Should not retry after context cancellation + mockClient.AssertNumberOfCalls(t, "Create", 0) +} + +func TestRetryResourceInterface_ExponentialBackoff(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + callCount := 0 + + // Set up expectations for multiple calls + mockClient.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + callCount++ + }). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")).Twice() + mockClient.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + callCount++ + }). + Return(&unstructured.Unstructured{}, nil).Once() + + start := time.Now() + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 50 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, + Cap: 1 * time.Second, + }) + + obj := &unstructured.Unstructured{} + _, err := retryClient.Create(context.Background(), obj, metav1.CreateOptions{}) + + duration := time.Since(start) + + assert.NoError(t, err) + assert.Equal(t, 3, callCount) + // Should have waited at least initialDelay + (initialDelay * multiplier) = 50ms + 100ms = 150ms + assert.GreaterOrEqual(t, duration, 100*time.Millisecond) + // But not too long (with some buffer for jitter) + assert.Less(t, duration, 1*time.Second) +} + +func TestRetryResourceInterface_MaxDelayRespected(t *testing.T) { + mockClient := &MockDynamicResourceInterface{} + callCount := 0 + + // Set up expectations for multiple calls - always return transient error + mockClient.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + callCount++ + }). + Return(nil, apierrors.NewServiceUnavailable("service unavailable")). + Maybe() // Allow unlimited calls + + retryClient := newRetryResourceInterface(mockClient, wait.Backoff{ + Duration: 50 * time.Millisecond, + Factor: 2.0, + Jitter: 0.1, + Steps: 5, // 5 total attempts + Cap: 200 * time.Millisecond, // Max delay is 200ms (cap should prevent exponential growth beyond this) + }) + + start := time.Now() + obj := &unstructured.Unstructured{} + _, err := retryClient.Create(context.Background(), obj, metav1.CreateOptions{}) + duration := time.Since(start) + + assert.Error(t, err) + // Should have retried multiple times + assert.GreaterOrEqual(t, callCount, 2, "should have retried at least once") + // Verify that the delay was capped - if it wasn't capped, duration would be much longer + // With cap at 200ms and Steps=5, max total time should be reasonable + // The cap ensures delays don't grow exponentially beyond 200ms + assert.Less(t, duration, 2*time.Second, "duration should be reasonable due to cap") +} + +func TestDefaultRetryBackoff(t *testing.T) { + backoff := defaultRetryBackoff() + + assert.Equal(t, 100*time.Millisecond, backoff.Duration) + assert.Equal(t, 2.0, backoff.Factor) + assert.Equal(t, 0.1, backoff.Jitter) + assert.Equal(t, 8, backoff.Steps) // Updated to 8 steps for ~10s total retry window + assert.Equal(t, 5*time.Second, backoff.Cap) +} + +// mockWatch implements watch.Interface for testing +type mockWatch struct{} + +func (m *mockWatch) Stop() {} + +func (m *mockWatch) ResultChan() <-chan watch.Event { + return make(chan watch.Event) +} + +var _ watch.Interface = (*mockWatch)(nil) From 834f1c5e98e75083e777d0cc9ed46813fa0214f1 Mon Sep 17 00:00:00 2001 From: Kristina Demeshchik Date: Thu, 20 Nov 2025 09:13:19 -0500 Subject: [PATCH 025/378] TS checker: Increase memory limit (#114236) Increase ts checker memory limit --- scripts/webpack/webpack.dev.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index 050f54a2c3a..f48aada0ed7 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -135,7 +135,7 @@ module.exports = (env = {}) => { async: true, // don't block webpack emit typescript: { mode: 'write-references', - memoryLimit: 5096, + memoryLimit: 8192, diagnosticOptions: { semantic: true, syntactic: true, From 7db72e03b55f8293061e387aebea4ad4ce1e3712 Mon Sep 17 00:00:00 2001 From: Robby Milo Date: Thu, 20 Nov 2025 07:39:10 -0700 Subject: [PATCH 026/378] remove image rendering docs (#114217) --- .../setup-grafana/image-rendering/_index.md | 617 ------------------ .../image-rendering/monitoring/index.md | 221 ------- .../image-rendering/troubleshooting/index.md | 156 ----- 3 files changed, 994 deletions(-) delete mode 100644 docs/sources/setup-grafana/image-rendering/_index.md delete mode 100644 docs/sources/setup-grafana/image-rendering/monitoring/index.md delete mode 100644 docs/sources/setup-grafana/image-rendering/troubleshooting/index.md diff --git a/docs/sources/setup-grafana/image-rendering/_index.md b/docs/sources/setup-grafana/image-rendering/_index.md deleted file mode 100644 index 32df8dc2154..00000000000 --- a/docs/sources/setup-grafana/image-rendering/_index.md +++ /dev/null @@ -1,617 +0,0 @@ ---- -aliases: - - ../administration/image_rendering/ - - ../image-rendering/ -description: Image rendering -keywords: - - grafana - - image - - rendering - - plugin -labels: - products: - - enterprise - - oss -title: Set up image rendering -weight: 1000 ---- - -# Set up image rendering - -Grafana supports automatic rendering of panels as PNG images. This allows Grafana to automatically generate images of your panels to include in alert notifications, [PDF export](../../dashboards/create-reports/#export-dashboard-as-pdf), and [Reporting](../../dashboards/create-reports/). PDF Export and Reporting are available only in [Grafana Enterprise](../../introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud/). - -While an image is being rendered, the PNG image is temporarily written to the file system. When the image is rendered, the PNG image is temporarily written to the `png` folder in the Grafana `data` folder. - -A background job runs every 10 minutes and removes temporary images. You can configure how long an image should be stored before being removed by configuring the [temp_data_lifetime](../configure-grafana/#temp_data_lifetime) setting. - -You can also render a PNG by hovering over the panel to display the actions menu in the top-right corner, and then clicking **Share > Share link**. The **Render image** option is displayed in the link settings. - -## Alerting and render limits - -Alert notifications can include images, but rendering many images at the same time can overload the server where the renderer is running. For instructions of how to configure this, see [max_concurrent_screenshots](../configure-grafana/#max_concurrent_screenshots). - -## Install Grafana Image Renderer plugin - -{{< admonition type="caution" >}} -Starting with Grafana v12.2, the Grafana Image Renderer plugin is deprecated and is no longer maintained. - -Instead, use the Grafana Image Renderer remote rendering service. -{{< /admonition >}} - -{{< admonition type="note" >}} -All PhantomJS support has been removed. Instead, use the Grafana Image Renderer plugin or remote rendering service. -{{< /admonition >}} - -To install the plugin, refer to the [Grafana Image Renderer Installation instructions](/grafana/plugins/grafana-image-renderer/?tab=installation#installation). - -### Memory requirements - -Rendering images requires a lot of memory, mainly because Grafana creates browser instances in the background for the actual rendering. Grafana recommends a minimum of 16GB of free memory on the system rendering images. - -Rendering multiple images in parallel requires an even bigger memory footprint. You can use the remote rendering service in order to render images on a remote system, so your local system resources are not affected. - -## Configuration - -The Grafana Image Renderer plugin has a number of configuration options that are used in plugin or remote rendering modes. - -In plugin mode, you can specify them directly in the [Grafana configuration file](../configure-grafana/#plugingrafana-image-renderer). - -In remote rendering mode, you can specify them in a `.json` [configuration file](#configuration-file) or, for some of them, you can override the configuration defaults using environment variables. - -### Configuration file - -You can update your settings by using a configuration file, see [default.json](https://github.com/grafana/grafana-image-renderer/tree/master/default.json) for defaults. Note that any configured environment variable takes precedence over configuration file settings. - -You can volume mount your custom configuration file when starting the docker container: - -```bash -docker run -d --name=renderer --network=host -v /some/path/config.json:/home/nonroot/config.json grafana/grafana-image-renderer:latest -``` - -You can see a docker-compose example using a custom configuration file [here](https://github.com/grafana/grafana-image-renderer/tree/master/devenv/docker/custom-config). - -{{< admonition type="note" >}} -The configuration files were located in `/usr/src/app` up until v4.0.0 and later. -After this point, they are located in `/home/nonroot`. -{{< /admonition >}} - -### Security - -{{< admonition type="note" >}} -This feature is available in Image Renderer v3.6.1 and later. -{{< /admonition >}} - -You can restrict access to the rendering endpoint by specifying a secret token. The token should be configured in the Grafana configuration file and the renderer configuration file. This token is important when you run the plugin in remote rendering mode. - -Renderer versions v3.6.1 or later require a Grafana version with this feature. These include: - -- Grafana v9.1.2 or later -- Grafana v9.0.8 or later patch releases -- Grafana v8.5.11 or later patch releases -- Grafana v8.4.11 or later patch releases -- Grafana v8.3.11 or later patch releases - -```bash -AUTH_TOKEN=- -``` - -```json -{ - "service": { - "security": { - "authToken": "-" - } - } -} -``` - -See [Grafana configuration](../configure-grafana/#renderer_token) for how to configure the token in Grafana. - -### Rendering mode - -You can instruct how headless browser instances are created by configuring a rendering mode. Default is `default`, other supported values are `clustered` and `reusable`. - -#### Default - -Default mode will create a new browser instance on each request. When handling multiple concurrent requests, this mode increases memory usage as it will launch multiple browsers at the same time. If you want to set a maximum number of browser to open, you'll need to use the [clustered mode](#clustered). - -{{< admonition type="note" >}} -When using the `default` mode, it's recommended to not remove the default Chromium flag `--disable-gpu`. When receiving a lot of concurrent requests, not using this flag can cause Puppeteer `newPage` function to freeze, causing request timeouts and leaving browsers open. -{{< /admonition >}} - -```bash -RENDERING_MODE=default -``` - -```json -{ - "rendering": { - "mode": "default" - } -} -``` - -#### Clustered - -With the `clustered` mode, you can configure how many browser instances or incognito pages can execute concurrently. Default is `browser` and will ensure a maximum amount of browser instances can execute concurrently. Mode `context` will ensure a maximum amount of incognito pages can execute concurrently. You can also configure the maximum concurrency allowed, which per default is `5`, and the maximum duration of a rendering request, which per default is `30` seconds. - -Using a cluster of incognito pages is more performant and consumes less CPU and memory than a cluster of browsers. However, if one page crashes it can bring down the entire browser with it (making all the rendering requests happening at the same time fail). Also, each page isn't guaranteed to be totally clean (cookies and storage might bleed-through as seen [here](https://bugs.chromium.org/p/chromium/issues/detail?id=754576)). - -```bash -RENDERING_MODE=clustered -RENDERING_CLUSTERING_MODE=browser -RENDERING_CLUSTERING_MAX_CONCURRENCY=5 -RENDERING_CLUSTERING_TIMEOUT=30 -``` - -```json -{ - "rendering": { - "mode": "clustered", - "clustering": { - "mode": "browser", - "maxConcurrency": 5, - "timeout": 30 - } - } -} -``` - -#### Reusable (experimental) - -When using the rendering mode `reusable`, one browser instance will be created and reused. A new incognito page will be opened for each request. This mode is experimental since, if the browser instance crashes, it will not automatically be restarted. You can achieve a similar behavior using `clustered` mode with a high `maxConcurrency` setting. - -```bash -RENDERING_MODE=reusable -``` - -```json -{ - "rendering": { - "mode": "reusable" - } -} -``` - -#### Optimize the performance, CPU and memory usage of the image renderer - -The performance and resources consumption of the different modes depend a lot on the number of concurrent requests your service is handling. To understand how many concurrent requests your service is handling, [monitor your image renderer service](monitoring/). - -With no concurrent requests, the different modes show very similar performance and CPU / memory usage. - -When handling concurrent requests, we see the following trends: - -- To improve performance and reduce CPU and memory consumption, use [clustered](#clustered) mode with `RENDERING_CLUSTERING_MODE` set as `context`. This parallelizes incognito pages instead of browsers. -- If you use the [clustered](#clustered) mode with a `maxConcurrency` setting below your average number of concurrent requests, performance will drop as the rendering requests will need to wait for the other to finish before getting access to an incognito page / browser. - -To achieve better performance, monitor the machine on which your service is running. If you don't have enough memory and / or CPU, every rendering step will be slower than usual, increasing the duration of every rendering request. - -### Other available settings - -{{< admonition type="note" >}} -Please note that not all settings are available using environment variables. If there is no example using environment variable below, it means that you need to update the configuration file. -{{< /admonition >}} - -#### HTTP host - -Change the listening host of the HTTP server. Default is unset and will use the local host. - -```bash -HTTP_HOST=localhost -``` - -```json -{ - "service": { - "host": "localhost" - } -} -``` - -#### HTTP port - -Change the listening port of the HTTP server. Default is `8081`. Setting `0` will automatically assign a port not in use. - -```bash -HTTP_PORT=0 -``` - -```json -{ - "service": { - "port": 0 - } -} -``` - -#### HTTP protocol - -{{< admonition type="note" >}} -HTTPS protocol is supported in the image renderer v3.11.0 and later. -{{< /admonition >}} - -Change the protocol of the server, it can be `http` or `https`. Default is `http`. - -```bash -HTTP_PROTOCOL=https -``` - -```json -{ - "service": { - "protocol": "https" - } -} -``` - -#### HTTPS certificate and key file - -Path to the image renderer certificate and key file used to start an HTTPS server. - -```bash -HTTP_CERT_FILE=./path/to/cert -HTTP_CERT_KEY=./path/to/key -``` - -```json -{ - "service": { - "certFile": "./path/to/cert", - "certKey": "./path/to/key" - } -} -``` - -#### HTTPS min TLS version - -Minimum TLS version allowed. Accepted values are: `TLSv1.2`, `TLSv1.3`. Default is `TLSv1.2`. - -```bash -HTTP_MIN_TLS_VERSION=TLSv1.2 -``` - -```json -{ - "service": { - "minTLSVersion": "TLSv1.2" - } -} -``` - -#### Enable Prometheus metrics - -You can enable [Prometheus](https://prometheus.io/) metrics endpoint `/metrics` using the environment variable `ENABLE_METRICS`. Node.js and render request duration metrics are included, see [Enable Prometheus metrics endpoint](monitoring/#enable-prometheus-metrics-endpoint) for details. - -Default is `false`. - -```bash -ENABLE_METRICS=true -``` - -```json -{ - "service": { - "metrics": { - "enabled": true, - "collectDefaultMetrics": true, - "requestDurationBuckets": [1, 5, 7, 9, 11, 13, 15, 20, 30] - } - } -} -``` - -#### Enable detailed timing metrics - -With the [Prometheus metrics enabled](#enable-prometheus-metrics), you can also enable detailed metrics to get the duration of every rendering step. - -Default is `false`. - -```bash -# Available from v3.9.0+ -RENDERING_TIMING_METRICS=true -``` - -```json -{ - "rendering": { - "timingMetrics": true - } -} -``` - -#### Log level - -Change the log level. Default is `info` and will include log messages with level `error`, `warning` and `info`. - -```bash -LOG_LEVEL=debug -``` - -```json -{ - "service": { - "logging": { - "level": "debug", - "console": { - "json": false, - "colorize": true - } - } - } -} -``` - -#### Verbose logging - -Instruct headless browser instance whether to capture and log verbose information when rendering an image. Default is `false` and will only capture and log error messages. When enabled (`true`) debug messages are captured and logged as well. - -Note that you need to change log level to `debug`, see above, for the verbose information to be included in the logs. - -```bash -RENDERING_VERBOSE_LOGGING=true -``` - -```json -{ - "rendering": { - "verboseLogging": true - } -} -``` - -#### Capture browser output - -Instruct headless browser instance whether to output its debug and error messages into running process of remote rendering service. Default is `false`. -This can be useful to enable (`true`) when troubleshooting. - -```bash -RENDERING_DUMPIO=true -``` - -```json -{ - "rendering": { - "dumpio": true - } -} -``` - -#### Tracing - -{{< admonition type="note" >}} -Tracing is supported in the image renderer v3.12.6 and later. -{{< /admonition >}} - -Set the tracing URL to enable OpenTelemetry Tracing. The default is empty (disabled). -You can also configure the service name that will be set in the traces. The default is `grafana-image-renderer`. - -```bash -RENDERING_TRACING_URL="http://localhost:4318/v1/traces" -``` - -```json -{ - "rendering": { - "tracing": { - "url": "http://localhost:4318/v1/traces", - "serviceName": "grafana-renderer" - } - } -} -``` - -#### Custom Chrome/Chromium - -If you already have [Chrome](https://www.google.com/chrome/) or [Chromium](https://www.chromium.org/) -installed on your system, then you can use this instead of the pre-packaged version of Chromium. - -{{< admonition type="note" >}} -Please note that this is not recommended, since you may encounter problems if the installed version of Chrome/Chromium is not compatible with the [Grafana Image renderer plugin](/grafana/plugins/grafana-image-renderer). -{{< /admonition >}} - -You need to make sure that the Chrome/Chromium executable is available for the Grafana/image rendering service process. - -```bash -CHROME_BIN="/usr/bin/chromium-browser" -``` - -```json -{ - "rendering": { - "chromeBin": "/usr/bin/chromium-browser" - } -} -``` - -#### Start browser with additional arguments - -Additional arguments to pass to the headless browser instance. Defaults are `--no-sandbox,--disable-gpu`. The list of Chromium flags can be found [here](https://peter.sh/experiments/chromium-command-line-switches/) and the list of flags used as defaults by Puppeteer can be found [there](https://cri.dev/posts/2020-04-04-Full-list-of-Chromium-Puppeteer-flags/). Multiple arguments is separated with comma-character. - -```bash -RENDERING_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-accelerated-2d-canvas,--disable-gpu,--window-size=1280x758 -``` - -```json -{ - "rendering": { - "args": [ - "--no-sandbox", - "--disable-setuid-sandbox", - "--disable-dev-shm-usage", - "--disable-accelerated-2d-canvas", - "--disable-gpu", - "--window-size=1280x758" - ] - } -} -``` - -#### Ignore HTTPS errors - -Instruct headless browser instance whether to ignore HTTPS errors during navigation. Per default HTTPS errors are not ignored. -Due to the security risk it's not recommended to ignore HTTPS errors. - -```bash -IGNORE_HTTPS_ERRORS=true -``` - -```json -{ - "rendering": { - "ignoresHttpsErrors": true - } -} -``` - -#### Default timezone - -Instruct headless browser instance to use a default timezone when not provided by Grafana, .e.g. when rendering panel image of alert. See [ICU’s metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) for a list of supported timezone IDs. Fallbacks to `TZ` environment variable if not set. - -```bash -BROWSER_TZ=Europe/Stockholm -``` - -```json -{ - "rendering": { - "timezone": "Europe/Stockholm" - } -} -``` - -#### Default language - -Instruct headless browser instance to use a default language when not provided by Grafana, e.g. when rendering panel image of alert. -Refer to the HTTP header Accept-Language to understand how to format this value. - -```bash -# Available from v3.9.0+ -RENDERING_LANGUAGE="fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5" -``` - -```json -{ - "rendering": { - "acceptLanguage": "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5" - } -} -``` - -#### Viewport width - -Default viewport width when width is not specified in the rendering request. Default is `1000`. - -```bash -# Available from v3.9.0+ -RENDERING_VIEWPORT_WIDTH=1000 -``` - -```json -{ - "rendering": { - "width": 1000 - } -} -``` - -#### Viewport height - -Default viewport height when height is not specified in the rendering request. Default is `500`. - -```bash -# Available from v3.9.0+ -RENDERING_VIEWPORT_HEIGHT=500 -``` - -```json -{ - "rendering": { - "height": 500 - } -} -``` - -#### Viewport maximum width - -Limit the maximum viewport width that can be requested. Default is `3000`. - -```bash -# Available from v3.9.0+ -RENDERING_VIEWPORT_MAX_WIDTH=1000 -``` - -```json -{ - "rendering": { - "maxWidth": 1000 - } -} -``` - -#### Viewport maximum height - -Limit the maximum viewport height that can be requested. Default is `3000`. - -```bash -# Available from v3.9.0+ -RENDERING_VIEWPORT_MAX_HEIGHT=500 -``` - -```json -{ - "rendering": { - "maxHeight": 500 - } -} -``` - -#### Device scale factor - -Specify default device scale factor for rendering images. `2` is enough for monitor resolutions, `4` would be better for printed material. Setting a higher value affects performance and memory. Default is `1`. -This can be overridden in the rendering request. - -```bash -# Available from v3.9.0+ -RENDERING_VIEWPORT_DEVICE_SCALE_FACTOR=2 -``` - -```json -{ - "rendering": { - "deviceScaleFactor": 2 - } -} -``` - -#### Maximum device scale factor - -Limit the maximum device scale factor that can be requested. Default is `4`. - -```bash -# Available from v3.9.0+ -RENDERING_VIEWPORT_MAX_DEVICE_SCALE_FACTOR=4 -``` - -```json -{ - "rendering": { - "maxDeviceScaleFactor": 4 - } -} -``` - -#### Page zoom level - -The following command sets a page zoom level. The default value is `1`. A value of `1.5` equals 150% zoom. - -```bash -RENDERING_VIEWPORT_PAGE_ZOOM_LEVEL=1 -``` - -```json -{ - "rendering": { - "pageZoomLevel": 1 - } -} -``` diff --git a/docs/sources/setup-grafana/image-rendering/monitoring/index.md b/docs/sources/setup-grafana/image-rendering/monitoring/index.md deleted file mode 100644 index ad4334f52e3..00000000000 --- a/docs/sources/setup-grafana/image-rendering/monitoring/index.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -aliases: - - ../../image-rendering/monitoring/ -description: Image rendering monitoring -keywords: - - grafana - - image - - rendering - - plugin - - monitoring -labels: - products: - - enterprise - - oss -title: Monitor the image renderer -weight: 100 ---- - -# Monitor the image renderer - -Rendering images requires a lot of memory, mainly because Grafana creates browser instances in the background for the actual rendering. Monitoring your service can help you allocate the right amount of resources to your rendering service and set the right [rendering mode](../#rendering-mode). - -## Enable Prometheus metrics endpoint - -Configure this service to expose a Prometheus metrics endpoint. For information on how to configure and monitor this service using Prometheus as a data source, refer to [Grafana Image Rendering Service dashboard](/grafana/dashboards/12203). - -**Metrics endpoint output example:** - -``` -# HELP process_cpu_user_seconds_total Total user CPU time spent in seconds. -# TYPE process_cpu_user_seconds_total counter -process_cpu_user_seconds_total 0.536 1579444523566 - -# HELP process_cpu_system_seconds_total Total system CPU time spent in seconds. -# TYPE process_cpu_system_seconds_total counter -process_cpu_system_seconds_total 0.064 1579444523566 - -# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds. -# TYPE process_cpu_seconds_total counter -process_cpu_seconds_total 0.6000000000000001 1579444523566 - -# HELP process_start_time_seconds Start time of the process since unix epoch in seconds. -# TYPE process_start_time_seconds gauge -process_start_time_seconds 1579444433 - -# HELP process_resident_memory_bytes Resident memory size in bytes. -# TYPE process_resident_memory_bytes gauge -process_resident_memory_bytes 52686848 1579444523568 - -# HELP process_virtual_memory_bytes Virtual memory size in bytes. -# TYPE process_virtual_memory_bytes gauge -process_virtual_memory_bytes 2055344128 1579444523568 - -# HELP process_heap_bytes Process heap size in bytes. -# TYPE process_heap_bytes gauge -process_heap_bytes 1996390400 1579444523568 - -# HELP process_open_fds Number of open file descriptors. -# TYPE process_open_fds gauge -process_open_fds 31 1579444523567 - -# HELP process_max_fds Maximum number of open file descriptors. -# TYPE process_max_fds gauge -process_max_fds 1573877 - -# HELP nodejs_eventloop_lag_seconds Lag of event loop in seconds. -# TYPE nodejs_eventloop_lag_seconds gauge -nodejs_eventloop_lag_seconds 0.000915922 1579444523567 - -# HELP nodejs_active_handles Number of active libuv handles grouped by handle type. Every handle type is C++ class name. -# TYPE nodejs_active_handles gauge -nodejs_active_handles{type="WriteStream"} 2 1579444523566 -nodejs_active_handles{type="Server"} 1 1579444523566 -nodejs_active_handles{type="Socket"} 9 1579444523566 -nodejs_active_handles{type="ChildProcess"} 2 1579444523566 - -# HELP nodejs_active_handles_total Total number of active handles. -# TYPE nodejs_active_handles_total gauge -nodejs_active_handles_total 14 1579444523567 - -# HELP nodejs_active_requests Number of active libuv requests grouped by request type. Every request type is C++ class name. -# TYPE nodejs_active_requests gauge -nodejs_active_requests{type="FSReqCallback"} 2 - -# HELP nodejs_active_requests_total Total number of active requests. -# TYPE nodejs_active_requests_total gauge -nodejs_active_requests_total 2 1579444523567 - -# HELP nodejs_heap_size_total_bytes Process heap size from node.js in bytes. -# TYPE nodejs_heap_size_total_bytes gauge -nodejs_heap_size_total_bytes 13725696 1579444523567 - -# HELP nodejs_heap_size_used_bytes Process heap size used from node.js in bytes. -# TYPE nodejs_heap_size_used_bytes gauge -nodejs_heap_size_used_bytes 12068008 1579444523567 - -# HELP nodejs_external_memory_bytes Nodejs external memory size in bytes. -# TYPE nodejs_external_memory_bytes gauge -nodejs_external_memory_bytes 1728962 1579444523567 - -# HELP nodejs_heap_space_size_total_bytes Process heap space size total from node.js in bytes. -# TYPE nodejs_heap_space_size_total_bytes gauge -nodejs_heap_space_size_total_bytes{space="read_only"} 262144 1579444523567 -nodejs_heap_space_size_total_bytes{space="new"} 1048576 1579444523567 -nodejs_heap_space_size_total_bytes{space="old"} 9809920 1579444523567 -nodejs_heap_space_size_total_bytes{space="code"} 425984 1579444523567 -nodejs_heap_space_size_total_bytes{space="map"} 1052672 1579444523567 -nodejs_heap_space_size_total_bytes{space="large_object"} 1077248 1579444523567 -nodejs_heap_space_size_total_bytes{space="code_large_object"} 49152 1579444523567 -nodejs_heap_space_size_total_bytes{space="new_large_object"} 0 1579444523567 - -# HELP nodejs_heap_space_size_used_bytes Process heap space size used from node.js in bytes. -# TYPE nodejs_heap_space_size_used_bytes gauge -nodejs_heap_space_size_used_bytes{space="read_only"} 32296 1579444523567 -nodejs_heap_space_size_used_bytes{space="new"} 601696 1579444523567 -nodejs_heap_space_size_used_bytes{space="old"} 9376600 1579444523567 -nodejs_heap_space_size_used_bytes{space="code"} 286688 1579444523567 -nodejs_heap_space_size_used_bytes{space="map"} 704320 1579444523567 -nodejs_heap_space_size_used_bytes{space="large_object"} 1064872 1579444523567 -nodejs_heap_space_size_used_bytes{space="code_large_object"} 3552 1579444523567 -nodejs_heap_space_size_used_bytes{space="new_large_object"} 0 1579444523567 - -# HELP nodejs_heap_space_size_available_bytes Process heap space size available from node.js in bytes. -# TYPE nodejs_heap_space_size_available_bytes gauge -nodejs_heap_space_size_available_bytes{space="read_only"} 229576 1579444523567 -nodejs_heap_space_size_available_bytes{space="new"} 445792 1579444523567 -nodejs_heap_space_size_available_bytes{space="old"} 417712 1579444523567 -nodejs_heap_space_size_available_bytes{space="code"} 20576 1579444523567 -nodejs_heap_space_size_available_bytes{space="map"} 343632 1579444523567 -nodejs_heap_space_size_available_bytes{space="large_object"} 0 1579444523567 -nodejs_heap_space_size_available_bytes{space="code_large_object"} 0 1579444523567 -nodejs_heap_space_size_available_bytes{space="new_large_object"} 1047488 1579444523567 - -# HELP nodejs_version_info Node.js version info. -# TYPE nodejs_version_info gauge -nodejs_version_info{version="v14.16.1",major="14",minor="16",patch="1"} 1 - -# HELP grafana_image_renderer_service_http_request_duration_seconds duration histogram of http responses labeled with: status_code -# TYPE grafana_image_renderer_service_http_request_duration_seconds histogram -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="1",status_code="200"} 0 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="5",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="7",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="9",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="11",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="13",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="15",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="20",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="30",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_bucket{le="+Inf",status_code="200"} 4 -grafana_image_renderer_service_http_request_duration_seconds_sum{status_code="200"} 10.492873834 -grafana_image_renderer_service_http_request_duration_seconds_count{status_code="200"} 4 - -# HELP up 1 = up, 0 = not up -# TYPE up gauge -up 1 - -# HELP grafana_image_renderer_http_request_in_flight A gauge of requests currently being served by the image renderer. -# TYPE grafana_image_renderer_http_request_in_flight gauge -grafana_image_renderer_http_request_in_flight 1 - -# HELP grafana_image_renderer_step_duration_seconds duration histogram of browser steps for rendering an image labeled with: step -# TYPE grafana_image_renderer_step_duration_seconds histogram -grafana_image_renderer_step_duration_seconds_bucket{le="0.3",step="launch"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="0.5",step="launch"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="1",step="launch"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="2",step="launch"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="3",step="launch"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="5",step="launch"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="+Inf",step="launch"} 1 -grafana_image_renderer_step_duration_seconds_sum{step="launch"} 0.7914972 -grafana_image_renderer_step_duration_seconds_count{step="launch"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.3",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.5",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="1",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="2",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="3",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="5",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="+Inf",step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_sum{step="newPage"} 0.2217868 -grafana_image_renderer_step_duration_seconds_count{step="newPage"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.3",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.5",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="1",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="2",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="3",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="5",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="+Inf",step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_sum{step="prepare"} 0.0819274 -grafana_image_renderer_step_duration_seconds_count{step="prepare"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.3",step="navigate"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="0.5",step="navigate"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="1",step="navigate"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="2",step="navigate"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="3",step="navigate"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="5",step="navigate"} 0 -grafana_image_renderer_step_duration_seconds_bucket{le="+Inf",step="navigate"} 1 -grafana_image_renderer_step_duration_seconds_sum{step="navigate"} 15.3311258 -grafana_image_renderer_step_duration_seconds_count{step="navigate"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.3",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.5",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="1",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="2",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="3",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="5",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="+Inf",step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_sum{step="panelsRendered"} 0.0205577 -grafana_image_renderer_step_duration_seconds_count{step="panelsRendered"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.3",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="0.5",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="1",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="2",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="3",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="5",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_bucket{le="+Inf",step="screenshot"} 1 -grafana_image_renderer_step_duration_seconds_sum{step="screenshot"} 0.2866623 -grafana_image_renderer_step_duration_seconds_count{step="screenshot"} 1 - -# HELP grafana_image_renderer_browser_info A metric with a constant '1 value labeled by version of the browser in use -# TYPE grafana_image_renderer_browser_info gauge -grafana_image_renderer_browser_info{version="HeadlessChrome/79.0.3945.0"} 1 -``` diff --git a/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md b/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md deleted file mode 100644 index 61cd711bd50..00000000000 --- a/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -aliases: - - ../../image-rendering/troubleshooting/ -description: Image rendering troubleshooting -keywords: - - grafana - - image - - rendering - - plugin - - troubleshooting -labels: - products: - - enterprise - - oss -menuTitle: Troubleshooting -title: Troubleshoot image rendering -weight: 200 ---- - -# Troubleshoot image rendering - -In this section, you'll learn how to enable logging for the image renderer and you'll find the most common issues. - -## Enable debug logging - -To troubleshoot the image renderer, different kind of logs are available. - -You can enable debug log messages for rendering in the Grafana configuration file and inspect the Grafana server logs. - -```bash -[log] -filters = rendering:debug -``` - -You can also enable more logs in image renderer service itself by enabling [debug logging](#enable-debug-logging). - -## Missing libraries - -The plugin and rendering service uses [Chromium browser](https://www.chromium.org/) which depends on certain libraries. -If you don't have all of those libraries installed in your system you may encounter errors when trying to render an image, e.g. - -```bash -Rendering failed: Error: Failed to launch chrome!/var/lib/grafana/plugins/grafana-image-renderer/chrome-linux/chrome: -error while loading shared libraries: libX11.so.6: cannot open shared object file: No such file or directory\n\n\nTROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md -``` - -In general you can use the [`ldd`]() utility to figure out what shared libraries -are not installed in your system: - -```bash -cd -ldd chrome-headless-shell/linux-132.0.6781.0/chrome-headless-shell-linux64/chrome-headless-shell - linux-vdso.so.1 (0x00007fff1bf65000) - libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f2047945000) - libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f2047924000) - librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f204791a000) - libX11.so.6 => not found - libX11-xcb.so.1 => not found - libxcb.so.1 => not found - libXcomposite.so.1 => not found - ... -``` - -You can find a reference to all the relevant Debian packages for the service to function [in the Dockerfile](https://github.com/grafana/grafana-image-renderer/blob/master/Dockerfile). -If you are using an operating system that is not Debian 12, you should look up what each of those packages are called on your system. - -## Certificate signed by internal certificate authorities - -In many cases, Grafana runs on internal servers and uses certificates that have not been signed by a CA ([Certificate Authority](https://en.wikipedia.org/wiki/Certificate_authority)) known to Chrome, and therefore cannot be validated. Chrome internally uses NSS ([Network Security Services](https://en.wikipedia.org/wiki/Network_Security_Services)) for cryptographic operations such as the validation of certificates. - -If you are using the Grafana Image Renderer with a Grafana server that uses a certificate signed by such a custom CA (for example a company-internal CA), rendering images will fail and you will see messages like this in the Grafana log: - -``` -t=2019-12-04T12:39:22+0000 lvl=error msg="Render request failed" logger=rendering error=map[] url="https://192.168.106.101:3443/d-solo/zxDJxNaZk/graphite-metrics?orgId=1&refresh=1m&from=1575438321300&to=1575459921300&var-Host=master1&panelId=4&width=1000&height=500&tz=Europe%2FBerlin&render=1" timestamp=0001-01-01T00:00:00.000Z -t=2019-12-04T12:39:22+0000 lvl=error msg="Rendering failed." logger=context userId=1 orgId=1 uname=admin error="Rendering failed: Error: net::ERR_CERT_AUTHORITY_INVALID at https://192.168.106.101:3443/d-solo/zxDJxNaZk/graphite-metrics?orgId=1&refresh=1m&from=1575438321300&to=1575459921300&var-Host=master1&panelId=4&width=1000&height=500&tz=Europe%2FBerlin&render=1" -t=2019-12-04T12:39:22+0000 lvl=error msg="Request Completed" logger=context userId=1 orgId=1 uname=admin method=GET path=/render/d-solo/zxDJxNaZk/graphite-metrics status=500 remote_addr=192.168.106.101 time_ms=310 size=1722 referer="https://grafana.xxx-xxx/d/zxDJxNaZk/graphite-metrics?orgId=1&refresh=1m" -``` - -If this happens, then you have to add the certificate to the trust store. If you have the certificate file for the internal root CA in the file `internal-root-ca.crt.pem`, then use these commands to create a user specific NSS trust store for the Grafana user (`grafana` for the purpose of this example) and execute the following steps: - -**Linux:** - -``` -[root@server ~]# [ -d /usr/share/grafana/.pki/nssdb ] || mkdir -p /usr/share/grafana/.pki/nssdb -[root@server ~]# certutil -d sql:/usr/share/grafana/.pki/nssdb -A -n internal-root-ca -t C -i /etc/pki/tls/certs/internal-root-ca.crt.pem -[root@server ~]# chown -R grafana: /usr/share/grafana/.pki/nssdb -``` - -You may also have to use other tooling than `certutil`, such as `update-ca-certificates` and its accompanying paths. -This depends on the Linux distribution you use; distributions often have a wiki with this type of information. - -**Windows:** - -``` -certutil –addstore "Root" /internal-root-ca.crt.pem -``` - -**Container:** - -```Dockerfile -FROM grafana/grafana-image-renderer:latest - -# Elevate our permissions to access system resources. -USER root - -RUN mkdir -p /usr/local/share/ca-certificates/ -# Convert from .pem to .crt -RUN openssl x509 -inform PEM -in rootCA.pem -out /usr/local/share/ca-certificates/rootCA.crt - -# Regenerate the CA certificates in the container. -RUN update-ca-certificates --fresh - -# Reassume the nonroot user for the service execution. -USER nonroot - -# Some CA certificates also need to explicitly be included in the user's network security services database. -# certutil is shipped in v4.0.8 and onwards of the image. -RUN mkdir -p /home/nonroot/.pki/nssdb -RUN certutil -d sql:/home/nonroot/.pki/nssdb -A -n internal-root-ca -t C -i /usr/local/share/ca-certificates/rootCA.crt -``` - -{{< admonition type="note" >}} -The container image was based on Alpine until v4.0.0. -After this point, it is based on distroless Debian. -{{< /admonition >}} - -## Custom Chrome/Chromium - -As a last resort, if you already have [Chrome](https://www.google.com/chrome/) or [Chromium](https://www.chromium.org/) -installed on your system, then you can configure the Grafana Image renderer plugin to use this -instead of the pre-packaged version of Chromium. - -{{< admonition type="note" >}} -Please note that this is not recommended, since you may encounter problems if the installed version of Chrome/Chromium is not -compatible with the [Grafana Image renderer plugin](/grafana/plugins/grafana-image-renderer). -{{< /admonition >}} - -To override the path to the Chrome/Chromium executable in plugin mode, set an environment variable and make sure that it's available for the Grafana process. For example: - -```bash -export GF_PLUGIN_RENDERING_CHROME_BIN="/usr/bin/chromium-browser" -``` - -In remote rendering mode, you need to set the environment variable or update the configuration file and make sure that it's available for the image rendering service process: - -```bash -CHROME_BIN="/usr/bin/chromium-browser" -``` - -```json -{ - "rendering": { - "chromeBin": "/usr/bin/chromium-browser" - } -} -``` From 65fd15bbf988b83fabd545b5bb33c4ae1fb9f8fc Mon Sep 17 00:00:00 2001 From: Rafael Bortolon Paulovic Date: Thu, 20 Nov 2025 15:47:44 +0100 Subject: [PATCH 027/378] feat: define unified disable migrations flag (#114120) chore: add US data migrations config --- pkg/setting/setting.go | 4 +- pkg/setting/setting_unified_storage.go | 57 +++++++++++++++++++- pkg/storage/unified/migrations/migrations.go | 8 ++- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index fb69aa60259..a8b48c67f29 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -578,7 +578,9 @@ type Cfg struct { ShortLinkExpiration int // Unified Storage - UnifiedStorage map[string]UnifiedStorageConfig + UnifiedStorage map[string]UnifiedStorageConfig + // DisableDataMigrations will disable resources data migration to unified storage at startup + DisableDataMigrations bool MaxPageSizeBytes int IndexPath string IndexWorkers int diff --git a/pkg/setting/setting_unified_storage.go b/pkg/setting/setting_unified_storage.go index 15bed66b6bb..29cb5d0270a 100644 --- a/pkg/setting/setting_unified_storage.go +++ b/pkg/setting/setting_unified_storage.go @@ -5,8 +5,15 @@ import ( "time" "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/util/osutil" ) +var migratedUnifiedResources = []string{ + //"playlists.playlist.grafana.app", + "folders.folder.grafana.app", + "dashboards.dashboard.grafana.app", +} + // read storage configs from ini file. They look like: // [unified_storage..] // = @@ -51,8 +58,14 @@ func (cfg *Cfg) setUnifiedStorageConfig() { // Set indexer config for unified storage section := cfg.Raw.Section("unified_storage") - - cfg.EnableSearch = section.Key("enable_search").MustBool(false) + // TODO: Re-enable once migrations are ready and disabled on cloud + //cfg.DisableDataMigrations = section.Key("disable_data_migrations").MustBool(false) + cfg.DisableDataMigrations = true + if !cfg.DisableDataMigrations && cfg.getUnifiedStorageType() == "unified" { + cfg.enforceMigrationToUnifiedConfigs() + } else { + cfg.EnableSearch = section.Key("enable_search").MustBool(false) + } cfg.MaxPageSizeBytes = section.Key("max_page_size_bytes").MustInt(0) cfg.IndexPath = section.Key("index_path").String() cfg.IndexWorkers = section.Key("index_workers").MustInt(10) @@ -84,3 +97,43 @@ func (cfg *Cfg) setUnifiedStorageConfig() { cfg.MaxFileIndexAge = section.Key("max_file_index_age").MustDuration(0) cfg.MinFileIndexBuildVersion = section.Key("min_file_index_build_version").MustString("") } + +// enforceMigrationToUnifiedConfigs enforces configurations required to run migrated resources in mode 5 +// All migrated resources in MigratedUnifiedResources are set to mode 5 and unified search is enabled +func (cfg *Cfg) enforceMigrationToUnifiedConfigs() { + section := cfg.Raw.Section("unified_storage") + cfg.EnableSearch = section.Key("enable_search").MustBool(true) + if !cfg.EnableSearch { + cfg.Logger.Info("Enforcing enable_search for unified storage") + section.Key("enable_search").SetValue("true") + cfg.EnableSearch = true + } + for _, resource := range migratedUnifiedResources { + cfg.Logger.Info("Enforcing mode 5 for resource in unified storage", "resource", resource) + if oldCfg, ok := cfg.UnifiedStorage[resource]; ok { + cfg.Logger.Info("Overriding unified storage config for migrated resource", "resource", resource, "old_config", oldCfg) + } + cfg.UnifiedStorage[resource] = UnifiedStorageConfig{ + DualWriterMode: 5, + DualWriterMigrationDataSyncDisabled: true, + } + } +} + +// getUnifiedStorageType returns the configured storage type without creating or mutating keys. +// Precedence: env > ini > default ("unified"). +// Used to decide unified storage behavior early without side effects. +func (cfg *Cfg) getUnifiedStorageType() string { + const ( + grafanaAPIServerSectionName = "grafana-apiserver" + storageTypeKeyName = "storage_type" + defaultStorageType = "unified" + ) + if envStorageType := (osutil.RealEnv{}).Getenv(EnvKey(grafanaAPIServerSectionName, storageTypeKeyName)); envStorageType != "" { + return envStorageType + } + if cfg.Raw.Section(grafanaAPIServerSectionName).HasKey(storageTypeKeyName) { + return cfg.Raw.Section(grafanaAPIServerSectionName).Key(storageTypeKeyName).Value() + } + return defaultStorageType +} diff --git a/pkg/storage/unified/migrations/migrations.go b/pkg/storage/unified/migrations/migrations.go index 195b7f5d154..a30777e308a 100644 --- a/pkg/storage/unified/migrations/migrations.go +++ b/pkg/storage/unified/migrations/migrations.go @@ -17,6 +17,7 @@ import ( ) var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/migrations") +var logger = log.New("storage.unified.migrations") // UnifiedStorageMigrationProvider provides unified storage migrations as a background service type UnifiedStorageMigrationProvider interface { @@ -56,7 +57,11 @@ func (p *UnifiedStorageMigrationProviderImpl) Run(ctx context.Context) error { if os.Getenv("GRAFANA_TEST_DB") != "" { return nil } - + // skip migrations if disabled in config + if p.cfg.DisableDataMigrations { + logger.Info("Data migrations are disabled, skipping") + return nil + } // TODO: Re-enable once migrations are ready // return RegisterMigrations(p.legacyMigrator, p.cfg, p.client, p.sqlStore) return nil @@ -74,7 +79,6 @@ func RegisterMigrations( ) error { ctx, span := tracer.Start(context.Background(), "storage.unified.RegisterMigrations") defer span.End() - logger := log.New("storage.unified.migrations.folders-dashboards") mg := migrator.NewScopedMigrator(sqlStore.GetEngine(), cfg, "unified_storage") mg.AddCreateMigration() From b5a50e7772aa0a49cb8f00f04d371792e2673f62 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Thu, 20 Nov 2025 15:51:50 +0100 Subject: [PATCH 028/378] `grafana-iam`: Use the UniStore as the default store (#113614) * `grafana-iam`: Use the UniStore as the default store * Refactor all instantiations * Remove enableDualWriter * Nit. dw is clear enough * Use the correct access control client --- pkg/registry/apis/iam/models.go | 15 ++- pkg/registry/apis/iam/register.go | 212 ++++++++++++++---------------- 2 files changed, 112 insertions(+), 115 deletions(-) diff --git a/pkg/registry/apis/iam/models.go b/pkg/registry/apis/iam/models.go index c960b7a8655..309b266ccb4 100644 --- a/pkg/registry/apis/iam/models.go +++ b/pkg/registry/apis/iam/models.go @@ -8,6 +8,10 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" + "github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount" + "github.com/grafana/grafana/pkg/registry/apis/iam/sso" + "github.com/grafana/grafana/pkg/registry/apis/iam/team" + "github.com/grafana/grafana/pkg/registry/apis/iam/teambinding" "github.com/grafana/grafana/pkg/registry/apis/iam/user" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/authz/zanzana" @@ -42,7 +46,13 @@ type ExternalGroupMappingStorageBackend interface{ resource.StorageBackend } // This is used just so wire has something unique to return type IdentityAccessManagementAPIBuilder struct { // Stores - store legacy.LegacyIdentityStore + store legacy.LegacyIdentityStore + + userLegacyStore *user.LegacyStore + saLegacyStore *serviceaccount.LegacyStore + legacyTeamStore *team.LegacyStore + teamBindingLegacyStore *teambinding.LegacyBindingStore + ssoLegacyStore *sso.LegacyStore coreRolesStorage CoreRoleStorageBackend rolesStorage RoleStorageBackend resourcePermissionsStorage resource.StorageBackend @@ -78,7 +88,4 @@ type IdentityAccessManagementAPIBuilder struct { // Toggle for enabling authz management apis features featuremgmt.FeatureToggles - - // Toggle for enabling dual writer - enableDualWriter bool } diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index cc229f2ed4b..2f214fd0630 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -31,7 +31,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/iam/externalgroupmapping" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" - "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/iam/resourcepermission" "github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount" "github.com/grafana/grafana/pkg/registry/apis/iam/sso" @@ -76,8 +75,16 @@ func RegisterAPIService( authorizer := newIAMAuthorizer(accessClient, legacyAccessClient) registerMetrics(reg) + //nolint:staticcheck // not yet migrated to OpenFeature + enableAuthnMutation := features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation) + builder := &IdentityAccessManagementAPIBuilder{ store: store, + userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation), + saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation), + legacyTeamStore: team.NewLegacyStore(store, legacyAccessClient, enableAuthnMutation), + teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation), + ssoLegacyStore: sso.NewLegacyStore(ssoService), coreRolesStorage: coreRolesStorage, rolesStorage: rolesStorage, resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider), @@ -93,7 +100,6 @@ func RegisterAPIService( reg: reg, logger: log.New("iam.apis"), features: features, - enableDualWriter: true, dual: dual, unified: unified, userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), unified, user.NewUserLegacySearchClient(userService), features), @@ -113,18 +119,16 @@ func NewAPIService( store := legacy.NewLegacySQLStores(dbProvider) resourcePermissionsStorage := resourcepermission.ProvideStorageBackend(dbProvider) resourceAuthorizer := gfauthorizer.NewResourceAuthorizer(accessClient) - noopStorage := noopstorage.ProvideStorageBackend() registerMetrics(reg) return &IdentityAccessManagementAPIBuilder{ - store: store, - display: user.NewLegacyDisplayREST(store), - resourcePermissionsStorage: resourcePermissionsStorage, - externalGroupMappingStorage: noopStorage, - logger: log.New("iam.apis"), - features: features, - zClient: zClient, - zTickets: make(chan bool, MaxConcurrentZanzanaWrites), - reg: reg, + store: store, + display: user.NewLegacyDisplayREST(store), + resourcePermissionsStorage: resourcePermissionsStorage, + logger: log.New("iam.apis"), + features: features, + zClient: zClient, + zTickets: make(chan bool, MaxConcurrentZanzanaWrites), + reg: reg, authorizer: authorizer.AuthorizerFunc( func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { // For now only authorize resourcepermissions resource @@ -183,8 +187,7 @@ func (b *IdentityAccessManagementAPIBuilder) AllowedV0Alpha1Resources() []string func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { storage := map[string]rest.Storage{} - //nolint:staticcheck // not yet migrated to OpenFeature - enableAuthnMutation := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation) + //nolint:staticcheck // not yet migrated to OpenFeature enableZanzanaSync := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzZanzanaSync) @@ -197,69 +200,63 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge }) teamResource := iamv0.TeamResourceInfo - teamLegacyStore := team.NewLegacyStore(b.store, b.legacyAccessClient, enableAuthnMutation) - storage[teamResource.StoragePath()] = teamLegacyStore - storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store) + teamUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamResource, opts.OptsGetter) + if err != nil { + return err + } + storage[teamResource.StoragePath()] = teamUniStore - if b.enableDualWriter { - teamStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamResource, opts.OptsGetter) + if b.legacyTeamStore != nil { + dw, err := opts.DualWriteBuilder(teamResource.GroupResource(), b.legacyTeamStore, teamUniStore) if err != nil { return err } - teamDW, err := opts.DualWriteBuilder(teamResource.GroupResource(), teamLegacyStore, teamStore) - if err != nil { - return err - } - - storage[teamResource.StoragePath()] = teamDW + storage[teamResource.StoragePath()] = dw } + storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store) + teamBindingResource := iamv0.TeamBindingResourceInfo - teamBindingLegacyStore := teambinding.NewLegacyBindingStore(b.store, enableAuthnMutation) - storage[teamBindingResource.StoragePath()] = teamBindingLegacyStore + teamBindingUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamBindingResource, opts.OptsGetter) + if err != nil { + return err + } + storage[teamBindingResource.StoragePath()] = teamBindingUniStore - if b.enableDualWriter { - teamBindingStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamBindingResource, opts.OptsGetter) + // Only teamBindingStore exposes the AfterCreate, AfterDelete, and BeginUpdate hooks + if enableZanzanaSync { + b.logger.Info("Enabling hooks for TeamBinding to sync to Zanzana") + teamBindingUniStore.AfterCreate = b.AfterTeamBindingCreate + teamBindingUniStore.AfterDelete = b.AfterTeamBindingDelete + teamBindingUniStore.BeginUpdate = b.BeginTeamBindingUpdate + } + + if b.teamBindingLegacyStore != nil { + dw, err := opts.DualWriteBuilder(teamBindingResource.GroupResource(), b.teamBindingLegacyStore, teamBindingUniStore) if err != nil { return err } - - teamBindingDW, err := opts.DualWriteBuilder(teamBindingResource.GroupResource(), teamBindingLegacyStore, teamBindingStore) - if err != nil { - return err - } - - // Only teamBindingStore exposes the AfterCreate, AfterDelete, and BeginUpdate hooks - if enableZanzanaSync { - b.logger.Info("Enabling hooks for TeamBinding to sync to Zanzana") - teamBindingStore.AfterCreate = b.AfterTeamBindingCreate - teamBindingStore.AfterDelete = b.AfterTeamBindingDelete - teamBindingStore.BeginUpdate = b.BeginTeamBindingUpdate - } - - storage[teamBindingResource.StoragePath()] = teamBindingDW + storage[teamBindingResource.StoragePath()] = dw } // User store registration userResource := iamv0.UserResourceInfo - legacyStore := user.NewLegacyStore(b.store, b.accessClient, enableAuthnMutation) - storage[userResource.StoragePath()] = legacyStore + userUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, userResource, opts.OptsGetter) + if err != nil { + return err + } + storage[userResource.StoragePath()] = userUniStore - if b.enableDualWriter { - store, err := grafanaregistry.NewRegistryStore(opts.Scheme, userResource, opts.OptsGetter) - if err != nil { - return err - } + if enableZanzanaSync { + b.logger.Info("Enabling hooks for User to sync basic role assignments to Zanzana") + userUniStore.AfterCreate = b.AfterUserCreate + userUniStore.BeginUpdate = b.BeginUserUpdate + userUniStore.AfterDelete = b.AfterUserDelete + } - if enableZanzanaSync { - b.logger.Info("Enabling hooks for User to sync basic role assignments to Zanzana") - store.AfterCreate = b.AfterUserCreate - store.BeginUpdate = b.BeginUserUpdate - store.AfterDelete = b.AfterUserDelete - } - - dw, err := opts.DualWriteBuilder(userResource.GroupResource(), legacyStore, store) + if b.userLegacyStore != nil { + dw, err := opts.DualWriteBuilder(userResource.GroupResource(), b.userLegacyStore, userUniStore) if err != nil { return err } @@ -270,50 +267,46 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.store) // Service Accounts store registration - serviceAccountResource := iamv0.ServiceAccountResourceInfo - saLegacyStore := serviceaccount.NewLegacyStore(b.store, b.accessClient, enableAuthnMutation) - storage[serviceAccountResource.StoragePath()] = saLegacyStore - - if b.enableDualWriter { - store, err := grafanaregistry.NewRegistryStore(opts.Scheme, serviceAccountResource, opts.OptsGetter) - if err != nil { - return err - } - - dw, err := opts.DualWriteBuilder(serviceAccountResource.GroupResource(), saLegacyStore, store) - if err != nil { - return err - } - - storage[serviceAccountResource.StoragePath()] = dw - } - - storage[serviceAccountResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.store) - - if b.sso != nil { - ssoResource := legacyiamv0.SSOSettingResourceInfo - storage[ssoResource.StoragePath()] = sso.NewLegacyStore(b.sso) - } - - externalGroupMappingResource := iamv0.ExternalGroupMappingResourceInfo - externalGroupMappingLegacyStore, err := NewLocalStore(externalGroupMappingResource, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.externalGroupMappingStorage) + saResource := iamv0.ServiceAccountResourceInfo + saUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, saResource, opts.OptsGetter) if err != nil { return err } - storage[externalGroupMappingResource.StoragePath()] = externalGroupMappingLegacyStore + storage[saResource.StoragePath()] = saUniStore - if b.enableDualWriter { - externalGroupMappingStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, externalGroupMappingResource, opts.OptsGetter) + if b.saLegacyStore != nil { + dw, err := opts.DualWriteBuilder(saResource.GroupResource(), b.saLegacyStore, saUniStore) + if err != nil { + return err + } + storage[saResource.StoragePath()] = dw + } + + storage[saResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.store) + + if b.ssoLegacyStore != nil { + ssoResource := legacyiamv0.SSOSettingResourceInfo + storage[ssoResource.StoragePath()] = b.ssoLegacyStore + } + + extGroupMappingResource := iamv0.ExternalGroupMappingResourceInfo + extGroupMappingUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, extGroupMappingResource, opts.OptsGetter) + if err != nil { + return err + } + storage[extGroupMappingResource.StoragePath()] = extGroupMappingUniStore + + if b.externalGroupMappingStorage != nil { + extGroupMappingLegacyStore, err := NewLocalStore(extGroupMappingResource, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.externalGroupMappingStorage) if err != nil { return err } - externalGroupMappingDW, err := opts.DualWriteBuilder(externalGroupMappingResource.GroupResource(), externalGroupMappingLegacyStore, externalGroupMappingStore) + dw, err := opts.DualWriteBuilder(extGroupMappingResource.GroupResource(), extGroupMappingLegacyStore, extGroupMappingUniStore) if err != nil { return err } - - storage[externalGroupMappingResource.StoragePath()] = externalGroupMappingDW + storage[extGroupMappingResource.StoragePath()] = dw } //nolint:staticcheck // not yet migrated to OpenFeature @@ -351,7 +344,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge } //nolint:staticcheck // not yet migrated to OpenFeature if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) { - if err := b.UpdateResourcePermissionsAPIGroup(apiGroupInfo, opts, storage, b.enableDualWriter, enableZanzanaSync); err != nil { + if err := b.UpdateResourcePermissionsAPIGroup(apiGroupInfo, opts, storage, enableZanzanaSync); err != nil { return err } } @@ -364,11 +357,19 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateResourcePermissionsAPIGroup( apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions, storage map[string]rest.Storage, - enableDualWriter bool, enableZanzanaSync bool, ) error { - var store rest.Storage - // Create the legacy store first + uniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, iamv0.ResourcePermissionInfo, opts.OptsGetter) + if err != nil { + return err + } + storage[iamv0.ResourcePermissionInfo.StoragePath()] = uniStore + + if b.resourcePermissionsStorage == nil { + // No legacy storage configured, nothing more to do + return nil + } + legacyStore, err := NewLocalStore(iamv0.ResourcePermissionInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.resourcePermissionsStorage) if err != nil { return err @@ -384,23 +385,12 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateResourcePermissionsAPIGroup( legacyStore.AfterDelete = b.AfterResourcePermissionDelete } - // Set the default store to the legacy store - store = legacyStore - - if enableDualWriter { - // Create the dual write store (UniStore + LegacyStore) - uniStore, err := grafanaregistry.NewRegistryStore(apiGroupInfo.Scheme, iamv0.ResourcePermissionInfo, opts.OptsGetter) - if err != nil { - return err - } - - store, err = opts.DualWriteBuilder(iamv0.ResourcePermissionInfo.GroupResource(), legacyStore, uniStore) - if err != nil { - return err - } + dw, err := opts.DualWriteBuilder(iamv0.ResourcePermissionInfo.GroupResource(), legacyStore, uniStore) + if err != nil { + return err } - storage[iamv0.ResourcePermissionInfo.StoragePath()] = store + storage[iamv0.ResourcePermissionInfo.StoragePath()] = dw return nil } From cb06bba24354811d5e66dd4eba8f9096591f5245 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 20 Nov 2025 15:54:32 +0100 Subject: [PATCH 029/378] Zanzana: Add token namespace to config (#114165) --- pkg/services/authz/zanzana.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 3be1b18dc1d..da77010d0eb 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -118,6 +118,7 @@ type ZanzanaClientConfig struct { Token string TokenExchangeURL string ServerCertFile string + TokenNamespace string } // NewRemoteZanzanaClient creates a new Zanzana client that connects to remote Zanzana server. From 30c04ab3fc7dbaafe7d031727daa5f3d697ff513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Sencer=20=C3=96zcan?= <32759850+mustafasencer@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:40:20 +0100 Subject: [PATCH 030/378] feat: inject unified data migrations in dual writer (#114138) * feat: draft changes for on-prem unified migration * feat: further draft changes for on-prem unified migration * fix: remove some tbis * refactor: rename * fix: another approach * fix: background service related issues * fix: address comments * fix: make gen-go * fix: background service related issues * feat: refactor dual writer and legacy migrator * fix: minor issues * feat: working version in oss * fix: wire * fix: revert test data override * fix: enterprise related issues * chore: add todo * fix: revert dual writer method * fix: lint * chore: logger format * fix: reduce log level * fix: log change * fix: disable * fix: address comments * fix: return error on dual writer service * fix: merge conflict --------- Co-authored-by: Rafael Paulovic --- .../commands/datamigrations/stubs.go | 70 --- .../datamigrations/to_unified_storage.go | 41 +- .../dashboard/legacy/legacy_migrator_mock.go | 96 ---- pkg/registry/apis/dashboard/legacy/migrate.go | 460 ------------------ .../apis/dashboard/legacy/migrate_test.go | 154 ------ .../migration_dashboard_accessor_mock.go | 279 +++++++++++ .../apis/dashboard/legacy/sql_dashboards.go | 336 ++++++++++++- .../dashboard/legacy/sql_dashboards_test.go | 146 ++++++ pkg/registry/apis/dashboard/legacy/types.go | 12 +- pkg/registry/apis/dashboard/legacy_storage.go | 2 +- pkg/registry/apis/dashboard/libary_panel.go | 2 +- pkg/registry/apis/dashboard/register.go | 2 +- pkg/registry/apis/dashboard/sub_dto.go | 4 +- .../jobs/migrate/legacy_resources.go | 64 +-- .../jobs/migrate/legacy_resources_test.go | 112 +++-- pkg/registry/apis/provisioning/register.go | 19 +- .../apis/provisioning/resources/object.go | 19 +- .../backgroundsvcs/background_services.go | 3 - pkg/server/wire.go | 8 +- pkg/server/wire_gen.go | 47 +- pkg/server/wireexts_oss.go | 2 + .../apiserver/appinstaller/storage.go | 2 +- pkg/services/apiserver/builder/helper.go | 2 +- pkg/services/apiserver/service.go | 17 +- pkg/services/provisioning/stubs.go | 69 +++ .../dualwrite/dualwriter_mode1_test.go | 12 +- .../dualwrite/dualwriter_mode2_test.go | 12 +- .../dualwrite/dualwriter_mode3_test.go | 12 +- pkg/storage/legacysql/dualwrite/runtime.go | 31 -- .../legacysql/dualwrite/runtime_test.go | 6 +- pkg/storage/legacysql/dualwrite/service.go | 58 +++ .../legacysql/dualwrite/service_test.go | 4 +- pkg/storage/legacysql/dualwrite/static.go | 4 +- .../unified/migrations/contract/migrations.go | 12 + .../migrations/dashboard_folder_migration.go | 93 ---- pkg/storage/unified/migrations/migrations.go | 112 ----- pkg/storage/unified/migrations/migrator.go | 312 ++++++------ .../unified/migrations/migrator_mock.go | 98 ++++ .../unified/migrations/resource_migration.go | 275 +++++++++++ pkg/storage/unified/migrations/service.go | 118 +++++ 40 files changed, 1779 insertions(+), 1348 deletions(-) delete mode 100644 pkg/cmd/grafana-cli/commands/datamigrations/stubs.go delete mode 100644 pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go delete mode 100644 pkg/registry/apis/dashboard/legacy/migrate.go delete mode 100644 pkg/registry/apis/dashboard/legacy/migrate_test.go create mode 100644 pkg/registry/apis/dashboard/legacy/migration_dashboard_accessor_mock.go create mode 100644 pkg/services/provisioning/stubs.go create mode 100644 pkg/storage/unified/migrations/contract/migrations.go delete mode 100644 pkg/storage/unified/migrations/dashboard_folder_migration.go delete mode 100644 pkg/storage/unified/migrations/migrations.go create mode 100644 pkg/storage/unified/migrations/migrator_mock.go create mode 100644 pkg/storage/unified/migrations/resource_migration.go create mode 100644 pkg/storage/unified/migrations/service.go diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/stubs.go b/pkg/cmd/grafana-cli/commands/datamigrations/stubs.go deleted file mode 100644 index 22dc15d6fc7..00000000000 --- a/pkg/cmd/grafana-cli/commands/datamigrations/stubs.go +++ /dev/null @@ -1,70 +0,0 @@ -package datamigrations - -import ( - "context" - "path/filepath" - - "github.com/grafana/grafana/pkg/services/provisioning" - "github.com/grafana/grafana/pkg/services/provisioning/dashboards" -) - -var ( - _ provisioning.ProvisioningService = (*stubProvisioning)(nil) -) - -func newStubProvisioning(path string) (provisioning.ProvisioningService, error) { - cfgs, err := dashboards.ReadDashboardConfig(filepath.Join(path, "dashboards")) - if err != nil { - return nil, err - } - stub := &stubProvisioning{ - path: make(map[string]string), - } - for _, cfg := range cfgs { - stub.path[cfg.Name] = cfg.Options["path"].(string) - } - return &stubProvisioning{}, nil -} - -type stubProvisioning struct { - path map[string]string // name > options.path -} - -// GetAllowUIUpdatesFromConfig implements provisioning.ProvisioningService. -func (s *stubProvisioning) GetAllowUIUpdatesFromConfig(name string) bool { - return false -} - -func (s *stubProvisioning) GetDashboardProvisionerResolvedPath(name string) string { - return s.path[name] -} - -// ProvisionAlerting implements provisioning.ProvisioningService. -func (s *stubProvisioning) ProvisionAlerting(ctx context.Context) error { - panic("unimplemented") -} - -// ProvisionDashboards implements provisioning.ProvisioningService. -func (s *stubProvisioning) ProvisionDashboards(ctx context.Context) error { - panic("unimplemented") -} - -// ProvisionDatasources implements provisioning.ProvisioningService. -func (s *stubProvisioning) ProvisionDatasources(ctx context.Context) error { - panic("unimplemented") -} - -// ProvisionPlugins implements provisioning.ProvisioningService. -func (s *stubProvisioning) ProvisionPlugins(ctx context.Context) error { - panic("unimplemented") -} - -// Run implements provisioning.ProvisioningService. -func (s *stubProvisioning) Run(ctx context.Context) error { - panic("unimplemented") -} - -// RunInitProvisioners implements provisioning.ProvisioningService. -func (s *stubProvisioning) RunInitProvisioners(ctx context.Context) error { - panic("unimplemented") -} diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go index f6bdc211f15..2e8a9892ab1 100644 --- a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go +++ b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go @@ -24,10 +24,11 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/search/sort" + "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified" + "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/parquet" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" @@ -52,7 +53,6 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err {Group: folders.GROUP, Resource: folders.RESOURCE}, {Group: dashboard.GROUP, Resource: dashboard.DASHBOARD_RESOURCE}, }, - LargeObjects: nil, // TODO... from config Progress: func(count int, msg string) { const minInterval = time.Second shouldPrint := count < 1 || time.Since(last) > minInterval @@ -69,31 +69,26 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err } featureToggles := featuremgmt.ProvideToggles(featureManager) - provisioning, err := newStubProvisioning(cfg.ProvisioningPath) + provisioning, err := provisioning.ProvideStubProvisioningService(cfg) if err != nil { return err } - migrator := legacy.NewDashboardAccess( + grpcClient, err := newUnifiedClient(cfg, sqlStore, featureToggles) + if err != nil { + return err + } + + dashboardAccess := legacy.ProvideMigratorDashboardAccessor( legacysql.NewDatabaseProvider(sqlStore), - authlib.OrgNamespaceFormatter, - nil, // no dashboards.Store provisioning, - nil, // no librarypanels.Service - sort.ProvideService(), - nil, // we don't delete during migration, and this is only need to delete permission. acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), featureToggles, ) - client, err := newUnifiedClient(cfg, sqlStore, featureToggles) - if err != nil { - return err - } - if c.Bool("non-interactive") { - opts.Store = client - opts.BlobStore = client + migrator := migrations.ProvideUnifiedMigrator(dashboardAccess, grpcClient) + opts.WithHistory = true // always include history in non-interactive mode rsp, err := migrator.Migrate(ctx, opts) if exitErr := handleMigrationError(err, rsp); exitErr != nil { @@ -113,6 +108,8 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err return err } if yes { + migrator := migrations.ProvideUnifiedMigrator(dashboardAccess, nil) // no need for grpc client for counting + opts.OnlyCount = true rsp, err := migrator.Migrate(ctx, opts) if err != nil { @@ -141,12 +138,13 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err if err != nil { return err } - start = time.Now() - last = time.Now() - opts.Store, err = newParquetClient(file) + parquetClient, err := newParquetClient(file) if err != nil { return err } + migrator := migrations.ProvideUnifiedMigratorParquet(dashboardAccess, parquetClient) + start = time.Now() + last = time.Now() rsp, err := migrator.Migrate(ctx, opts) if err != nil { return err @@ -172,7 +170,7 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err req.Kinds = append(req.Kinds, fmt.Sprintf("%s/%s", r.Group, r.Resource)) } - stats, err := client.GetStats(ctx, req) + stats, err := grpcClient.GetStats(ctx, req) if err != nil { return err } @@ -188,10 +186,9 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err return err } if yes { + migrator := migrations.ProvideUnifiedMigrator(dashboardAccess, grpcClient) start = time.Now() last = time.Now() - opts.Store = client - opts.BlobStore = client rsp, err := migrator.Migrate(ctx, opts) if err != nil { return err diff --git a/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go b/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go deleted file mode 100644 index bb7231e86f6..00000000000 --- a/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go +++ /dev/null @@ -1,96 +0,0 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. - -package legacy - -import ( - context "context" - - resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - mock "github.com/stretchr/testify/mock" -) - -// MockLegacyMigrator is an autogenerated mock type for the LegacyMigrator type -type MockLegacyMigrator struct { - mock.Mock -} - -type MockLegacyMigrator_Expecter struct { - mock *mock.Mock -} - -func (_m *MockLegacyMigrator) EXPECT() *MockLegacyMigrator_Expecter { - return &MockLegacyMigrator_Expecter{mock: &_m.Mock} -} - -// Migrate provides a mock function with given fields: ctx, opts -func (_m *MockLegacyMigrator) Migrate(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Migrate") - } - - var r0 *resourcepb.BulkResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, MigrateOptions) (*resourcepb.BulkResponse, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, MigrateOptions) *resourcepb.BulkResponse); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*resourcepb.BulkResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, MigrateOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockLegacyMigrator_Migrate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Migrate' -type MockLegacyMigrator_Migrate_Call struct { - *mock.Call -} - -// Migrate is a helper method to define mock.On call -// - ctx context.Context -// - opts MigrateOptions -func (_e *MockLegacyMigrator_Expecter) Migrate(ctx interface{}, opts interface{}) *MockLegacyMigrator_Migrate_Call { - return &MockLegacyMigrator_Migrate_Call{Call: _e.mock.On("Migrate", ctx, opts)} -} - -func (_c *MockLegacyMigrator_Migrate_Call) Run(run func(ctx context.Context, opts MigrateOptions)) *MockLegacyMigrator_Migrate_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(MigrateOptions)) - }) - return _c -} - -func (_c *MockLegacyMigrator_Migrate_Call) Return(_a0 *resourcepb.BulkResponse, _a1 error) *MockLegacyMigrator_Migrate_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockLegacyMigrator_Migrate_Call) RunAndReturn(run func(context.Context, MigrateOptions) (*resourcepb.BulkResponse, error)) *MockLegacyMigrator_Migrate_Call { - _c.Call.Return(run) - return _c -} - -// NewMockLegacyMigrator creates a new instance of MockLegacyMigrator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockLegacyMigrator(t interface { - mock.TestingT - Cleanup(func()) -}) *MockLegacyMigrator { - mock := &MockLegacyMigrator{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/dashboard/legacy/migrate.go b/pkg/registry/apis/dashboard/legacy/migrate.go deleted file mode 100644 index e43a0fb7629..00000000000 --- a/pkg/registry/apis/dashboard/legacy/migrate.go +++ /dev/null @@ -1,460 +0,0 @@ -package legacy - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - - "google.golang.org/grpc/metadata" - "k8s.io/apimachinery/pkg/runtime/schema" - - authlib "github.com/grafana/authlib/types" - - dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" - folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" - "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/librarypanels" - "github.com/grafana/grafana/pkg/services/provisioning" - "github.com/grafana/grafana/pkg/services/search/sort" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/storage/legacysql" - "github.com/grafana/grafana/pkg/storage/unified/apistore" - "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" -) - -type MigrateOptions struct { - Namespace string - Store resourcepb.BulkStoreClient - LargeObjects apistore.LargeObjectSupport - BlobStore resourcepb.BlobStoreClient - Resources []schema.GroupResource - WithHistory bool // only applies to dashboards - OnlyCount bool // just count the values - Progress func(count int, msg string) -} - -// Read from legacy and write into unified storage -// -//go:generate mockery --name LegacyMigrator --structname MockLegacyMigrator --inpackage --filename legacy_migrator_mock.go --with-expecter -type LegacyMigrator interface { - Migrate(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) -} - -// This can migrate Folders, Dashboards and LibraryPanels -func ProvideLegacyMigrator( - sql db.DB, // direct access to tables - provisioning provisioning.ProvisioningService, // only needed for dashboard settings - libraryPanelSvc librarypanels.Service, - dashboardPermissionSvc accesscontrol.DashboardPermissionsService, - accessControl accesscontrol.AccessControl, - features featuremgmt.FeatureToggles, -) LegacyMigrator { - dbp := legacysql.NewDatabaseProvider(sql) - return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, libraryPanelSvc, sort.ProvideService(), dashboardPermissionSvc, accessControl, features) -} - -type BlobStoreInfo struct { - Count int64 - Size int64 -} - -// migrate function -- works for a single kind -type migratorFunc = func(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) - -func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) { - info, err := authlib.ParseNamespace(opts.Namespace) - if err != nil { - return nil, err - } - if opts.Progress == nil { - opts.Progress = func(count int, msg string) {} // noop - } - - // Migrate everything - if len(opts.Resources) < 1 { - return nil, fmt.Errorf("missing resource selector") - } - - migratorFuncs := []migratorFunc{} - settings := resource.BulkSettings{ - RebuildCollection: true, - SkipValidation: true, - } - - for _, res := range opts.Resources { - switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { - case "folder.grafana.app/folders": - migratorFuncs = append(migratorFuncs, a.migrateFolders) - settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ - Namespace: opts.Namespace, - Group: folders.GROUP, - Resource: folders.RESOURCE, - }) - - case "dashboard.grafana.app/librarypanels": - migratorFuncs = append(migratorFuncs, a.migratePanels) - settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ - Namespace: opts.Namespace, - Group: dashboard.GROUP, - Resource: dashboard.LIBRARY_PANEL_RESOURCE, - }) - - case "dashboard.grafana.app/dashboards": - migratorFuncs = append(migratorFuncs, a.migrateDashboards) - settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ - Namespace: opts.Namespace, - Group: dashboard.GROUP, - Resource: dashboard.DASHBOARD_RESOURCE, - }) - default: - return nil, fmt.Errorf("unsupported resource: %s", res) - } - } - if opts.OnlyCount { - return a.countValues(ctx, opts) - } - - ctx = metadata.NewOutgoingContext(ctx, settings.ToMD()) - if md, ok := metadata.FromOutgoingContext(ctx); ok { - a.log.Debug("bulk grpc request metadata", - "metadata", md, - "collection", settings.Collection, - ) - } else { - a.log.Debug("bulk grpc request, no metadata found", - "collection", settings.Collection, - ) - } - - stream, err := opts.Store.BulkProcess(ctx) - if err != nil { - return nil, err - } - - // Now run each migration - blobStore := BlobStoreInfo{} - a.log.Info("start migrating legacy resources", "namespace", opts.Namespace, "orgId", info.OrgID, "stackId", info.StackID) - for _, m := range migratorFuncs { - blobs, err := m(ctx, info.OrgID, opts, stream) - if err != nil { - a.log.Error("error migrating legacy resources", "error", err, "namespace", opts.Namespace) - return nil, err - } - if blobs != nil { - blobStore.Count += blobs.Count - blobStore.Size += blobs.Size - } - } - a.log.Info("finished migrating legacy resources", "blobStore", blobStore) - return stream.CloseAndRecv() -} - -func (a *dashboardSqlAccess) countValues(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) { - sql, err := a.sql(ctx) - if err != nil { - return nil, err - } - ns, err := authlib.ParseNamespace(opts.Namespace) - if err != nil { - return nil, err - } - orgId := ns.OrgID - rsp := &resourcepb.BulkResponse{} - err = sql.DB.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { - for _, res := range opts.Resources { - switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { - case "folder.grafana.app/folders": - summary := &resourcepb.BulkResponse_Summary{} - summary.Group = folders.GROUP - summary.Group = folders.RESOURCE - _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("dashboard")+ - " WHERE is_folder=TRUE AND org_id=?", orgId).Get(&summary.Count) - rsp.Summary = append(rsp.Summary, summary) - - case "dashboard.grafana.app/librarypanels": - summary := &resourcepb.BulkResponse_Summary{} - summary.Group = dashboard.GROUP - summary.Resource = dashboard.LIBRARY_PANEL_RESOURCE - _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("library_element")+ - " WHERE org_id=?", orgId).Get(&summary.Count) - rsp.Summary = append(rsp.Summary, summary) - - case "dashboard.grafana.app/dashboards": - summary := &resourcepb.BulkResponse_Summary{} - summary.Group = dashboard.GROUP - summary.Resource = dashboard.DASHBOARD_RESOURCE - rsp.Summary = append(rsp.Summary, summary) - - _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("dashboard")+ - " WHERE is_folder=FALSE AND org_id=?", orgId).Get(&summary.Count) - if err != nil { - return err - } - - // Also count history - _, err = sess.SQL(`SELECT COUNT(*) - FROM `+sql.Table("dashboard_version")+` as dv - JOIN `+sql.Table("dashboard")+` as dd - ON dd.id = dv.dashboard_id - WHERE org_id=?`, orgId).Get(&summary.History) - } - if err != nil { - return err - } - } - return nil - }) - return rsp, nil -} - -func (a *dashboardSqlAccess) migrateDashboards(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { - query := &DashboardQuery{ - OrgID: orgId, - Limit: 100000000, - GetHistory: opts.WithHistory, // include history - AllowFallback: true, // allow fallback to dashboard table during migration - Order: "ASC", // oldest first - } - - blobs := &BlobStoreInfo{} - sql, err := a.sql(ctx) - if err != nil { - return blobs, err - } - - opts.Progress(-1, "migrating dashboards...") - rows, err := a.getRows(ctx, sql, query) - if rows != nil { - defer func() { - _ = rows.Close() - }() - } - if err != nil { - return blobs, err - } - - large := opts.LargeObjects - - // Now send each dashboard - for i := 1; rows.Next(); i++ { - dash := rows.row.Dash - if dash.APIVersion == "" { - dash.APIVersion = fmt.Sprintf("%s/v0alpha1", dashboard.GROUP) - } - dash.SetNamespace(opts.Namespace) - dash.SetResourceVersion("") // it will be filled in by the backend - - body, err := json.Marshal(dash) - if err != nil { - err = fmt.Errorf("error reading json from: %s // %w", rows.row.Dash.Name, err) - return blobs, err - } - - req := &resourcepb.BulkRequest{ - Key: &resourcepb.ResourceKey{ - Namespace: opts.Namespace, - Group: dashboard.GROUP, - Resource: dashboard.DASHBOARD_RESOURCE, - Name: rows.Name(), - }, - Value: body, - Folder: rows.row.FolderUID, - Action: resourcepb.BulkRequest_ADDED, - } - if dash.Generation > 1 { - req.Action = resourcepb.BulkRequest_MODIFIED - } else if dash.Generation < 0 { - req.Action = resourcepb.BulkRequest_DELETED - } - - // With large object support - if large != nil && len(body) > large.Threshold() { - obj, err := utils.MetaAccessor(dash) - if err != nil { - return blobs, err - } - - opts.Progress(i, fmt.Sprintf("[v:%d] %s Large object (%d)", dash.Generation, dash.Name, len(body))) - err = large.Deconstruct(ctx, req.Key, opts.BlobStore, obj, req.Value) - if err != nil { - return blobs, err - } - - // The smaller version (most of spec removed) - req.Value, err = json.Marshal(dash) - if err != nil { - return blobs, err - } - blobs.Count++ - blobs.Size += int64(len(body)) - } - - opts.Progress(i, fmt.Sprintf("[v:%2d] %s (size:%d / %d|%d)", dash.Generation, dash.Name, len(req.Value), i, rows.count)) - - err = stream.Send(req) - if err != nil { - if errors.Is(err, io.EOF) { - opts.Progress(i, fmt.Sprintf("stream EOF/cancelled. index=%d", i)) - err = nil - } - return blobs, err - } - } - - if len(rows.rejected) > 0 { - for _, row := range rows.rejected { - id := row.Dash.Labels[utils.LabelKeyDeprecatedInternalID] - a.log.Warn("rejected dashboard", - "namespace", opts.Namespace, - "dashboard", row.Dash.Name, - "uid", row.Dash.UID, - "id", id, - "version", row.Dash.Generation, - ) - opts.Progress(-2, fmt.Sprintf("rejected: id:%s, uid:%s", id, row.Dash.Name)) - } - } - - if rows.Error() != nil { - return blobs, rows.Error() - } - - opts.Progress(-2, fmt.Sprintf("finished dashboards... (%d)", rows.count)) - return blobs, err -} - -func (a *dashboardSqlAccess) migrateFolders(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { - query := &DashboardQuery{ - OrgID: orgId, - Limit: 100000000, - GetFolders: true, - Order: "ASC", - } - - sql, err := a.sql(ctx) - if err != nil { - return nil, err - } - - opts.Progress(-1, "migrating folders...") - rows, err := a.getRows(ctx, sql, query) - if rows != nil { - defer func() { - _ = rows.Close() - }() - } - if err != nil { - return nil, err - } - - // Now send each dashboard - for i := 1; rows.Next(); i++ { - dash := rows.row.Dash - dash.APIVersion = "folder.grafana.app/v1beta1" - dash.Kind = "Folder" - dash.SetNamespace(opts.Namespace) - dash.SetResourceVersion("") // it will be filled in by the backend - - spec := map[string]any{ - "title": dash.Spec.Object["title"], - } - description := dash.Spec.Object["description"] - if description != nil { - spec["description"] = description - } - dash.Spec.Object = spec - - body, err := json.Marshal(dash) - if err != nil { - return nil, err - } - - req := &resourcepb.BulkRequest{ - Key: &resourcepb.ResourceKey{ - Namespace: opts.Namespace, - Group: "folder.grafana.app", - Resource: "folders", - Name: rows.Name(), - }, - Value: body, - Folder: rows.row.FolderUID, - Action: resourcepb.BulkRequest_ADDED, - } - if dash.Generation > 1 { - req.Action = resourcepb.BulkRequest_MODIFIED - } else if dash.Generation < 0 { - req.Action = resourcepb.BulkRequest_DELETED - } - - opts.Progress(i, fmt.Sprintf("[v:%d] %s (%d)", dash.Generation, dash.Name, len(req.Value))) - - err = stream.Send(req) - if err != nil { - if errors.Is(err, io.EOF) { - err = nil - } - return nil, err - } - } - - if rows.Error() != nil { - return nil, rows.Error() - } - - opts.Progress(-2, fmt.Sprintf("finished folders... (%d)", rows.count)) - return nil, err -} - -func (a *dashboardSqlAccess) migratePanels(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { - opts.Progress(-1, "migrating library panels...") - panels, err := a.GetLibraryPanels(ctx, LibraryPanelQuery{ - OrgID: orgId, - Limit: 1000000, - }) - if err != nil { - return nil, err - } - for i, panel := range panels.Items { - meta, err := utils.MetaAccessor(&panel) - if err != nil { - return nil, err - } - body, err := json.Marshal(panel) - if err != nil { - return nil, err - } - - req := &resourcepb.BulkRequest{ - Key: &resourcepb.ResourceKey{ - Namespace: opts.Namespace, - Group: dashboard.GROUP, - Resource: dashboard.LIBRARY_PANEL_RESOURCE, - Name: panel.Name, - }, - Value: body, - Folder: meta.GetFolder(), - Action: resourcepb.BulkRequest_ADDED, - } - if panel.Generation > 1 { - req.Action = resourcepb.BulkRequest_MODIFIED - } - - opts.Progress(i, fmt.Sprintf("[v:%d] %s (%d)", i, meta.GetName(), len(req.Value))) - - err = stream.Send(req) - if err != nil { - if errors.Is(err, io.EOF) { - err = nil - } - return nil, err - } - } - opts.Progress(-2, fmt.Sprintf("finished panels... (%d)", len(panels.Items))) - return nil, nil -} diff --git a/pkg/registry/apis/dashboard/legacy/migrate_test.go b/pkg/registry/apis/dashboard/legacy/migrate_test.go deleted file mode 100644 index 5b134cb31dc..00000000000 --- a/pkg/registry/apis/dashboard/legacy/migrate_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package legacy - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/storage/legacysql" - "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" - "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks" -) - -func TestDashboardMigrationQuery(t *testing.T) { - // Test that migration queries use AllowFallback flag correctly - nodb := &legacysql.LegacyDatabaseHelper{ - Table: func(n string) string { - return "grafana." + n - }, - } - - t.Run("Migration query should enable AllowFallback flag", func(t *testing.T) { - // Create a migration query as would be used in actual migration - migrationQuery := &DashboardQuery{ - OrgID: 1, - GetHistory: true, // Migration includes history - AllowFallback: true, // This is the key flag for migration - Order: "ASC", // Migration uses ascending order - } - - // Verify UseHistoryTable returns true (requirement for COALESCE logic) - require.True(t, migrationQuery.UseHistoryTable(), "Migration query should use history table") - - // Verify the flag is set correctly - require.True(t, migrationQuery.AllowFallback, "Migration query should allow fallback") - require.True(t, migrationQuery.GetHistory, "Migration query should get history") - require.Equal(t, "ASC", migrationQuery.Order, "Migration should use ascending order") - }) - - t.Run("Regular history query should not use AllowFallback", func(t *testing.T) { - // Regular history query without migration - historyQuery := &DashboardQuery{ - OrgID: 1, - GetHistory: true, - Order: "DESC", - } - - require.True(t, historyQuery.UseHistoryTable(), "History query should use history table") - require.False(t, historyQuery.AllowFallback, "Regular history query should not allow fallback") - require.True(t, historyQuery.GetHistory, "History query should get history") - }) - - t.Run("Migration query template produces COALESCE SQL", func(t *testing.T) { - // Test that the SQL template produces COALESCE logic for migration queries - migrationQuery := &DashboardQuery{ - OrgID: 1, - GetHistory: true, - AllowFallback: true, - Order: "ASC", - } - - req := newQueryReq(nodb, migrationQuery) - req.SQLTemplate = mocks.NewTestingSQLTemplate() - - // Execute the template to get the generated SQL - rawQuery, err := sqltemplate.Execute(sqlQueryDashboards, &req) - require.NoError(t, err) - - sql := rawQuery - - // Verify that COALESCE functions are present in the generated SQL - // These should be used when GetHistory=true AND AllowFallback=true - require.Contains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated)", - "Migration SQL should contain COALESCE for updated timestamp") - require.Contains(t, sql, "COALESCE(dashboard_version.version, dashboard.version)", - "Migration SQL should contain COALESCE for version") - require.Contains(t, sql, "COALESCE(dashboard_version.data, dashboard.data)", - "Migration SQL should contain COALESCE for data") - require.Contains(t, sql, "COALESCE(dashboard_version.api_version, dashboard.api_version)", - "Migration SQL should contain COALESCE for api_version") - require.Contains(t, sql, "COALESCE(dashboard_version.message, '')", - "Migration SQL should contain COALESCE for message with empty string fallback") - - // Verify ORDER BY uses COALESCE as well - require.Contains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated) ASC", - "Migration SQL should ORDER BY COALESCED created timestamp") - require.Contains(t, sql, "COALESCE(dashboard_version.version, dashboard.version) ASC", - "Migration SQL should ORDER BY COALESCED version") - - // Verify it doesn't have the strict history table filter that would exclude NULL version entries - require.NotContains(t, sql, "dashboard_version.id IS NOT NULL", - "Migration SQL should not exclude dashboards without version entries") - }) - - t.Run("Regular history query produces strict SQL", func(t *testing.T) { - // Test that regular history queries still use strict dashboard_version fields - historyQuery := &DashboardQuery{ - OrgID: 1, - GetHistory: true, - Order: "DESC", - } - - req := newQueryReq(nodb, historyQuery) - req.SQLTemplate = mocks.NewTestingSQLTemplate() - - rawQuery, err := sqltemplate.Execute(sqlQueryDashboards, &req) - require.NoError(t, err) - - sql := rawQuery - - // Verify that direct dashboard_version fields are used (no COALESCE) - require.Contains(t, sql, "dashboard_version.created as updated", - "Regular history SQL should use direct dashboard_version.created") - require.Contains(t, sql, "dashboard_version.version", - "Regular history SQL should use direct dashboard_version.version") - require.Contains(t, sql, "dashboard_version.data", - "Regular history SQL should use direct dashboard_version.data") - - // NOTE: We intentionally do NOT add dashboard_version.id IS NOT NULL filter - // to allow for cases where dashboard_version entries might be missing - - // Should not contain COALESCE functions - require.NotContains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated)", - "Regular history SQL should not contain COALESCE for updated") - }) -} - -func TestMigrateDashboardsConfiguration(t *testing.T) { - // Test the actual migration function configuration - - t.Run("Migration options should configure query correctly", func(t *testing.T) { - // Test the migration configuration as used in real migration - opts := MigrateOptions{ - WithHistory: true, // Migration includes history - } - - // This simulates what happens in migrateDashboards function - expectedQuery := &DashboardQuery{ - OrgID: 1, - Limit: 100000000, - GetHistory: opts.WithHistory, // Should be true - AllowFallback: true, // Should be true for migration - Order: "ASC", // Should be ASC for migration - } - - // Verify the configuration matches what migration sets up - require.True(t, expectedQuery.GetHistory, "Migration should enable GetHistory") - require.True(t, expectedQuery.AllowFallback, "Migration should enable AllowFallback") - require.Equal(t, "ASC", expectedQuery.Order, "Migration should use ascending order") - require.Equal(t, 100000000, expectedQuery.Limit, "Migration should use large limit") - - // Verify UseHistoryTable logic - require.True(t, expectedQuery.UseHistoryTable(), "Migration query should use history table") - }) -} diff --git a/pkg/registry/apis/dashboard/legacy/migration_dashboard_accessor_mock.go b/pkg/registry/apis/dashboard/legacy/migration_dashboard_accessor_mock.go new file mode 100644 index 00000000000..43f2ce05ad1 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/migration_dashboard_accessor_mock.go @@ -0,0 +1,279 @@ +// Code generated by mockery v2.53.4. DO NOT EDIT. + +package legacy + +import ( + context "context" + + resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + mock "github.com/stretchr/testify/mock" +) + +// MockMigrationDashboardAccessor is an autogenerated mock type for the MigrationDashboardAccessor type +type MockMigrationDashboardAccessor struct { + mock.Mock +} + +type MockMigrationDashboardAccessor_Expecter struct { + mock *mock.Mock +} + +func (_m *MockMigrationDashboardAccessor) EXPECT() *MockMigrationDashboardAccessor_Expecter { + return &MockMigrationDashboardAccessor_Expecter{mock: &_m.Mock} +} + +// CountResources provides a mock function with given fields: ctx, opts +func (_m *MockMigrationDashboardAccessor) CountResources(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for CountResources") + } + + var r0 *resourcepb.BulkResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, MigrateOptions) (*resourcepb.BulkResponse, error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, MigrateOptions) *resourcepb.BulkResponse); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*resourcepb.BulkResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, MigrateOptions) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockMigrationDashboardAccessor_CountResources_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CountResources' +type MockMigrationDashboardAccessor_CountResources_Call struct { + *mock.Call +} + +// CountResources is a helper method to define mock.On call +// - ctx context.Context +// - opts MigrateOptions +func (_e *MockMigrationDashboardAccessor_Expecter) CountResources(ctx interface{}, opts interface{}) *MockMigrationDashboardAccessor_CountResources_Call { + return &MockMigrationDashboardAccessor_CountResources_Call{Call: _e.mock.On("CountResources", ctx, opts)} +} + +func (_c *MockMigrationDashboardAccessor_CountResources_Call) Run(run func(ctx context.Context, opts MigrateOptions)) *MockMigrationDashboardAccessor_CountResources_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(MigrateOptions)) + }) + return _c +} + +func (_c *MockMigrationDashboardAccessor_CountResources_Call) Return(_a0 *resourcepb.BulkResponse, _a1 error) *MockMigrationDashboardAccessor_CountResources_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockMigrationDashboardAccessor_CountResources_Call) RunAndReturn(run func(context.Context, MigrateOptions) (*resourcepb.BulkResponse, error)) *MockMigrationDashboardAccessor_CountResources_Call { + _c.Call.Return(run) + return _c +} + +// MigrateDashboards provides a mock function with given fields: ctx, orgId, opts, stream +func (_m *MockMigrationDashboardAccessor) MigrateDashboards(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { + ret := _m.Called(ctx, orgId, opts, stream) + + if len(ret) == 0 { + panic("no return value specified for MigrateDashboards") + } + + var r0 *BlobStoreInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)); ok { + return rf(ctx, orgId, opts, stream) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) *BlobStoreInfo); ok { + r0 = rf(ctx, orgId, opts, stream) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*BlobStoreInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) error); ok { + r1 = rf(ctx, orgId, opts, stream) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockMigrationDashboardAccessor_MigrateDashboards_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MigrateDashboards' +type MockMigrationDashboardAccessor_MigrateDashboards_Call struct { + *mock.Call +} + +// MigrateDashboards is a helper method to define mock.On call +// - ctx context.Context +// - orgId int64 +// - opts MigrateOptions +// - stream resourcepb.BulkStore_BulkProcessClient +func (_e *MockMigrationDashboardAccessor_Expecter) MigrateDashboards(ctx interface{}, orgId interface{}, opts interface{}, stream interface{}) *MockMigrationDashboardAccessor_MigrateDashboards_Call { + return &MockMigrationDashboardAccessor_MigrateDashboards_Call{Call: _e.mock.On("MigrateDashboards", ctx, orgId, opts, stream)} +} + +func (_c *MockMigrationDashboardAccessor_MigrateDashboards_Call) Run(run func(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient)) *MockMigrationDashboardAccessor_MigrateDashboards_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].(MigrateOptions), args[3].(resourcepb.BulkStore_BulkProcessClient)) + }) + return _c +} + +func (_c *MockMigrationDashboardAccessor_MigrateDashboards_Call) Return(_a0 *BlobStoreInfo, _a1 error) *MockMigrationDashboardAccessor_MigrateDashboards_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockMigrationDashboardAccessor_MigrateDashboards_Call) RunAndReturn(run func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)) *MockMigrationDashboardAccessor_MigrateDashboards_Call { + _c.Call.Return(run) + return _c +} + +// MigrateFolders provides a mock function with given fields: ctx, orgId, opts, stream +func (_m *MockMigrationDashboardAccessor) MigrateFolders(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { + ret := _m.Called(ctx, orgId, opts, stream) + + if len(ret) == 0 { + panic("no return value specified for MigrateFolders") + } + + var r0 *BlobStoreInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)); ok { + return rf(ctx, orgId, opts, stream) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) *BlobStoreInfo); ok { + r0 = rf(ctx, orgId, opts, stream) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*BlobStoreInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) error); ok { + r1 = rf(ctx, orgId, opts, stream) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockMigrationDashboardAccessor_MigrateFolders_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MigrateFolders' +type MockMigrationDashboardAccessor_MigrateFolders_Call struct { + *mock.Call +} + +// MigrateFolders is a helper method to define mock.On call +// - ctx context.Context +// - orgId int64 +// - opts MigrateOptions +// - stream resourcepb.BulkStore_BulkProcessClient +func (_e *MockMigrationDashboardAccessor_Expecter) MigrateFolders(ctx interface{}, orgId interface{}, opts interface{}, stream interface{}) *MockMigrationDashboardAccessor_MigrateFolders_Call { + return &MockMigrationDashboardAccessor_MigrateFolders_Call{Call: _e.mock.On("MigrateFolders", ctx, orgId, opts, stream)} +} + +func (_c *MockMigrationDashboardAccessor_MigrateFolders_Call) Run(run func(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient)) *MockMigrationDashboardAccessor_MigrateFolders_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].(MigrateOptions), args[3].(resourcepb.BulkStore_BulkProcessClient)) + }) + return _c +} + +func (_c *MockMigrationDashboardAccessor_MigrateFolders_Call) Return(_a0 *BlobStoreInfo, _a1 error) *MockMigrationDashboardAccessor_MigrateFolders_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockMigrationDashboardAccessor_MigrateFolders_Call) RunAndReturn(run func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)) *MockMigrationDashboardAccessor_MigrateFolders_Call { + _c.Call.Return(run) + return _c +} + +// MigrateLibraryPanels provides a mock function with given fields: ctx, orgId, opts, stream +func (_m *MockMigrationDashboardAccessor) MigrateLibraryPanels(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { + ret := _m.Called(ctx, orgId, opts, stream) + + if len(ret) == 0 { + panic("no return value specified for MigrateLibraryPanels") + } + + var r0 *BlobStoreInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)); ok { + return rf(ctx, orgId, opts, stream) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) *BlobStoreInfo); ok { + r0 = rf(ctx, orgId, opts, stream) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*BlobStoreInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) error); ok { + r1 = rf(ctx, orgId, opts, stream) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockMigrationDashboardAccessor_MigrateLibraryPanels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MigrateLibraryPanels' +type MockMigrationDashboardAccessor_MigrateLibraryPanels_Call struct { + *mock.Call +} + +// MigrateLibraryPanels is a helper method to define mock.On call +// - ctx context.Context +// - orgId int64 +// - opts MigrateOptions +// - stream resourcepb.BulkStore_BulkProcessClient +func (_e *MockMigrationDashboardAccessor_Expecter) MigrateLibraryPanels(ctx interface{}, orgId interface{}, opts interface{}, stream interface{}) *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call { + return &MockMigrationDashboardAccessor_MigrateLibraryPanels_Call{Call: _e.mock.On("MigrateLibraryPanels", ctx, orgId, opts, stream)} +} + +func (_c *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call) Run(run func(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient)) *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].(MigrateOptions), args[3].(resourcepb.BulkStore_BulkProcessClient)) + }) + return _c +} + +func (_c *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call) Return(_a0 *BlobStoreInfo, _a1 error) *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call) RunAndReturn(run func(context.Context, int64, MigrateOptions, resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error)) *MockMigrationDashboardAccessor_MigrateLibraryPanels_Call { + _c.Call.Return(run) + return _c +} + +// NewMockMigrationDashboardAccessor creates a new instance of MockMigrationDashboardAccessor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockMigrationDashboardAccessor(t interface { + mock.TestingT + Cleanup(func()) +}) *MockMigrationDashboardAccessor { + mock := &MockMigrationDashboardAccessor{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 3d157e09489..7c17ec8b6aa 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -4,7 +4,9 @@ import ( "context" "database/sql" "encoding/json" + "errors" "fmt" + "io" "strconv" "strings" "sync" @@ -13,6 +15,7 @@ import ( "go.opentelemetry.io/otel" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/utils/ptr" claims "github.com/grafana/authlib/types" @@ -20,6 +23,7 @@ import ( dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -35,16 +39,30 @@ import ( "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/search/sort" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) var ( - _ DashboardAccess = (*dashboardSqlAccess)(nil) - tracer = otel.Tracer("github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy") + tracer = otel.Tracer("github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy") ) +type MigrateOptions struct { + Namespace string + Resources []schema.GroupResource + WithHistory bool // only applies to dashboards + OnlyCount bool // just count the values + Progress func(count int, msg string) +} + +type BlobStoreInfo struct { + Count int64 + Size int64 +} + type dashboardRow struct { // The numeric version for this dashboard RV int64 @@ -63,8 +81,9 @@ type dashboardRow struct { type dashboardSqlAccess struct { sql legacysql.LegacyDatabaseProvider namespacer request.NamespaceMapper - provisioning provisioning.ProvisioningService + provisioning provisioning.StubProvisioningService + // TODO: consider enabling this by default for on-prem migrations invalidDashboardParseFallbackEnabled bool // Use for writing (not reading) @@ -73,7 +92,7 @@ type dashboardSqlAccess struct { dashboardPermissionSvc accesscontrol.DashboardPermissionsService accessControl accesscontrol.AccessControl - libraryPanelSvc librarypanels.Service + libraryPanelSvc librarypanels.Service // only used for save dashboard // Typically one... the server wrapper subscribers []chan *resource.WrittenEvent @@ -81,7 +100,27 @@ type dashboardSqlAccess struct { log log.Logger } -func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, +// ProvideMigratorDashboardAccessor creates a DashboardAccess specifically for migration purposes. +// This provider is used by Wire DI and only includes the minimal dependencies needed for migrations. +func ProvideMigratorDashboardAccessor( + sql legacysql.LegacyDatabaseProvider, + provisioning provisioning.StubProvisioningService, + accessControl accesscontrol.AccessControl, + features featuremgmt.FeatureToggles, +) MigrationDashboardAccessor { + return &dashboardSqlAccess{ + sql: sql, + namespacer: claims.OrgNamespaceFormatter, + dashStore: nil, // not needed for migration + provisioning: provisioning, + dashboardPermissionSvc: nil, // not needed for migration + libraryPanelSvc: nil, // not needed for migration + accessControl: accessControl, + invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled), + } +} + +func NewDashboardSQLAccess(sql legacysql.LegacyDatabaseProvider, namespacer request.NamespaceMapper, dashStore dashboards.Store, provisioning provisioning.ProvisioningService, @@ -90,7 +129,7 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, dashboardPermissionSvc accesscontrol.DashboardPermissionsService, accessControl accesscontrol.AccessControl, features featuremgmt.FeatureToggles, -) DashboardAccess { +) *dashboardSqlAccess { dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter) return &dashboardSqlAccess{ sql: sql, @@ -101,7 +140,6 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, dashboardPermissionSvc: dashboardPermissionSvc, libraryPanelSvc: libraryPanelSvc, accessControl: accessControl, - log: log.New("dashboard.legacysql"), invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled), } } @@ -149,6 +187,290 @@ func (a *dashboardSqlAccess) getRows(ctx context.Context, sql *legacysql.LegacyD }, err } +// CountResources counts resources without migrating them +func (a *dashboardSqlAccess) CountResources(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) { + sql, err := a.sql(ctx) + if err != nil { + return nil, err + } + ns, err := claims.ParseNamespace(opts.Namespace) + if err != nil { + return nil, err + } + orgId := ns.OrgID + rsp := &resourcepb.BulkResponse{} + err = sql.DB.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + for _, res := range opts.Resources { + switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { + case "folder.grafana.app/folders": + summary := &resourcepb.BulkResponse_Summary{} + summary.Group = folders.GROUP + summary.Group = folders.RESOURCE + _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("dashboard")+ + " WHERE is_folder=TRUE AND org_id=?", orgId).Get(&summary.Count) + rsp.Summary = append(rsp.Summary, summary) + + case "dashboard.grafana.app/librarypanels": + summary := &resourcepb.BulkResponse_Summary{} + summary.Group = dashboardV1.GROUP + summary.Resource = dashboardV1.LIBRARY_PANEL_RESOURCE + _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("library_element")+ + " WHERE org_id=?", orgId).Get(&summary.Count) + rsp.Summary = append(rsp.Summary, summary) + + case "dashboard.grafana.app/dashboards": + summary := &resourcepb.BulkResponse_Summary{} + summary.Group = dashboardV1.GROUP + summary.Resource = dashboardV1.DASHBOARD_RESOURCE + rsp.Summary = append(rsp.Summary, summary) + + _, err = sess.SQL("SELECT COUNT(*) FROM "+sql.Table("dashboard")+ + " WHERE is_folder=FALSE AND org_id=?", orgId).Get(&summary.Count) + if err != nil { + return err + } + + // Also count history + _, err = sess.SQL(`SELECT COUNT(*) + FROM `+sql.Table("dashboard_version")+` as dv + JOIN `+sql.Table("dashboard")+` as dd + ON dd.id = dv.dashboard_id + WHERE org_id=?`, orgId).Get(&summary.History) + } + if err != nil { + return err + } + } + return nil + }) + return rsp, nil +} + +// MigrateDashboards handles the dashboard migration logic +func (a *dashboardSqlAccess) MigrateDashboards(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { + query := &DashboardQuery{ + OrgID: orgId, + Limit: 100000000, + GetHistory: opts.WithHistory, // include history + AllowFallback: true, // allow fallback to dashboard table during migration + Order: "ASC", // oldest first + } + + blobs := &BlobStoreInfo{} + sql, err := a.sql(ctx) + if err != nil { + return blobs, err + } + + opts.Progress(-1, "migrating dashboards...") + rows, err := a.getRows(ctx, sql, query) + if rows != nil { + defer func() { + _ = rows.Close() + }() + } + if err != nil { + return blobs, err + } + + // Now send each dashboard + for i := 1; rows.Next(); i++ { + dash := rows.row.Dash + if dash.APIVersion == "" { + dash.APIVersion = fmt.Sprintf("%s/v0alpha1", dashboardV1.GROUP) + } + dash.SetNamespace(opts.Namespace) + dash.SetResourceVersion("") // it will be filled in by the backend + + body, err := json.Marshal(dash) + if err != nil { + err = fmt.Errorf("error reading json from: %s // %w", rows.row.Dash.Name, err) + return blobs, err + } + + req := &resourcepb.BulkRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: dashboardV1.GROUP, + Resource: dashboardV1.DASHBOARD_RESOURCE, + Name: rows.Name(), + }, + Value: body, + Folder: rows.row.FolderUID, + Action: resourcepb.BulkRequest_ADDED, + } + if dash.Generation > 1 { + req.Action = resourcepb.BulkRequest_MODIFIED + } else if dash.Generation < 0 { + req.Action = resourcepb.BulkRequest_DELETED + } + + opts.Progress(i, fmt.Sprintf("[v:%2d] %s (size:%d / %d|%d)", dash.Generation, dash.Name, len(req.Value), i, rows.count)) + + err = stream.Send(req) + if err != nil { + if errors.Is(err, io.EOF) { + opts.Progress(i, fmt.Sprintf("stream EOF/cancelled. index=%d", i)) + err = nil + } + return blobs, err + } + } + + if len(rows.rejected) > 0 { + for _, row := range rows.rejected { + id := row.Dash.Labels[utils.LabelKeyDeprecatedInternalID] + a.log.Warn("rejected dashboard", + "namespace", opts.Namespace, + "dashboard", row.Dash.Name, + "uid", row.Dash.UID, + "id", id, + "version", row.Dash.Generation, + ) + opts.Progress(-2, fmt.Sprintf("rejected: id:%s, uid:%s", id, row.Dash.Name)) + } + } + + if rows.Error() != nil { + return blobs, rows.Error() + } + + opts.Progress(-2, fmt.Sprintf("finished dashboards... (%d)", rows.count)) + return blobs, err +} + +// MigrateFolders handles the folder migration logic +func (a *dashboardSqlAccess) MigrateFolders(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { + query := &DashboardQuery{ + OrgID: orgId, + Limit: 100000000, + GetFolders: true, + Order: "ASC", + } + + sql, err := a.sql(ctx) + if err != nil { + return nil, err + } + + opts.Progress(-1, "migrating folders...") + rows, err := a.getRows(ctx, sql, query) + if rows != nil { + defer func() { + _ = rows.Close() + }() + } + if err != nil { + return nil, err + } + + // Now send each dashboard + for i := 1; rows.Next(); i++ { + dash := rows.row.Dash + dash.APIVersion = "folder.grafana.app/v1beta1" + dash.Kind = "Folder" + dash.SetNamespace(opts.Namespace) + dash.SetResourceVersion("") // it will be filled in by the backend + + spec := map[string]any{ + "title": dash.Spec.Object["title"], + } + description := dash.Spec.Object["description"] + if description != nil { + spec["description"] = description + } + dash.Spec.Object = spec + + body, err := json.Marshal(dash) + if err != nil { + return nil, err + } + + req := &resourcepb.BulkRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: "folder.grafana.app", + Resource: "folders", + Name: rows.Name(), + }, + Value: body, + Folder: rows.row.FolderUID, + Action: resourcepb.BulkRequest_ADDED, + } + if dash.Generation > 1 { + req.Action = resourcepb.BulkRequest_MODIFIED + } else if dash.Generation < 0 { + req.Action = resourcepb.BulkRequest_DELETED + } + + opts.Progress(i, fmt.Sprintf("[v:%d] %s (%d)", dash.Generation, dash.Name, len(req.Value))) + + err = stream.Send(req) + if err != nil { + if errors.Is(err, io.EOF) { + err = nil + } + return nil, err + } + } + + if rows.Error() != nil { + return nil, rows.Error() + } + + opts.Progress(-2, fmt.Sprintf("finished folders... (%d)", rows.count)) + return nil, err +} + +// MigrateLibraryPanels handles the library panel migration logic +func (a *dashboardSqlAccess) MigrateLibraryPanels(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) { + opts.Progress(-1, "migrating library panels...") + panels, err := a.GetLibraryPanels(ctx, LibraryPanelQuery{ + OrgID: orgId, + Limit: 1000000, + }) + if err != nil { + return nil, err + } + for i, panel := range panels.Items { + meta, err := utils.MetaAccessor(&panel) + if err != nil { + return nil, err + } + body, err := json.Marshal(panel) + if err != nil { + return nil, err + } + + req := &resourcepb.BulkRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: dashboardV1.GROUP, + Resource: dashboardV1.LIBRARY_PANEL_RESOURCE, + Name: panel.Name, + }, + Value: body, + Folder: meta.GetFolder(), + Action: resourcepb.BulkRequest_ADDED, + } + if panel.Generation > 1 { + req.Action = resourcepb.BulkRequest_MODIFIED + } + + opts.Progress(i, fmt.Sprintf("[v:%d] %s (%d)", i, meta.GetName(), len(req.Value))) + + err = stream.Send(req) + if err != nil { + if errors.Is(err, io.EOF) { + err = nil + } + return nil, err + } + } + opts.Progress(-2, fmt.Sprintf("finished panels... (%d)", len(panels.Items))) + return nil, nil +} + var _ resource.ListIterator = (*rowsWrapper)(nil) type rowsWrapper struct { diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go index e33841c044c..b142b144800 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go @@ -21,6 +21,9 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks" ) func TestScanRow(t *testing.T) { @@ -529,3 +532,146 @@ func TestParseLibraryPanelRow(t *testing.T) { require.Nil(t, updatedTimestamp) }) } + +func TestDashboardMigrationQuery(t *testing.T) { + // Test that migration queries use AllowFallback flag correctly + nodb := &legacysql.LegacyDatabaseHelper{ + Table: func(n string) string { + return "grafana." + n + }, + } + + t.Run("Migration query should enable AllowFallback flag", func(t *testing.T) { + // Create a migration query as would be used in actual migration + migrationQuery := &DashboardQuery{ + OrgID: 1, + GetHistory: true, // Migration includes history + AllowFallback: true, // This is the key flag for migration + Order: "ASC", // Migration uses ascending order + } + + // Verify UseHistoryTable returns true (requirement for COALESCE logic) + require.True(t, migrationQuery.UseHistoryTable(), "Migration query should use history table") + + // Verify the flag is set correctly + require.True(t, migrationQuery.AllowFallback, "Migration query should allow fallback") + require.True(t, migrationQuery.GetHistory, "Migration query should get history") + require.Equal(t, "ASC", migrationQuery.Order, "Migration should use ascending order") + }) + + t.Run("Regular history query should not use AllowFallback", func(t *testing.T) { + // Regular history query without migration + historyQuery := &DashboardQuery{ + OrgID: 1, + GetHistory: true, + Order: "DESC", + } + + require.True(t, historyQuery.UseHistoryTable(), "History query should use history table") + require.False(t, historyQuery.AllowFallback, "Regular history query should not allow fallback") + require.True(t, historyQuery.GetHistory, "History query should get history") + }) + + t.Run("Migration query template produces COALESCE SQL", func(t *testing.T) { + // Test that the SQL template produces COALESCE logic for migration queries + migrationQuery := &DashboardQuery{ + OrgID: 1, + GetHistory: true, + AllowFallback: true, + Order: "ASC", + } + + req := newQueryReq(nodb, migrationQuery) + req.SQLTemplate = mocks.NewTestingSQLTemplate() + + // Execute the template to get the generated SQL + rawQuery, err := sqltemplate.Execute(sqlQueryDashboards, &req) + require.NoError(t, err) + + sql := rawQuery + + // Verify that COALESCE functions are present in the generated SQL + // These should be used when GetHistory=true AND AllowFallback=true + require.Contains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated)", + "Migration SQL should contain COALESCE for updated timestamp") + require.Contains(t, sql, "COALESCE(dashboard_version.version, dashboard.version)", + "Migration SQL should contain COALESCE for version") + require.Contains(t, sql, "COALESCE(dashboard_version.data, dashboard.data)", + "Migration SQL should contain COALESCE for data") + require.Contains(t, sql, "COALESCE(dashboard_version.api_version, dashboard.api_version)", + "Migration SQL should contain COALESCE for api_version") + require.Contains(t, sql, "COALESCE(dashboard_version.message, '')", + "Migration SQL should contain COALESCE for message with empty string fallback") + + // Verify ORDER BY uses COALESCE as well + require.Contains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated) ASC", + "Migration SQL should ORDER BY COALESCED created timestamp") + require.Contains(t, sql, "COALESCE(dashboard_version.version, dashboard.version) ASC", + "Migration SQL should ORDER BY COALESCED version") + + // Verify it doesn't have the strict history table filter that would exclude NULL version entries + require.NotContains(t, sql, "dashboard_version.id IS NOT NULL", + "Migration SQL should not exclude dashboards without version entries") + }) + + t.Run("Regular history query produces strict SQL", func(t *testing.T) { + // Test that regular history queries still use strict dashboard_version fields + historyQuery := &DashboardQuery{ + OrgID: 1, + GetHistory: true, + Order: "DESC", + } + + req := newQueryReq(nodb, historyQuery) + req.SQLTemplate = mocks.NewTestingSQLTemplate() + + rawQuery, err := sqltemplate.Execute(sqlQueryDashboards, &req) + require.NoError(t, err) + + sql := rawQuery + + // Verify that direct dashboard_version fields are used (no COALESCE) + require.Contains(t, sql, "dashboard_version.created as updated", + "Regular history SQL should use direct dashboard_version.created") + require.Contains(t, sql, "dashboard_version.version", + "Regular history SQL should use direct dashboard_version.version") + require.Contains(t, sql, "dashboard_version.data", + "Regular history SQL should use direct dashboard_version.data") + + // NOTE: We intentionally do NOT add dashboard_version.id IS NOT NULL filter + // to allow for cases where dashboard_version entries might be missing + + // Should not contain COALESCE functions + require.NotContains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated)", + "Regular history SQL should not contain COALESCE for updated") + }) +} + +func TestMigrateDashboardsConfiguration(t *testing.T) { + // Test the actual migration function configuration + + t.Run("Migration options should configure query correctly", func(t *testing.T) { + // Test the migration configuration as used in real migration + opts := MigrateOptions{ + WithHistory: true, // Migration includes history + } + + // This simulates what happens in migrateDashboards function + expectedQuery := &DashboardQuery{ + OrgID: 1, + Limit: 100000000, + GetHistory: opts.WithHistory, // Should be true + AllowFallback: true, // Should be true for migration + Order: "ASC", // Should be ASC for migration + } + + // Verify the configuration matches what migration sets up + require.True(t, expectedQuery.GetHistory, "Migration should enable GetHistory") + require.True(t, expectedQuery.AllowFallback, "Migration should enable AllowFallback") + require.Equal(t, "ASC", expectedQuery.Order, "Migration should use ascending order") + require.Equal(t, 100000000, expectedQuery.Limit, "Migration should use large limit") + + // Verify UseHistoryTable logic + require.True(t, expectedQuery.UseHistoryTable(), "Migration query should use history table") + }) +} diff --git a/pkg/registry/apis/dashboard/legacy/types.go b/pkg/registry/apis/dashboard/legacy/types.go index 4ffa7b80863..31d08af16d8 100644 --- a/pkg/registry/apis/dashboard/legacy/types.go +++ b/pkg/registry/apis/dashboard/legacy/types.go @@ -55,10 +55,9 @@ type LibraryPanelQuery struct { LastID int64 } -type DashboardAccess interface { +type DashboardAccessor interface { resource.StorageBackend resourcepb.ResourceIndexServer - LegacyMigrator GetDashboard(ctx context.Context, orgId int64, uid string, version int64) (*dashboardV1.Dashboard, int64, error) SaveDashboard(ctx context.Context, orgId int64, dash *dashboardV1.Dashboard, failOnExisting bool) (*dashboardV1.Dashboard, bool, error) @@ -67,3 +66,12 @@ type DashboardAccess interface { // Get a typed list GetLibraryPanels(ctx context.Context, query LibraryPanelQuery) (*dashboardV0.LibraryPanelList, error) } + +//go:generate mockery --name MigrationDashboardAccessor --structname MockMigrationDashboardAccessor --inpackage --filename migration_dashboard_accessor_mock.go --with-expecter +type MigrationDashboardAccessor interface { + // Migration helper methods - these support the separate LegacyMigrator + CountResources(ctx context.Context, opts MigrateOptions) (*resourcepb.BulkResponse, error) + MigrateDashboards(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) + MigrateFolders(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) + MigrateLibraryPanels(ctx context.Context, orgId int64, opts MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*BlobStoreInfo, error) +} diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go index d51bea21bf9..1439574efe2 100644 --- a/pkg/registry/apis/dashboard/legacy_storage.go +++ b/pkg/registry/apis/dashboard/legacy_storage.go @@ -22,7 +22,7 @@ import ( ) type DashboardStorage struct { - Access legacy.DashboardAccess + Access legacy.DashboardAccessor DashboardService dashboards.DashboardService } diff --git a/pkg/registry/apis/dashboard/libary_panel.go b/pkg/registry/apis/dashboard/libary_panel.go index aa2b2705bb1..f170831da2b 100644 --- a/pkg/registry/apis/dashboard/libary_panel.go +++ b/pkg/registry/apis/dashboard/libary_panel.go @@ -30,7 +30,7 @@ var ( ) type LibraryPanelStore struct { - Access legacy.DashboardAccess + Access legacy.DashboardAccessor ResourceInfo utils.ResourceInfo service libraryelements.Service } diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index f784fc9c014..fb42543de33 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -169,7 +169,7 @@ func RegisterAPIService( publicDashboardService: publicDashboardService, legacy: &DashboardStorage{ - Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features), + Access: legacy.NewDashboardSQLAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features), DashboardService: dashboardService, }, } diff --git a/pkg/registry/apis/dashboard/sub_dto.go b/pkg/registry/apis/dashboard/sub_dto.go index f2e9bc0b6df..04a14387fc7 100644 --- a/pkg/registry/apis/dashboard/sub_dto.go +++ b/pkg/registry/apis/dashboard/sub_dto.go @@ -30,7 +30,7 @@ type dtoBuilder = func(dashboard runtime.Object, access *dashboard.DashboardAcce // The DTO returns everything the UI needs in a single request type DTOConnector struct { getter rest.Getter - legacy legacy.DashboardAccess + legacy legacy.DashboardAccessor unified resource.ResourceClient largeObjects apistore.LargeObjectSupport accessControl accesscontrol.AccessControl @@ -42,7 +42,7 @@ type DTOConnector struct { func NewDTOConnector( getter rest.Getter, largeObjects apistore.LargeObjectSupport, - legacyAccess legacy.DashboardAccess, + legacyAccess legacy.DashboardAccessor, resourceClient resource.ResourceClient, accessControl accesscontrol.AccessControl, scheme *runtime.Scheme, diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources.go index b7b4bb45a4f..48add96ee16 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/export" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature" + unifiedmigrations "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/parquet" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" @@ -29,7 +30,7 @@ type LegacyResourcesMigrator interface { type legacyResourcesMigrator struct { repositoryResources resources.RepositoryResourcesFactory parsers resources.ParserFactory - legacyMigrator legacy.LegacyMigrator + dashboardAccess legacy.MigrationDashboardAccessor signerFactory signature.SignerFactory clients resources.ClientFactory exportFn export.ExportFn @@ -38,7 +39,7 @@ type legacyResourcesMigrator struct { func NewLegacyResourcesMigrator( repositoryResources resources.RepositoryResourcesFactory, parsers resources.ParserFactory, - legacyMigrator legacy.LegacyMigrator, + dashboardAccess legacy.MigrationDashboardAccessor, signerFactory signature.SignerFactory, clients resources.ClientFactory, exportFn export.ExportFn, @@ -46,7 +47,7 @@ func NewLegacyResourcesMigrator( return &legacyResourcesMigrator{ repositoryResources: repositoryResources, parsers: parsers, - legacyMigrator: legacyMigrator, + dashboardAccess: dashboardAccess, signerFactory: signerFactory, clients: clients, exportFn: exportFn, @@ -94,7 +95,7 @@ func (m *legacyResourcesMigrator) Migrate(ctx context.Context, rw repository.Rea reader := newLegacyResourceMigrator( rw, - m.legacyMigrator, + m.dashboardAccess, parser, repositoryResources, progress, @@ -113,21 +114,21 @@ func (m *legacyResourcesMigrator) Migrate(ctx context.Context, rw repository.Rea } type legacyResourceResourceMigrator struct { - repo repository.ReaderWriter - legacy legacy.LegacyMigrator - parser resources.Parser - progress jobs.JobProgressRecorder - namespace string - kind schema.GroupResource - options provisioning.MigrateJobOptions - resources resources.RepositoryResources - signer signature.Signer - history map[string]string // UID >> file path + repo repository.ReaderWriter + dashboardAccess legacy.MigrationDashboardAccessor + parser resources.Parser + progress jobs.JobProgressRecorder + namespace string + kind schema.GroupResource + options provisioning.MigrateJobOptions + resources resources.RepositoryResources + signer signature.Signer + history map[string]string // UID >> file path } func newLegacyResourceMigrator( repo repository.ReaderWriter, - legacy legacy.LegacyMigrator, + dashboardAccess legacy.MigrationDashboardAccessor, parser resources.Parser, resources resources.RepositoryResources, progress jobs.JobProgressRecorder, @@ -141,16 +142,16 @@ func newLegacyResourceMigrator( history = make(map[string]string) } return &legacyResourceResourceMigrator{ - repo: repo, - legacy: legacy, - parser: parser, - progress: progress, - options: options, - namespace: namespace, - kind: kind, - resources: resources, - signer: signer, - history: history, + repo: repo, + dashboardAccess: dashboardAccess, + parser: parser, + progress: progress, + options: options, + namespace: namespace, + kind: kind, + resources: resources, + signer: signer, + history: history, } } @@ -225,14 +226,21 @@ func (r *legacyResourceResourceMigrator) Write(ctx context.Context, key *resourc func (r *legacyResourceResourceMigrator) Migrate(ctx context.Context) error { r.progress.SetMessage(ctx, fmt.Sprintf("migrate %s resource", r.kind.Resource)) + + // Create a parquet migrator with this instance as the BulkResourceWriter + parquetClient := parquet.NewBulkResourceWriterClient(r) + migrator := unifiedmigrations.ProvideUnifiedMigratorParquet( + r.dashboardAccess, + parquetClient, + ) + opts := legacy.MigrateOptions{ Namespace: r.namespace, WithHistory: r.options.History, Resources: []schema.GroupResource{r.kind}, - Store: parquet.NewBulkResourceWriterClient(r), OnlyCount: true, // first get the count } - stats, err := r.legacy.Migrate(ctx, opts) + stats, err := migrator.Migrate(ctx, opts) if err != nil { return fmt.Errorf("unable to count legacy items %w", err) } @@ -248,7 +256,7 @@ func (r *legacyResourceResourceMigrator) Migrate(ctx context.Context) error { } opts.OnlyCount = false // this time actually write - _, err = r.legacy.Migrate(ctx, opts) + _, err = migrator.Migrate(ctx, opts) if err != nil { return fmt.Errorf("migrate legacy %s: %w", r.kind.Resource, err) } diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources_test.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources_test.go index 89077848f25..7f4318639c2 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources_test.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/legacy_resources_test.go @@ -93,8 +93,8 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) { mockRepoResourcesFactory.On("Client", mock.Anything, mock.Anything). Return(mockRepoResources, nil) - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" })).Return(&resourcepb.BulkResponse{}, errors.New("legacy migrator error")) @@ -115,7 +115,7 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) { migrator := NewLegacyResourcesMigrator( mockRepoResourcesFactory, mockParserFactory, - mockLegacyMigrator, + mockDashboardAccess, signerFactory, mockClientFactory, mockExportFn.Execute, @@ -136,7 +136,7 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) { mockParserFactory.AssertExpectations(t) mockRepoResourcesFactory.AssertExpectations(t) - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) progress.AssertExpectations(t) mockExportFn.AssertExpectations(t) mockClientFactory.AssertExpectations(t) @@ -308,22 +308,18 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) { History: true, }).Return(mockSigner, nil) - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + // Mock CountResources for the count phase + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, nil).Once() // Count phase - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + })).Return(&resourcepb.BulkResponse{}, nil).Once() + // Mock MigrateDashboards for the actual migration phase (dashboards resource) + mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return !opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{ - Summary: []*resourcepb.BulkResponse_Summary{ - { - Group: "test.grafana.app", - Resource: "tests", - Count: 10, - History: 5, - }, - }, - }, nil).Once() // Migration phase + }), mock.Anything).Return(&legacy.BlobStoreInfo{ + Count: 10, + Size: 5, + }, nil).Once() mockClients := resources.NewMockResourceClients(t) mockClientFactory := resources.NewMockClientFactory(t) @@ -339,7 +335,7 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) { migrator := NewLegacyResourcesMigrator( mockRepoResourcesFactory, mockParserFactory, - mockLegacyMigrator, + mockDashboardAccess, mockSignerFactory, mockClientFactory, mockExportFn.Execute, @@ -362,7 +358,7 @@ func TestLegacyResourcesMigrator_Migrate(t *testing.T) { mockParserFactory.AssertExpectations(t) mockRepoResourcesFactory.AssertExpectations(t) - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) mockClientFactory.AssertExpectations(t) mockExportFn.AssertExpectations(t) progress.AssertExpectations(t) @@ -742,8 +738,8 @@ func TestLegacyResourceResourceMigrator_Write(t *testing.T) { func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) { t.Run("should fail when legacy migrate count fails", func(t *testing.T) { - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" })).Return(&resourcepb.BulkResponse{}, errors.New("count error")) @@ -752,7 +748,7 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) { migrator := newLegacyResourceMigrator( nil, - mockLegacyMigrator, + mockDashboardAccess, nil, nil, progress, @@ -766,89 +762,91 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "unable to count legacy items") - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) progress.AssertExpectations(t) }) t.Run("should fail when legacy migrate write fails", func(t *testing.T) { - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" })).Return(&resourcepb.BulkResponse{}, nil).Once() // Count phase - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + // For test-resources GroupResource, we don't know which method it will call, but since it's not dashboards/folders/librarypanels, + // the Migrate will fail trying to map the resource type. Let's make it dashboards for this test. + mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return !opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, errors.New("write error")).Once() // Write phase + }), mock.Anything).Return(nil, errors.New("write error")).Once() // Write phase progress := jobs.NewMockJobProgressRecorder(t) progress.On("SetMessage", mock.Anything, mock.Anything).Return() migrator := newLegacyResourceMigrator( nil, - mockLegacyMigrator, + mockDashboardAccess, nil, nil, progress, provisioning.MigrateJobOptions{}, "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "test-resources"}, + schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, signature.NewGrafanaSigner(), ) err := migrator.Migrate(context.Background()) require.Error(t, err) - require.Contains(t, err.Error(), "migrate legacy test-resources: write error") + require.Contains(t, err.Error(), "migrate legacy dashboards: write error") - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) progress.AssertExpectations(t) }) t.Run("should successfully migrate resource", func(t *testing.T) { - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" })).Return(&resourcepb.BulkResponse{}, nil).Once() // Count phase - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return !opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, nil).Once() // Write phase + }), mock.Anything).Return(&legacy.BlobStoreInfo{}, nil).Once() // Write phase progress := jobs.NewMockJobProgressRecorder(t) progress.On("SetMessage", mock.Anything, mock.Anything).Return() migrator := newLegacyResourceMigrator( nil, - mockLegacyMigrator, + mockDashboardAccess, nil, nil, progress, provisioning.MigrateJobOptions{}, "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, + schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, signature.NewGrafanaSigner(), ) err := migrator.Migrate(context.Background()) require.NoError(t, err) - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) progress.AssertExpectations(t) }) t.Run("should set total to history if history is greater than count", func(t *testing.T) { - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" })).Return(&resourcepb.BulkResponse{ Summary: []*resourcepb.BulkResponse_Summary{ { - Group: "test.grafana.app", - Resource: "tests", + Group: "dashboard.grafana.app", + Resource: "dashboards", Count: 1, History: 100, }, }, }, nil).Once() // Count phase - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return !opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, nil).Once() // Write phase + }), mock.Anything).Return(&legacy.BlobStoreInfo{}, nil).Once() // Write phase progress := jobs.NewMockJobProgressRecorder(t) progress.On("SetMessage", mock.Anything, mock.Anything).Return() @@ -856,39 +854,39 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) { migrator := newLegacyResourceMigrator( nil, - mockLegacyMigrator, + mockDashboardAccess, nil, nil, progress, provisioning.MigrateJobOptions{}, "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, + schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, signature.NewGrafanaSigner(), ) err := migrator.Migrate(context.Background()) require.NoError(t, err) - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) progress.AssertExpectations(t) }) t.Run("should set total to count if history is less than count", func(t *testing.T) { - mockLegacyMigrator := legacy.NewMockLegacyMigrator(t) - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess := legacy.NewMockMigrationDashboardAccessor(t) + mockDashboardAccess.On("CountResources", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return opts.OnlyCount && opts.Namespace == "test-namespace" })).Return(&resourcepb.BulkResponse{ Summary: []*resourcepb.BulkResponse_Summary{ { - Group: "test.grafana.app", - Resource: "tests", + Group: "dashboard.grafana.app", + Resource: "dashboards", Count: 200, History: 1, }, }, }, nil).Once() // Count phase - mockLegacyMigrator.On("Migrate", mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { + mockDashboardAccess.On("MigrateDashboards", mock.Anything, mock.Anything, mock.MatchedBy(func(opts legacy.MigrateOptions) bool { return !opts.OnlyCount && opts.Namespace == "test-namespace" - })).Return(&resourcepb.BulkResponse{}, nil).Once() // Write phase + }), mock.Anything).Return(&legacy.BlobStoreInfo{}, nil).Once() // Write phase progress := jobs.NewMockJobProgressRecorder(t) progress.On("SetMessage", mock.Anything, mock.Anything).Return() @@ -897,20 +895,20 @@ func TestLegacyResourceResourceMigrator_Migrate(t *testing.T) { migrator := newLegacyResourceMigrator( nil, - mockLegacyMigrator, + mockDashboardAccess, nil, nil, progress, provisioning.MigrateJobOptions{}, "test-namespace", - schema.GroupResource{Group: "test.grafana.app", Resource: "tests"}, + schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}, signer, ) err := migrator.Migrate(context.Background()) require.NoError(t, err) - mockLegacyMigrator.AssertExpectations(t) + mockDashboardAccess.AssertExpectations(t) progress.AssertExpectations(t) }) } diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index c30b43eeb60..d18fc1156a8 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -60,6 +60,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/resource" ) @@ -109,7 +110,7 @@ type APIBuilder struct { jobHistoryConfig *JobHistoryConfig jobHistoryLoki *jobs.LokiJobHistory resourceLister resources.ResourceLister - legacyMigrator legacy.LegacyMigrator + dashboardAccess legacy.MigrationDashboardAccessor storageStatus dualwrite.Service unified resource.ResourceClient repoFactory repository.Factory @@ -135,7 +136,7 @@ func NewAPIBuilder( features featuremgmt.FeatureToggles, unified resource.ResourceClient, configProvider apiserver.RestConfigProvider, - legacyMigrator legacy.LegacyMigrator, + dashboardAccess legacy.MigrationDashboardAccessor, storageStatus dualwrite.Service, usageStats usagestats.Service, access authlib.AccessChecker, @@ -158,6 +159,7 @@ func NewAPIBuilder( clients = resources.NewClientFactory(configProvider) } parsers := resources.NewParserFactory(clients) + legacyMigrator := migrations.ProvideUnifiedMigrator(dashboardAccess, unified) resourceLister := resources.NewResourceListerForMigrations(unified, legacyMigrator, storageStatus) b := &APIBuilder{ @@ -170,7 +172,7 @@ func NewAPIBuilder( parsers: parsers, repositoryResources: resources.NewRepositoryResourcesFactory(parsers, clients, resourceLister), resourceLister: resourceLister, - legacyMigrator: legacyMigrator, + dashboardAccess: dashboardAccess, storageStatus: storageStatus, unified: unified, access: access, @@ -234,7 +236,7 @@ func RegisterAPIService( client resource.ResourceClient, // implements resource.RepositoryClient configProvider apiserver.RestConfigProvider, access authlib.AccessClient, - legacyMigrator legacy.LegacyMigrator, + dashboardAccess legacy.MigrationDashboardAccessor, storageStatus dualwrite.Service, usageStats usagestats.Service, tracer tracing.Tracer, @@ -258,7 +260,7 @@ func RegisterAPIService( features, client, configProvider, - legacyMigrator, storageStatus, + dashboardAccess, storageStatus, usageStats, access, tracer, @@ -722,7 +724,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH legacyResources := migrate.NewLegacyResourcesMigrator( b.repositoryResources, b.parsers, - b.legacyMigrator, + b.dashboardAccess, signerFactory, b.clients, export.ExportAll, @@ -1241,8 +1243,9 @@ func (b *APIBuilder) tryRunningOnlyUnifiedStorage() error { return nil } - // Count how many things exist - rsp, err := b.legacyMigrator.Migrate(ctx, legacy.MigrateOptions{ + // Count how many things exist - create a migrator on-demand for this + legacyMigrator := migrations.ProvideUnifiedMigrator(b.dashboardAccess, b.unified) + rsp, err := legacyMigrator.Migrate(ctx, legacy.MigrateOptions{ Namespace: "default", // FIXME! this works for single org, but need to check multi-org Resources: []schema.GroupResource{{ Group: dashboard.GROUP, Resource: dashboard.DASHBOARD_RESOURCE, diff --git a/pkg/registry/apis/provisioning/resources/object.go b/pkg/registry/apis/provisioning/resources/object.go index 4d08de5c05c..0a016786410 100644 --- a/pkg/registry/apis/provisioning/resources/object.go +++ b/pkg/registry/apis/provisioning/resources/object.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) @@ -30,9 +31,9 @@ type ResourceStore interface { } type ResourceListerFromSearch struct { - store ResourceStore - legacyMigrator legacy.LegacyMigrator - storageStatus dualwrite.Service + store ResourceStore + migrator migrations.UnifiedMigrator + storageStatus dualwrite.Service } func NewResourceLister(store ResourceStore) ResourceLister { @@ -42,13 +43,13 @@ func NewResourceLister(store ResourceStore) ResourceLister { // FIXME: the logic about migration and storage should probably be separated from this func NewResourceListerForMigrations( store ResourceStore, - legacyMigrator legacy.LegacyMigrator, + migrator migrations.UnifiedMigrator, storageStatus dualwrite.Service, ) ResourceLister { return &ResourceListerFromSearch{ - store: store, - legacyMigrator: legacyMigrator, - storageStatus: storageStatus, + store: store, + migrator: migrator, + storageStatus: storageStatus, } } @@ -133,8 +134,8 @@ func (o *ResourceListerFromSearch) Stats(ctx context.Context, namespace, reposit } // Get the stats based on what a migration could support - if o.storageStatus != nil && o.legacyMigrator != nil && dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, o.storageStatus) { - rsp, err := o.legacyMigrator.Migrate(ctx, legacy.MigrateOptions{ + if o.storageStatus != nil && o.migrator != nil && dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, o.storageStatus) { + rsp, err := o.migrator.Migrate(ctx, legacy.MigrateOptions{ Namespace: namespace, Resources: []schema.GroupResource{{ Group: dashboard.GROUP, Resource: dashboard.DASHBOARD_RESOURCE, diff --git a/pkg/registry/backgroundsvcs/background_services.go b/pkg/registry/backgroundsvcs/background_services.go index 5f997875985..e80bcb720c9 100644 --- a/pkg/registry/backgroundsvcs/background_services.go +++ b/pkg/registry/backgroundsvcs/background_services.go @@ -48,7 +48,6 @@ import ( "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl" "github.com/grafana/grafana/pkg/services/team/teamapi" "github.com/grafana/grafana/pkg/services/updatemanager" - unifiedmigrations "github.com/grafana/grafana/pkg/storage/unified/migrations" ) func ProvideBackgroundServiceRegistry( @@ -74,7 +73,6 @@ func ProvideBackgroundServiceRegistry( dashboardServiceImpl *service.DashboardServiceImpl, secretsGarbageCollectionWorker *secretsgarbagecollectionworker.Worker, fixedRolesLoader *accesscontrol.FixedRolesLoader, - unifiedStorageMigrationProvider unifiedmigrations.UnifiedStorageMigrationProvider, // Need to make sure these are initialized, is there a better place to put them? _ dashboardsnapshots.Service, _ serviceaccounts.Service, @@ -91,7 +89,6 @@ func ProvideBackgroundServiceRegistry( notifications, rendering, tokenService, - unifiedStorageMigrationProvider, provisioning, grafanaUpdateChecker, pluginsUpdateChecker, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 2fe6db16b31..0d4bb10c0b5 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -128,6 +128,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/preference/prefimpl" promTypeMigration "github.com/grafana/grafana/pkg/services/promtypemigration" + "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/publicdashboards" publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api" publicdashboardsStore "github.com/grafana/grafana/pkg/services/publicdashboards/database" @@ -238,7 +239,9 @@ var wireBasicSet = wire.NewSet( uss.ProvideService, wire.Bind(new(usagestats.Service), new(*uss.UsageStats)), validator.ProvideService, - legacy.ProvideLegacyMigrator, + provisioning.ProvideStubProvisioningService, + legacy.ProvideMigratorDashboardAccessor, + unifiedmigrations.ProvideUnifiedMigrator, pluginsintegration.WireSet, pluginDashboards.ProvideFileStoreManager, wire.Bind(new(pluginDashboards.FileStore), new(*pluginDashboards.FileStoreManager)), @@ -466,8 +469,7 @@ var wireBasicSet = wire.NewSet( // Unified storage resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, - unifiedmigrations.ProvideUnifiedStorageMigrationProvider, - wire.Bind(new(unifiedmigrations.UnifiedStorageMigrationProvider), new(*unifiedmigrations.UnifiedStorageMigrationProviderImpl)), + unifiedmigrations.ProvideUnifiedStorageMigrationService, // Kubernetes API server grafanaapiserver.WireSet, apiregistry.WireSet, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 91a1e50a224..0cb27791c19 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -219,7 +219,7 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/database" kvstore2 "github.com/grafana/grafana/pkg/services/secrets/kvstore" - migrations2 "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" + migrations3 "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" "github.com/grafana/grafana/pkg/services/secrets/manager" migrator2 "github.com/grafana/grafana/pkg/services/secrets/migrator" "github.com/grafana/grafana/pkg/services/serviceaccounts" @@ -255,13 +255,14 @@ import ( "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" database4 "github.com/grafana/grafana/pkg/storage/secret/database" "github.com/grafana/grafana/pkg/storage/secret/encryption" "github.com/grafana/grafana/pkg/storage/secret/metadata" "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/grafana/grafana/pkg/storage/unified" - migrations3 "github.com/grafana/grafana/pkg/storage/unified/migrations" + migrations2 "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/storage/unified/sql" @@ -526,7 +527,15 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg) + legacyDatabaseProvider := legacysql.NewDatabaseProvider(sqlStore) + stubProvisioningService, err := provisioning.ProvideStubProvisioningService(cfg) + if err != nil { + return nil, err + } + migrationDashboardAccessor := legacy.ProvideMigratorDashboardAccessor(legacyDatabaseProvider, stubProvisioningService, accessControl, featureToggles) + unifiedMigrator := migrations2.ProvideUnifiedMigrator(migrationDashboardAccessor, resourceClient) + unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore) + dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg, unifiedStorageMigrationService) if err != nil { return nil, err } @@ -716,8 +725,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) - dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) - secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) + dataSourceSecretMigrationService := migrations3.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) + secretMigrationProviderImpl := migrations3.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) middleware := api2.ProvideMiddleware() apiApi := api2.ProvideApi(publicDashboardServiceImpl, routeRegisterImpl, accessControl, featureToggles, middleware, cfg, ossLicensingService) @@ -838,8 +847,6 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api dashboardUpdater := service8.ProvideDashboardUpdater(inProcBus, pluginstoreService, service14, importDashboardService, service13, pluginService, dashboardService) worker := garbagecollectionworker.ProvideWorker(cfg, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService) fixedRolesLoader := accesscontrol.ProvideFixedRolesLoader(acimplService, featureToggles) - legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles) - unifiedStorageMigrationProviderImpl := migrations3.ProvideUnifiedStorageMigrationProvider(legacyMigrator, cfg, resourceClient, sqlStore) healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) if err != nil { return nil, err @@ -892,7 +899,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, legacyMigrator, dualwriteService, usageStats, tracingService, v3, v4, repositoryFactory) + provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, migrationDashboardAccessor, dualwriteService, usageStats, tracingService, v3, v4, repositoryFactory) if err != nil { return nil, err } @@ -920,7 +927,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } ossUserProtectionImpl := authinfoimpl.ProvideOSSUserProtectionService() registration := authnimpl.ProvideRegistration(cfg, authnService, orgService, userAuthTokenService, acimplService, permissionRegistry, apikeyService, userService, authService, ossUserProtectionImpl, loginattemptimplService, quotaService, authinfoimplService, renderingService, featureToggles, oauthtokenService, socialService, remoteCache, ldapImpl, ossImpl, tracingService, tempuserService, notificationService) - backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, worker, fixedRolesLoader, unifiedStorageMigrationProviderImpl, serviceImpl, serviceAccountsProxy, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) + backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, worker, fixedRolesLoader, serviceImpl, serviceAccountsProxy, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) usageStatsProvidersRegistry := usagestatssvcs.ProvideUsageStatsProvidersRegistry(acimplService, userService) server, err := New(opts, cfg, httpServer, acimplService, provisioningServiceImpl, backgroundServiceRegistry, usageStatsProvidersRegistry, statscollectorService, tracingService, featureToggles, registerer) if err != nil { @@ -1167,7 +1174,15 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg) + legacyDatabaseProvider := legacysql.NewDatabaseProvider(sqlStore) + stubProvisioningService, err := provisioning.ProvideStubProvisioningService(cfg) + if err != nil { + return nil, err + } + migrationDashboardAccessor := legacy.ProvideMigratorDashboardAccessor(legacyDatabaseProvider, stubProvisioningService, accessControl, featureToggles) + unifiedMigrator := migrations2.ProvideUnifiedMigrator(migrationDashboardAccessor, resourceClient) + unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore) + dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg, unifiedStorageMigrationService) if err != nil { return nil, err } @@ -1359,8 +1374,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) secretsMigrator := migrator2.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) - dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) - secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) + dataSourceSecretMigrationService := migrations3.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) + secretMigrationProviderImpl := migrations3.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) middleware := api2.ProvideMiddleware() apiApi := api2.ProvideApi(publicDashboardServiceImpl, routeRegisterImpl, accessControl, featureToggles, middleware, cfg, ossLicensingService) @@ -1481,8 +1496,6 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac dashboardUpdater := service8.ProvideDashboardUpdater(inProcBus, pluginstoreService, service14, importDashboardService, service13, pluginService, dashboardService) worker := garbagecollectionworker.ProvideWorker(cfg, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService) fixedRolesLoader := accesscontrol.ProvideFixedRolesLoader(acimplService, featureToggles) - legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles) - unifiedStorageMigrationProviderImpl := migrations3.ProvideUnifiedStorageMigrationProvider(legacyMigrator, cfg, resourceClient, sqlStore) healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) if err != nil { return nil, err @@ -1535,7 +1548,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, legacyMigrator, dualwriteService, usageStats, tracingService, v3, v4, repositoryFactory) + provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, migrationDashboardAccessor, dualwriteService, usageStats, tracingService, v3, v4, repositoryFactory) if err != nil { return nil, err } @@ -1563,7 +1576,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } ossUserProtectionImpl := authinfoimpl.ProvideOSSUserProtectionService() registration := authnimpl.ProvideRegistration(cfg, authnService, orgService, userAuthTokenService, acimplService, permissionRegistry, apikeyService, userService, authService, ossUserProtectionImpl, loginattemptimplService, quotaService, authinfoimplService, renderingService, featureToggles, oauthtokentestService, socialService, remoteCache, ldapImpl, ossImpl, tracingService, tempuserService, notificationServiceMock) - backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, worker, fixedRolesLoader, unifiedStorageMigrationProviderImpl, serviceImpl, serviceAccountsProxy, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) + backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, worker, fixedRolesLoader, serviceImpl, serviceAccountsProxy, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) usageStatsProvidersRegistry := usagestatssvcs.ProvideUsageStatsProvidersRegistry(acimplService, userService) server, err := New(opts, cfg, httpServer, acimplService, provisioningServiceImpl, backgroundServiceRegistry, usageStatsProvidersRegistry, statscollectorService, tracingService, featureToggles, registerer) if err != nil { @@ -1759,7 +1772,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, wire.Bind(new(installsync.ServerLock), new(*serverlock.ServerLockService)), annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), garbagecollectionworker.ProvideWorker, grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), wire.Bind(new(folder.LegacyService), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), promtypemigration.ProvideAzurePromMigrationService, promtypemigration.ProvideAmazonPromMigrationService, promtypemigration.ProvidePromTypeMigrationProvider, wire.Bind(new(promtypemigration.PromTypeMigrationProvider), new(*promtypemigration.PromTypeMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, accesscontrol.ProvideFixedRolesLoader, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, caching.ProvideCachingServiceClient, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, wire.Value([]decrypt.ExtraOwnerDecrypter(nil)), decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, encryption.ProvideEncryptedValueMigrationExecutor, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator.NewWithEngine, database4.ProvideDatabase, clock.ProvideClock, wire.Bind(new(contracts.Database), new(*database4.Database)), wire.Bind(new(contracts.Clock), new(*clock.Clock)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, migrations3.ProvideUnifiedStorageMigrationProvider, wire.Bind(new(migrations3.UnifiedStorageMigrationProvider), new(*migrations3.UnifiedStorageMigrationProviderImpl)), apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet, client.ProvideK8sClientWithFallback) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, provisioning.ProvideStubProvisioningService, legacy.ProvideMigratorDashboardAccessor, migrations2.ProvideUnifiedMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, wire.Bind(new(installsync.ServerLock), new(*serverlock.ServerLockService)), annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), garbagecollectionworker.ProvideWorker, grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), wire.Bind(new(folder.LegacyService), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations3.ProvideDataSourceMigrationService, migrations3.ProvideSecretMigrationProvider, wire.Bind(new(migrations3.SecretMigrationProvider), new(*migrations3.SecretMigrationProviderImpl)), promtypemigration.ProvideAzurePromMigrationService, promtypemigration.ProvideAmazonPromMigrationService, promtypemigration.ProvidePromTypeMigrationProvider, wire.Bind(new(promtypemigration.PromTypeMigrationProvider), new(*promtypemigration.PromTypeMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, accesscontrol.ProvideFixedRolesLoader, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, caching.ProvideCachingServiceClient, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, wire.Value([]decrypt.ExtraOwnerDecrypter(nil)), decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, encryption.ProvideEncryptedValueMigrationExecutor, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator.NewWithEngine, database4.ProvideDatabase, clock.ProvideClock, wire.Bind(new(contracts.Database), new(*database4.Database)), wire.Bind(new(contracts.Clock), new(*clock.Clock)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, migrations2.ProvideUnifiedStorageMigrationService, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet, client.ProvideK8sClientWithFallback) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index b2b134ee4a9..9f069f807fc 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -62,6 +62,7 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified" "github.com/grafana/grafana/pkg/storage/unified/resource" search2 "github.com/grafana/grafana/pkg/storage/unified/search" @@ -98,6 +99,7 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(validations.DataSourceRequestURLValidator), new(*validations.OSSDataSourceRequestURLValidator)), provisioning.ProvideService, wire.Bind(new(provisioning.ProvisioningService), new(*provisioning.ProvisioningServiceImpl)), + legacysql.NewDatabaseProvider, backgroundsvcs.ProvideBackgroundServiceRegistry, wire.Bind(new(registry.BackgroundServiceRegistry), new(*backgroundsvcs.BackgroundServiceRegistry)), migrations.ProvideOSSMigrations, diff --git a/pkg/services/apiserver/appinstaller/storage.go b/pkg/services/apiserver/appinstaller/storage.go index 3ecc34cce58..98989508858 100644 --- a/pkg/services/apiserver/appinstaller/storage.go +++ b/pkg/services/apiserver/appinstaller/storage.go @@ -107,7 +107,7 @@ func NewDualWriter( if currentMode != mode { klog.Warningf("Requested DualWrite mode: %d, but using %d for %+v", mode, currentMode, gr) } - return dualwrite.NewDualWriter(gr, currentMode, legacy, storage) + return dualwrite.NewStaticStorage(gr, currentMode, legacy, storage) } func getRequestInfo(gr schema.GroupResource, namespaceMapper request.NamespaceMapper) *k8srequest.RequestInfo { diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index 98a9c08ed25..603c49f3cb2 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -379,7 +379,7 @@ func InstallAPIs( case grafanarest.Mode4, grafanarest.Mode5: return storage, nil default: - return dualwrite.NewDualWriter(gr, currentMode, legacy, storage) + return dualwrite.NewStaticStorage(gr, currentMode, legacy, storage) } } } diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 7d4ee3c0d5a..28da416536c 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -97,7 +97,7 @@ type service struct { authorizer *authorizer.GrafanaAuthorizer serverLockService builder.ServerLockService - storageStatus dualwrite.Service + dualWriter dualwrite.Service kvStore kvstore.KVStore pluginClient plugins.Client @@ -127,7 +127,7 @@ func ProvideService( datasources datasource.ScopedPluginDatasourceProvider, contextProvider datasource.PluginContextWrapper, pluginStore pluginstore.Store, - storageStatus dualwrite.Service, + dualWriter dualwrite.Service, unified resource.ResourceClient, secrets secret.InlineSecureValueSupport, restConfigProvider RestConfigProvider, @@ -158,7 +158,7 @@ func ProvideService( contextProvider: contextProvider, pluginStore: pluginStore, serverLockService: serverLockService, - storageStatus: storageStatus, + dualWriter: dualWriter, unified: unified, secrets: secrets, restConfigProvider: restConfigProvider, @@ -385,12 +385,17 @@ func (s *service) start(ctx context.Context) error { } // Install the API group+version for existing builders - err = builder.InstallAPIs(s.scheme, s.codecs, server, serverConfig.RESTOptionsGetter, builders, o.StorageOptions, + err = builder.InstallAPIs(s.scheme, + s.codecs, + server, + serverConfig.RESTOptionsGetter, + builders, + o.StorageOptions, s.metrics, request.GetNamespaceMapper(s.cfg), kvstore.WithNamespace(s.kvStore, 0, "storage.dualwriting"), s.serverLockService, - s.storageStatus, + s.dualWriter, optsregister, s.features, s.dualWriterMetrics, @@ -409,7 +414,7 @@ func (s *service) start(ctx context.Context) error { kvstore.WithNamespace(s.kvStore, 0, "storage.dualwriting"), s.serverLockService, request.GetNamespaceMapper(s.cfg), - s.storageStatus, + s.dualWriter, s.dualWriterMetrics, s.builderMetrics, serverConfig.MergedResourceConfig, diff --git a/pkg/services/provisioning/stubs.go b/pkg/services/provisioning/stubs.go new file mode 100644 index 00000000000..b40e66aac5c --- /dev/null +++ b/pkg/services/provisioning/stubs.go @@ -0,0 +1,69 @@ +package provisioning + +import ( + "os" + "path/filepath" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/provisioning/dashboards" + "github.com/grafana/grafana/pkg/setting" +) + +type StubProvisioningService interface { + GetDashboardProvisionerResolvedPath(name string) string + GetAllowUIUpdatesFromConfig(name string) bool +} + +func ProvideStubProvisioningService(cfg *setting.Cfg) (StubProvisioningService, error) { + return NewStubProvisioning(cfg.ProvisioningPath) +} + +func NewStubProvisioning(path string) (StubProvisioningService, error) { + cfgs, err := dashboards.ReadDashboardConfig(filepath.Join(path, "dashboards")) + if err != nil { + return nil, err + } + stub := &stubProvisioning{ + path: make(map[string]string), + allowUIUpdates: make(map[string]bool), + log: log.New("provisioning.stub"), + } + for _, cfg := range cfgs { + stub.path[cfg.Name] = cfg.Options["path"].(string) + stub.allowUIUpdates[cfg.Name] = cfg.AllowUIUpdates + } + return stub, nil +} + +type stubProvisioning struct { + path map[string]string // name > options.path + allowUIUpdates map[string]bool + log log.Logger +} + +func (s *stubProvisioning) GetAllowUIUpdatesFromConfig(name string) bool { + return s.allowUIUpdates[name] +} + +func (s *stubProvisioning) GetDashboardProvisionerResolvedPath(name string) string { + path := s.path[name] + if _, err := os.Stat(path); os.IsNotExist(err) { + s.log.Warn("Cannot read directory", "error", err) + } + + path, err := filepath.Abs(path) + if err != nil { + s.log.Warn("Could not create absolute path", "path", path, "error", err) + } + + path, err = filepath.EvalSymlinks(path) + if err != nil { + s.log.Warn("Failed to read content of symlinked path", "path", path, "error", err) + } + + if path == "" { + path = s.path[name] + s.log.Info("falling back to original path due to EvalSymlink/Abs failure") + } + return path +} diff --git a/pkg/storage/legacysql/dualwrite/dualwriter_mode1_test.go b/pkg/storage/legacysql/dualwrite/dualwriter_mode1_test.go index b864e3a360d..777d791d4e5 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter_mode1_test.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter_mode1_test.go @@ -71,7 +71,7 @@ func TestMode1_Create(t *testing.T) { tt.setupStorageFn(us.Mock) } - dw, err := NewDualWriter(kind, rest.Mode1, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode1, ls, us) require.NoError(t, err) obj, err := dw.Create(context.Background(), tt.input, func(context.Context, runtime.Object) error { return nil }, &metav1.CreateOptions{}) @@ -154,7 +154,7 @@ func TestMode1_Get(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode1, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode1, ls, us) require.NoError(t, err) obj, err := dw.Get(context.Background(), name, &metav1.GetOptions{}) @@ -217,7 +217,7 @@ func TestMode1_List(t *testing.T) { tt.setupStorageFn(us.Mock) } - dw, err := NewDualWriter(kind, rest.Mode1, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode1, ls, us) require.NoError(t, err) _, err = dw.List(context.Background(), &metainternalversion.ListOptions{}) @@ -286,7 +286,7 @@ func TestMode1_Delete(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode1, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode1, ls, us) require.NoError(t, err) obj, _, err := dw.Delete(context.Background(), name, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) @@ -361,7 +361,7 @@ func TestMode1_DeleteCollection(t *testing.T) { tt.setupStorageFn(us.Mock, tt.input) } - dw, err := NewDualWriter(kind, rest.Mode1, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode1, ls, us) require.NoError(t, err) obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, tt.input, &metainternalversion.ListOptions{}) @@ -434,7 +434,7 @@ func TestMode1_Update(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode1, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode1, ls, us) require.NoError(t, err) obj, _, err := dw.Update(context.Background(), name, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) diff --git a/pkg/storage/legacysql/dualwrite/dualwriter_mode2_test.go b/pkg/storage/legacysql/dualwrite/dualwriter_mode2_test.go index cd1e87b0b9a..a45dfa1285d 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter_mode2_test.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter_mode2_test.go @@ -69,7 +69,7 @@ func TestMode2_Create(t *testing.T) { tt.setupStorageFn(us.Mock, tt.input) } - dw, err := NewDualWriter(kind, rest.Mode2, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode2, ls, us) require.NoError(t, err) obj, err := dw.Create(context.Background(), tt.input, createFn, &metav1.CreateOptions{}) @@ -154,7 +154,7 @@ func TestMode2_Get(t *testing.T) { tt.setupStorageFn(us.Mock, tt.input) } - dw, err := NewDualWriter(kind, rest.Mode2, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode2, ls, us) require.NoError(t, err) obj, err := dw.Get(context.Background(), tt.input, &metav1.GetOptions{}) @@ -229,7 +229,7 @@ func TestMode2_List(t *testing.T) { tt.setupStorageFn(us.Mock) } - dw, err := NewDualWriter(kind, rest.Mode2, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode2, ls, us) require.NoError(t, err) obj, err := dw.List(context.Background(), &metainternalversion.ListOptions{}) @@ -331,7 +331,7 @@ func TestMode2_Delete(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode2, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode2, ls, us) require.NoError(t, err) obj, _, err := dw.Delete(context.Background(), name, func(context.Context, runtime.Object) error { return nil }, &metav1.DeleteOptions{}) @@ -401,7 +401,7 @@ func TestMode2_DeleteCollection(t *testing.T) { tt.setupStorageFn(us.Mock) } - dw, err := NewDualWriter(kind, rest.Mode2, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode2, ls, us) require.NoError(t, err) obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: name}}, &metainternalversion.ListOptions{}) @@ -471,7 +471,7 @@ func TestMode2_Update(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode2, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode2, ls, us) require.NoError(t, err) obj, _, err := dw.Update(context.Background(), name, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) diff --git a/pkg/storage/legacysql/dualwrite/dualwriter_mode3_test.go b/pkg/storage/legacysql/dualwrite/dualwriter_mode3_test.go index 3076d8a9409..d2485efb762 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter_mode3_test.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter_mode3_test.go @@ -75,7 +75,7 @@ func TestMode3_Create(t *testing.T) { tt.setupStorageFn(us.Mock, tt.input) } - dw, err := NewDualWriter(kind, rest.Mode3, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode3, ls, us) require.NoError(t, err) obj, err := dw.Create(context.Background(), tt.input, createFn, &metav1.CreateOptions{}) @@ -134,7 +134,7 @@ func TestMode3_Get(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode3, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode3, ls, us) require.NoError(t, err) obj, err := dw.Get(context.Background(), name, &metav1.GetOptions{}) @@ -185,7 +185,7 @@ func TestMode3_List(t *testing.T) { tt.setupStorageFn(us.Mock, &metainternalversion.ListOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}}) } - dw, err := NewDualWriter(kind, rest.Mode3, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode3, ls, us) require.NoError(t, err) res, err := dw.List(context.Background(), &metainternalversion.ListOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}}) @@ -276,7 +276,7 @@ func TestMode3_Delete(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode3, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode3, ls, us) require.NoError(t, err) obj, _, err := dw.Delete(context.Background(), name, func(context.Context, runtime.Object) error { return nil }, &metav1.DeleteOptions{}) @@ -346,7 +346,7 @@ func TestMode3_DeleteCollection(t *testing.T) { tt.setupStorageFn(us.Mock) } - dw, err := NewDualWriter(kind, rest.Mode3, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode3, ls, us) require.NoError(t, err) obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: name}}, &metainternalversion.ListOptions{}) @@ -416,7 +416,7 @@ func TestMode3_Update(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - dw, err := NewDualWriter(kind, rest.Mode3, ls, us) + dw, err := NewStaticStorage(kind, rest.Mode3, ls, us) require.NoError(t, err) obj, _, err := dw.Update(context.Background(), name, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) diff --git a/pkg/storage/legacysql/dualwrite/runtime.go b/pkg/storage/legacysql/dualwrite/runtime.go index 180beeb255a..b9c5355050c 100644 --- a/pkg/storage/legacysql/dualwrite/runtime.go +++ b/pkg/storage/legacysql/dualwrite/runtime.go @@ -14,37 +14,6 @@ import ( grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" ) -func (m *service) NewStorage(gr schema.GroupResource, legacy grafanarest.Storage, unified grafanarest.Storage) (grafanarest.Storage, error) { - status, err := m.Status(context.Background(), gr) - if err != nil { - return nil, err - } - - if m.enabled && status.Runtime { - // Dynamic storage behavior - return &runtimeDualWriter{ - service: m, - legacy: legacy, - unified: unified, - dualwrite: &dualWriter{legacy: legacy, unified: unified}, // not used for read - gr: gr, - }, nil - } - - if status.ReadUnified { - if status.WriteLegacy { - // Write both, read unified - return &dualWriter{legacy: legacy, unified: unified, readUnified: true}, nil - } - return unified, nil - } - if status.WriteUnified { - // Write both, read legacy - return &dualWriter{legacy: legacy, unified: unified}, nil - } - return legacy, nil -} - // The runtime dual writer implements the various modes we have described as: mode:1/2/3/4/5 // However the behavior can be configured at runtime rather than just at startup. // When a resource is marked as "migrating", all write requests will be 503 unavailable diff --git a/pkg/storage/legacysql/dualwrite/runtime_test.go b/pkg/storage/legacysql/dualwrite/runtime_test.go index b59801e3da8..01dd79647a7 100644 --- a/pkg/storage/legacysql/dualwrite/runtime_test.go +++ b/pkg/storage/legacysql/dualwrite/runtime_test.go @@ -77,7 +77,7 @@ func TestRuntime_Create(t *testing.T) { tt.setupStorageFn(us.Mock, tt.input) } - m, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), nil) + m, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), NewFakeConfig(), NewFakeMigrator()) require.NoError(t, err) dw, err := m.NewStorage(kind, ls, us) require.NoError(t, err) @@ -150,7 +150,7 @@ func TestRuntime_Get(t *testing.T) { tt.setupStorageFn(us.Mock, name) } - m, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), nil) + m, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), NewFakeConfig(), NewFakeMigrator()) require.NoError(t, err) dw, err := m.NewStorage(kind, ls, us) require.NoError(t, err) @@ -235,7 +235,7 @@ func TestRuntime_CreateWhileMigrating(t *testing.T) { } // Shared provider across all tests - dual, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), nil) + dual, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), NewFakeConfig(), NewFakeMigrator()) require.NoError(t, err) for _, tt := range tests { diff --git a/pkg/storage/legacysql/dualwrite/service.go b/pkg/storage/legacysql/dualwrite/service.go index 77ecc761de1..8ac88deb281 100644 --- a/pkg/storage/legacysql/dualwrite/service.go +++ b/pkg/storage/legacysql/dualwrite/service.go @@ -12,8 +12,28 @@ import ( "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" + unifiedmigrations "github.com/grafana/grafana/pkg/storage/unified/migrations/contract" ) +// fakeMigrator is a no-op implementation of UnifiedStorageMigrationService +type fakeMigrator struct{} + +func (f *fakeMigrator) Run(ctx context.Context) error { + return nil +} + +var _ unifiedmigrations.UnifiedStorageMigrationService = (*fakeMigrator)(nil) + +func NewFakeMigrator() unifiedmigrations.UnifiedStorageMigrationService { + return &fakeMigrator{} +} + +func NewFakeConfig() *setting.Cfg { + return &setting.Cfg{ + UnifiedStorage: make(map[string]setting.UnifiedStorageConfig), + } +} + func ProvideStaticServiceForTests(cfg *setting.Cfg) Service { if cfg == nil { cfg = &setting.Cfg{} @@ -25,7 +45,14 @@ func ProvideService( features featuremgmt.FeatureToggles, kv kvstore.KVStore, cfg *setting.Cfg, + migrator unifiedmigrations.UnifiedStorageMigrationService, ) (Service, error) { + // Ensure migrations have run before starting dualwrite + err := migrator.Run(context.Background()) + if err != nil { + return nil, fmt.Errorf("unable to start dualwrite service due to migration error: %w", err) + } + //nolint:staticcheck // not yet migrated to OpenFeature enabled := features.IsEnabledGlobally(featuremgmt.FlagManagedDualWriter) || features.IsEnabledGlobally(featuremgmt.FlagProvisioning) // required for git provisioning @@ -64,6 +91,37 @@ type service struct { enabled bool } +func (m *service) NewStorage(gr schema.GroupResource, legacy rest.Storage, unified rest.Storage) (rest.Storage, error) { + status, err := m.Status(context.Background(), gr) + if err != nil { + return nil, err + } + + if m.enabled && status.Runtime { + // Dynamic storage behavior + return &runtimeDualWriter{ + service: m, + legacy: legacy, + unified: unified, + dualwrite: &dualWriter{legacy: legacy, unified: unified}, // not used for read + gr: gr, + }, nil + } + + if status.ReadUnified { + if status.WriteLegacy { + // Write both, read unified + return &dualWriter{legacy: legacy, unified: unified, readUnified: true}, nil + } + return unified, nil + } + if status.WriteUnified { + // Write both, read legacy + return &dualWriter{legacy: legacy, unified: unified}, nil + } + return legacy, nil +} + // Hardcoded list of resources that should be controlled by the database (eventually everything?) func (m *service) ShouldManage(gr schema.GroupResource) bool { if !m.enabled { diff --git a/pkg/storage/legacysql/dualwrite/service_test.go b/pkg/storage/legacysql/dualwrite/service_test.go index a083e6470be..f9fcb9f9b38 100644 --- a/pkg/storage/legacysql/dualwrite/service_test.go +++ b/pkg/storage/legacysql/dualwrite/service_test.go @@ -17,7 +17,7 @@ import ( func TestService(t *testing.T) { t.Run("dynamic", func(t *testing.T) { ctx := context.Background() - mode, err := ProvideService(featuremgmt.WithFeatures(), kvstore.NewFakeKVStore(), nil) + mode, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagProvisioning), kvstore.NewFakeKVStore(), NewFakeConfig(), NewFakeMigrator()) require.NoError(t, err) gr := schema.GroupResource{Group: "ggg", Resource: "rrr"} @@ -122,7 +122,7 @@ func TestService(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() - svc, err := ProvideService(tc.flags, kvstore.NewFakeKVStore(), &tc.cfg) + svc, err := ProvideService(tc.flags, kvstore.NewFakeKVStore(), &tc.cfg, NewFakeMigrator()) if tc.error != "" { require.ErrorContains(t, err, tc.error) require.Nil(t, svc, "expect a nil service when an error exts") diff --git a/pkg/storage/legacysql/dualwrite/static.go b/pkg/storage/legacysql/dualwrite/static.go index 3835cf80752..bd51b9b6ac6 100644 --- a/pkg/storage/legacysql/dualwrite/static.go +++ b/pkg/storage/legacysql/dualwrite/static.go @@ -10,8 +10,8 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -// NewDualWriter -- temporary shim -func NewDualWriter( +// NewStaticStorage -- temporary shim +func NewStaticStorage( gr schema.GroupResource, mode rest.DualWriterMode, legacy rest.Storage, diff --git a/pkg/storage/unified/migrations/contract/migrations.go b/pkg/storage/unified/migrations/contract/migrations.go new file mode 100644 index 00000000000..fe1c42cea21 --- /dev/null +++ b/pkg/storage/unified/migrations/contract/migrations.go @@ -0,0 +1,12 @@ +package contract + +import ( + "github.com/grafana/grafana/pkg/registry" +) + +// UnifiedStorageMigrationService provides unified storage migrations as a background service. +// This interface is defined in a separate package to avoid import cycles between +// the migrations implementation and packages that need to depend on it (like dualwrite). +type UnifiedStorageMigrationService interface { + registry.BackgroundService +} diff --git a/pkg/storage/unified/migrations/dashboard_folder_migration.go b/pkg/storage/unified/migrations/dashboard_folder_migration.go deleted file mode 100644 index 96c5a824c8a..00000000000 --- a/pkg/storage/unified/migrations/dashboard_folder_migration.go +++ /dev/null @@ -1,93 +0,0 @@ -package migrations - -import ( - "context" - "fmt" - - "github.com/grafana/authlib/types" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/grafana/grafana/pkg/util/xorm" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -const ( - FoldersAndDashboardsMigrationID = "folders and dashboards migration" - UnifiedStorageDataMigrationSQL = "unified storage data migration" -) - -type dashboardAndFolderMigration struct { - migrator.MigrationBase - legacyMigrator legacy.LegacyMigrator - bulkStoreClient resource.ResourceClient -} - -var _ migrator.CodeMigration = (*dashboardAndFolderMigration)(nil) - -// SQL implements migrator.Migration interface. Returns a description string. -func (sp *dashboardAndFolderMigration) SQL(dialect migrator.Dialect) string { - return UnifiedStorageDataMigrationSQL -} - -func (sp *dashboardAndFolderMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { - ctx := context.Background() - logger := mg.Logger - - resources := []schema.GroupResource{ - { - Group: "folder.grafana.app", - Resource: "folders", - }, - { - Group: "dashboard.grafana.app", - Resource: "dashboards", - }, - } - - storageMigrator := newUnifiedStorageMigrator(sp.legacyMigrator, sp.bulkStoreClient, resources, "unified-storage-migration.folders-dashboards") - - orgs, err := sp.getAllOrgs(sess) - if err != nil { - logger.Error("failed to get organizations for folders and dashboards migration", "error", err) - return fmt.Errorf("failed to get organizations: %w", err) - } - - if len(orgs) == 0 { - logger.Info("No organizations found to migrate, skipping migration") - return nil - } - - logger.Info("Starting migration for all organizations", "org_count", len(orgs)) - - for _, org := range orgs { - namespace := types.OrgNamespaceFormatter(org.ID) - logger.Info("Migrating organization", "org_id", org.ID, "org_name", org.Name, "namespace", namespace) - - // Create a service identity context for this namespace to authenticate with unified storage - migrationCtx, _ := identity.WithServiceIdentityForSingleNamespace(ctx, namespace) - - if err := storageMigrator.executeMigration(migrationCtx, sess, mg, namespace); err != nil { - logger.Error("migration failed for organization", "org_id", org.ID, "org_name", org.Name, "error", err) - return fmt.Errorf("migration failed for org %d (%s): %w", org.ID, org.Name, err) - } - } - - logger.Info("Migration completed successfully for all organizations", "org_count", len(orgs)) - return nil -} - -type orgInfo struct { - ID int64 `xorm:"id"` - Name string `xorm:"name"` -} - -func (sp *dashboardAndFolderMigration) getAllOrgs(sess *xorm.Session) ([]orgInfo, error) { - var orgs []orgInfo - err := sess.Table("org").Cols("id", "name").Find(&orgs) - if err != nil { - return nil, err - } - return orgs, nil -} diff --git a/pkg/storage/unified/migrations/migrations.go b/pkg/storage/unified/migrations/migrations.go deleted file mode 100644 index a30777e308a..00000000000 --- a/pkg/storage/unified/migrations/migrations.go +++ /dev/null @@ -1,112 +0,0 @@ -package migrations - -import ( - "context" - "fmt" - "os" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/prometheus/client_golang/prometheus" - "go.opentelemetry.io/otel" -) - -var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/migrations") -var logger = log.New("storage.unified.migrations") - -// UnifiedStorageMigrationProvider provides unified storage migrations as a background service -type UnifiedStorageMigrationProvider interface { - registry.BackgroundService -} - -type UnifiedStorageMigrationProviderImpl struct { - legacyMigrator legacy.LegacyMigrator - cfg *setting.Cfg - client resource.ResourceClient - sqlStore db.DB -} - -var _ UnifiedStorageMigrationProvider = (*UnifiedStorageMigrationProviderImpl)(nil) - -// ProvideUnifiedStorageMigrationProvider is a Wire provider that creates the migration service. -// The service implements registry.BackgroundService and runs migrations during server startup. -func ProvideUnifiedStorageMigrationProvider( - legacyMigrator legacy.LegacyMigrator, - cfg *setting.Cfg, - client resource.ResourceClient, - sqlStore db.DB, -) *UnifiedStorageMigrationProviderImpl { - return &UnifiedStorageMigrationProviderImpl{ - legacyMigrator: legacyMigrator, - cfg: cfg, - client: client, - sqlStore: sqlStore, - } -} - -// Run executes unified storage migrations as a background service. -// This blocks until migrations complete. If migrations fail, an error is returned -// which will prevent Grafana from starting. -func (p *UnifiedStorageMigrationProviderImpl) Run(ctx context.Context) error { - // skip migrations in test environments to prevent integration test timeouts. - if os.Getenv("GRAFANA_TEST_DB") != "" { - return nil - } - // skip migrations if disabled in config - if p.cfg.DisableDataMigrations { - logger.Info("Data migrations are disabled, skipping") - return nil - } - // TODO: Re-enable once migrations are ready - // return RegisterMigrations(p.legacyMigrator, p.cfg, p.client, p.sqlStore) - return nil -} - -// RegisterMigrations initializes and registers all unified storage migrations. -// This function is the entry point for all data migrations from legacy storage -// to unified storage. It returns an error if migrations fail, preventing Grafana -// from starting with inconsistent data. -func RegisterMigrations( - legacyMigrator legacy.LegacyMigrator, - cfg *setting.Cfg, - client resource.ResourceClient, - sqlStore db.DB, -) error { - ctx, span := tracer.Start(context.Background(), "storage.unified.RegisterMigrations") - defer span.End() - mg := migrator.NewScopedMigrator(sqlStore.GetEngine(), cfg, "unified_storage") - mg.AddCreateMigration() - - if err := prometheus.Register(mg); err != nil { - logger.Warn("Failed to register migrator metrics", "error", err) - } - - // Add new migration registrations here for each resource type - registerDashboardAndFolderMigration(mg, legacyMigrator, client) - - // Run all registered migrations (blocking) - sec := cfg.Raw.Section("database") - if err := mg.RunMigrations(ctx, sec.Key("migration_locking").MustBool(true), sec.Key("locking_attempt_timeout_sec").MustInt()); err != nil { - return fmt.Errorf("unified storage data migration failed: %w", err) - } - - logger.Info("Unified storage migrations completed successfully") - return nil -} - -func registerDashboardAndFolderMigration( - mg *migrator.Migrator, - legacyMigrator legacy.LegacyMigrator, - bulkStoreClient resource.ResourceClient, -) { - migration := &dashboardAndFolderMigration{ - legacyMigrator: legacyMigrator, - bulkStoreClient: bulkStoreClient, - } - mg.AddMigration(FoldersAndDashboardsMigrationID, migration) -} diff --git a/pkg/storage/unified/migrations/migrator.go b/pkg/storage/unified/migrations/migrator.go index cab6ce38432..bdbb69f98a1 100644 --- a/pkg/storage/unified/migrations/migrator.go +++ b/pkg/storage/unified/migrations/migrator.go @@ -3,164 +3,200 @@ package migrations import ( "context" "fmt" - "time" - "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "google.golang.org/grpc/metadata" + + authlib "github.com/grafana/authlib/types" + + v1beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - "github.com/grafana/grafana/pkg/util/xorm" - "k8s.io/apimachinery/pkg/runtime/schema" ) -// StorageMigrator defines the interface for executing unified storage migrations -type StorageMigrator interface { - executeMigration(ctx context.Context, sess *xorm.Session, mg *migrator.Migrator, namespace string) error +// Read from legacy and write into unified storage +// +//go:generate mockery --name UnifiedMigrator --structname MockUnifiedMigrator --inpackage --filename migrator_mock.go --with-expecter +type UnifiedMigrator interface { + Migrate(ctx context.Context, opts legacy.MigrateOptions) (*resourcepb.BulkResponse, error) } -type unifiedStorageMigrator struct { - migrator legacy.LegacyMigrator - bulkStoreClient resource.ResourceClient - resources []schema.GroupResource - log log.Logger +// unifiedMigration handles the migration of legacy resources to unified storage +type unifiedMigration struct { + legacy.MigrationDashboardAccessor + streamProvider streamProvider + log log.Logger } -func newUnifiedStorageMigrator(migrator legacy.LegacyMigrator, bulkStoreClient resource.ResourceClient, resources []schema.GroupResource, logPrefix string) StorageMigrator { - return &unifiedStorageMigrator{ - migrator: migrator, - bulkStoreClient: bulkStoreClient, - resources: resources, - log: log.New(logPrefix), - } +// streamProvider abstracts the different ways to create a bulk process stream +type streamProvider interface { + createStream(ctx context.Context, opts legacy.MigrateOptions) (resourcepb.BulkStore_BulkProcessClient, error) } -func (m *unifiedStorageMigrator) executeMigration(ctx context.Context, sess *xorm.Session, mg *migrator.Migrator, namespace string) error { - startTime := time.Now() - m.log.Info("Starting unified storage migration", "namespace", namespace, "resources", m.resources) - - opts := legacy.MigrateOptions{ - Namespace: namespace, - Store: m.bulkStoreClient, - LargeObjects: nil, // Not using large object support to avoid import cycles - Resources: m.resources, - WithHistory: true, // Migrate with full history - OnlyCount: false, - Progress: func(count int, msg string) { - m.log.Info("Migration progress", "count", count, "message", msg) - }, - } - - // Execute the migration via legacy migrator - response, err := m.migrator.Migrate(ctx, opts) - if err != nil { - m.log.Error("Migration failed", "error", err, "duration", time.Since(startTime)) - return fmt.Errorf("failed to migrate resources: %w", err) - } - - // Validate the migration results - if err := m.validateMigration(sess, response); err != nil { - m.log.Error("Migration validation failed", "error", err, "duration", time.Since(startTime)) - return fmt.Errorf("migration validation failed: %w", err) - } - - m.log.Info("Migration completed successfully", - "duration", time.Since(startTime), - "processed", response.Processed, - "summaries", len(response.Summary), - "rejected", len(response.Rejected)) - - return nil +// resourceClientStreamProvider creates streams using resource.ResourceClient +type resourceClientStreamProvider struct { + client resource.ResourceClient } -func (m *unifiedStorageMigrator) validateMigration(sess *xorm.Session, response *resourcepb.BulkResponse) error { - // Check for rejected items - if len(response.Rejected) > 0 { - m.log.Warn("Migration had rejected items", "count", len(response.Rejected)) - for i, rejected := range response.Rejected { - if i < 10 { // Log first 10 rejected items - m.log.Warn("Rejected item", - "namespace", rejected.Key.Namespace, - "group", rejected.Key.Group, - "resource", rejected.Key.Resource, - "name", rejected.Key.Name, - "reason", rejected.Error) - } +func (r *resourceClientStreamProvider) createStream(ctx context.Context, opts legacy.MigrateOptions) (resourcepb.BulkStore_BulkProcessClient, error) { + // Build collection settings for resource client + settings := resource.BulkSettings{ + RebuildCollection: true, + SkipValidation: true, + } + for _, res := range opts.Resources { + switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { + case "folder.grafana.app/folders": + settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: folders.GROUP, + Resource: folders.RESOURCE, + }) + case "dashboard.grafana.app/librarypanels": + settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: v1beta1.GROUP, + Resource: v1beta1.LIBRARY_PANEL_RESOURCE, + }) + case "dashboard.grafana.app/dashboards": + settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: v1beta1.GROUP, + Resource: v1beta1.DASHBOARD_RESOURCE, + }) } - // Rejections are not fatal - they may be expected for invalid data + } + ctx = metadata.NewOutgoingContext(ctx, settings.ToMD()) + return r.client.BulkProcess(ctx) +} + +// bulkStoreClientStreamProvider creates streams using resourcepb.BulkStoreClient +type bulkStoreClientStreamProvider struct { + client resourcepb.BulkStoreClient +} + +func (b *bulkStoreClientStreamProvider) createStream(ctx context.Context, opts legacy.MigrateOptions) (resourcepb.BulkStore_BulkProcessClient, error) { + // Build collection settings for resource client + settings := resource.BulkSettings{ + RebuildCollection: true, + SkipValidation: true, + } + for _, res := range opts.Resources { + switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { + case "folder.grafana.app/folders": + settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: folders.GROUP, + Resource: folders.RESOURCE, + }) + case "dashboard.grafana.app/librarypanels": + settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: v1beta1.GROUP, + Resource: v1beta1.LIBRARY_PANEL_RESOURCE, + }) + case "dashboard.grafana.app/dashboards": + settings.Collection = append(settings.Collection, &resourcepb.ResourceKey{ + Namespace: opts.Namespace, + Group: v1beta1.GROUP, + Resource: v1beta1.DASHBOARD_RESOURCE, + }) + } + } + ctx = metadata.NewOutgoingContext(ctx, settings.ToMD()) + return b.client.BulkProcess(ctx) +} + +// This can migrate Folders, Dashboards and LibraryPanels +func ProvideUnifiedMigrator( + dashboardAccess legacy.MigrationDashboardAccessor, + client resource.ResourceClient, +) UnifiedMigrator { + return newUnifiedMigrator( + dashboardAccess, + &resourceClientStreamProvider{client: client}, + log.New("storage.unified.migrator"), + ) +} + +func ProvideUnifiedMigratorParquet( + dashboardAccess legacy.MigrationDashboardAccessor, + client resourcepb.BulkStoreClient, +) UnifiedMigrator { + return newUnifiedMigrator( + dashboardAccess, + &bulkStoreClientStreamProvider{client: client}, + log.New("storage.unified.migrator.parquet"), + ) +} + +func newUnifiedMigrator( + dashboardAccess legacy.MigrationDashboardAccessor, + streamProvider streamProvider, + log log.Logger, +) UnifiedMigrator { + return &unifiedMigration{ + MigrationDashboardAccessor: dashboardAccess, + streamProvider: streamProvider, + log: log, + } +} + +// migrate function -- works for a single kind +type migratorFunc = func(ctx context.Context, orgId int64, opts legacy.MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*legacy.BlobStoreInfo, error) + +func (m *unifiedMigration) Migrate(ctx context.Context, opts legacy.MigrateOptions) (*resourcepb.BulkResponse, error) { + info, err := authlib.ParseNamespace(opts.Namespace) + if err != nil { + return nil, err + } + if opts.Progress == nil { + opts.Progress = func(count int, msg string) {} // noop } - // Validate counts for each resource type - for _, summary := range response.Summary { - legacyCount, err := m.getLegacyCount(sess, summary.Group, summary.Resource, summary.Namespace) + if len(opts.Resources) < 1 { + return nil, fmt.Errorf("missing resource selector") + } + + if opts.OnlyCount { + return m.CountResources(ctx, opts) + } + + stream, err := m.streamProvider.createStream(ctx, opts) + if err != nil { + return nil, err + } + + migratorFuncs := []migratorFunc{} + for _, res := range opts.Resources { + switch fmt.Sprintf("%s/%s", res.Group, res.Resource) { + case "folder.grafana.app/folders": + migratorFuncs = append(migratorFuncs, m.MigrateFolders) + case "dashboard.grafana.app/librarypanels": + migratorFuncs = append(migratorFuncs, m.MigrateLibraryPanels) + case "dashboard.grafana.app/dashboards": + migratorFuncs = append(migratorFuncs, m.MigrateDashboards) + default: + return nil, fmt.Errorf("unsupported resource: %s", res) + } + } + + // Execute migrations + blobStore := legacy.BlobStoreInfo{} + m.log.Info("start migrating legacy resources", "namespace", opts.Namespace, "orgId", info.OrgID, "stackId", info.StackID) + for _, fn := range migratorFuncs { + blobs, err := fn(ctx, info.OrgID, opts, stream) if err != nil { - return fmt.Errorf("failed to get legacy count for %s/%s: %w", summary.Group, summary.Resource, err) + m.log.Error("error migrating legacy resources", "error", err, "namespace", opts.Namespace) + return nil, err } - - // Account for rejected items in validation - expectedCount := summary.Count + int64(len(response.Rejected)) - - m.log.Info("Count validation", - "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), - "namespace", summary.Namespace, - "legacy_count", legacyCount, - "unified_count", summary.Count, - "rejected", len(response.Rejected), - "history", summary.History) - - // Validate that we migrated all items (allowing for rejected items) - if legacyCount > expectedCount { - return fmt.Errorf("count mismatch for %s.%s in namespace %s: legacy has %d, unified has %d, rejected %d", - summary.Resource, summary.Group, summary.Namespace, - legacyCount, summary.Count, len(response.Rejected)) + if blobs != nil { + blobStore.Count += blobs.Count + blobStore.Size += blobs.Size } } - - return nil -} - -func (m *unifiedStorageMigrator) getLegacyCount(sess *xorm.Session, group, resourceType, namespace string) (int64, error) { - // Parse namespace to get org ID - orgID, err := ParseOrgIDFromNamespace(namespace) - if err != nil { - return 0, fmt.Errorf("invalid namespace %s: %w", namespace, err) - } - - // Map group/resource to legacy table - tableName, whereClause := m.getLegacyTableInfo(group, resourceType) - if tableName == "" { - return 0, fmt.Errorf("unknown resource type: %s.%s", resourceType, group) - } - - // Count items in legacy table using Table() before Count() - count, err := sess.Table(tableName).Where(whereClause, orgID).Count() - if err != nil { - return 0, fmt.Errorf("failed to count %s: %w", tableName, err) - } - - return count, nil -} - -func (m *unifiedStorageMigrator) getLegacyTableInfo(group, resource string) (table string, whereClause string) { - // Map unified storage group/resource to legacy tables - switch { - case group == "dashboard.grafana.app" && resource == "dashboards": - return "dashboard", "org_id = ? and is_folder = false" - case group == "folder.grafana.app" && resource == "folders": - return "dashboard", "org_id = ? and is_folder = true" - case group == "playlist.grafana.app" && resource == "playlists": - return "playlist", "org_id = ?" - default: - return "", "" - } -} - -func ParseOrgIDFromNamespace(namespace string) (int64, error) { - // Use authlib to properly parse all namespace formats including "default" for org 1 - info, err := types.ParseNamespace(namespace) - if err != nil { - return 0, fmt.Errorf("failed to parse namespace: %w", err) - } - return info.OrgID, nil + m.log.Info("finished migrating legacy resources", "blobStore", blobStore) + return stream.CloseAndRecv() } diff --git a/pkg/storage/unified/migrations/migrator_mock.go b/pkg/storage/unified/migrations/migrator_mock.go new file mode 100644 index 00000000000..3b460ca6e5b --- /dev/null +++ b/pkg/storage/unified/migrations/migrator_mock.go @@ -0,0 +1,98 @@ +// Code generated by mockery v2.53.4. DO NOT EDIT. + +package migrations + +import ( + context "context" + + legacy "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + mock "github.com/stretchr/testify/mock" + + resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" +) + +// MockUnifiedMigrator is an autogenerated mock type for the UnifiedMigrator type +type MockUnifiedMigrator struct { + mock.Mock +} + +type MockUnifiedMigrator_Expecter struct { + mock *mock.Mock +} + +func (_m *MockUnifiedMigrator) EXPECT() *MockUnifiedMigrator_Expecter { + return &MockUnifiedMigrator_Expecter{mock: &_m.Mock} +} + +// Migrate provides a mock function with given fields: ctx, opts +func (_m *MockUnifiedMigrator) Migrate(ctx context.Context, opts legacy.MigrateOptions) (*resourcepb.BulkResponse, error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Migrate") + } + + var r0 *resourcepb.BulkResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, legacy.MigrateOptions) (*resourcepb.BulkResponse, error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, legacy.MigrateOptions) *resourcepb.BulkResponse); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*resourcepb.BulkResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, legacy.MigrateOptions) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockUnifiedMigrator_Migrate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Migrate' +type MockUnifiedMigrator_Migrate_Call struct { + *mock.Call +} + +// Migrate is a helper method to define mock.On call +// - ctx context.Context +// - opts legacy.MigrateOptions +func (_e *MockUnifiedMigrator_Expecter) Migrate(ctx interface{}, opts interface{}) *MockUnifiedMigrator_Migrate_Call { + return &MockUnifiedMigrator_Migrate_Call{Call: _e.mock.On("Migrate", ctx, opts)} +} + +func (_c *MockUnifiedMigrator_Migrate_Call) Run(run func(ctx context.Context, opts legacy.MigrateOptions)) *MockUnifiedMigrator_Migrate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(legacy.MigrateOptions)) + }) + return _c +} + +func (_c *MockUnifiedMigrator_Migrate_Call) Return(_a0 *resourcepb.BulkResponse, _a1 error) *MockUnifiedMigrator_Migrate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockUnifiedMigrator_Migrate_Call) RunAndReturn(run func(context.Context, legacy.MigrateOptions) (*resourcepb.BulkResponse, error)) *MockUnifiedMigrator_Migrate_Call { + _c.Call.Return(run) + return _c +} + +// NewMockUnifiedMigrator creates a new instance of MockUnifiedMigrator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockUnifiedMigrator(t interface { + mock.TestingT + Cleanup(func()) +}) *MockUnifiedMigrator { + mock := &MockUnifiedMigrator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/storage/unified/migrations/resource_migration.go b/pkg/storage/unified/migrations/resource_migration.go new file mode 100644 index 00000000000..663bf030f67 --- /dev/null +++ b/pkg/storage/unified/migrations/resource_migration.go @@ -0,0 +1,275 @@ +package migrations + +import ( + "context" + "fmt" + "time" + + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/util/xorm" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// ValidationFunc is a function that validates migration results. +// It receives the database session, migration response, and logger for reporting. +// Return an error if validation fails, nil if validation passes or is skipped. +type ValidationFunc func(sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error + +// ResourceMigration handles migration of specific resource types from legacy to unified storage. +// It implements migrator.CodeMigration and provides a generic, extensible way to migrate any +// resource type by: +// +// 1. Iterating through all organizations +// 2. For each org, delegating to LegacyMigrator to read from legacy and write to unified storage +// 3. Validating migration results using the provided validation function (if any) +// +// To add a new resource type migration, simply create a new ResourceMigration instance in +// service.go with the appropriate schema.GroupResource specifications and optional validation function. +type ResourceMigration struct { + migrator.MigrationBase + migrator UnifiedMigrator + resources []schema.GroupResource + migrationID string + validationFunc ValidationFunc // Optional: custom validation logic for this migration + log log.Logger +} + +// NewResourceMigration creates a new migration for the specified resources. +// This is the primary way to register new resource migrations. +// +// Parameters: +// - legacyMigrator: handles reading from legacy storage and writing to unified storage +// - resources: list of GroupResource to migrate +// - migrationID: unique identifier for this migration +// - validationFunc: optional validation function to verify migration results. +// If nil, no validation will be performed. +// +// Example with legacy table count validation: +// +// NewResourceMigration( +// migrator, +// []schema.GroupResource{{Group: "playlist.grafana.app", Resource: "playlists"}}, +// "playlists", +// NewLegacyTableCountValidator(map[string]LegacyTableInfo{ +// "playlist.grafana.app/playlists": {Table: "playlist", WhereClause: "org_id = ?"}, +// }), +// ) +// +// Example without validation: +// +// NewResourceMigration(migrator, resources, "new-resource", nil) +func NewResourceMigration( + migrator UnifiedMigrator, + resources []schema.GroupResource, + migrationID string, + validationFunc ValidationFunc, +) *ResourceMigration { + return &ResourceMigration{ + migrator: migrator, + resources: resources, + migrationID: migrationID, + validationFunc: validationFunc, + log: log.New("storage.unified.resource_migration." + migrationID), + } +} + +var _ migrator.CodeMigration = (*ResourceMigration)(nil) + +// SQL implements migrator.Migration interface. Returns a description string. +func (m *ResourceMigration) SQL(_ migrator.Dialect) string { + return fmt.Sprintf("unified storage data migration: %s", m.migrationID) +} + +// Exec implements migrator.CodeMigration interface. Executes the migration across all organizations. +func (m *ResourceMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { + ctx := context.Background() + + orgs, err := m.getAllOrgs(sess) + if err != nil { + m.log.Error("failed to get organizations", "error", err) + return fmt.Errorf("failed to get organizations: %w", err) + } + + if len(orgs) == 0 { + m.log.Info("No organizations found to migrate, skipping migration") + return nil + } + + m.log.Info("Starting migration for all organizations", "org_count", len(orgs), "resources", m.resources) + + for _, org := range orgs { + if err := m.migrateOrg(ctx, sess, org); err != nil { + return err + } + } + + m.log.Info("Migration completed successfully for all organizations", "org_count", len(orgs)) + return nil +} + +// migrateOrg handles migration for a single organization +func (m *ResourceMigration) migrateOrg(ctx context.Context, sess *xorm.Session, org orgInfo) error { + namespace := types.OrgNamespaceFormatter(org.ID) + m.log.Info("Migrating organization", "org_id", org.ID, "org_name", org.Name, "namespace", namespace) + + // Create a service identity context for this namespace to authenticate with unified storage + migrationCtx, _ := identity.WithServiceIdentityForSingleNamespace(ctx, namespace) + + startTime := time.Now() + + opts := legacy.MigrateOptions{ + Namespace: namespace, + Resources: m.resources, + WithHistory: true, // Migrate with full history + Progress: func(count int, msg string) { + m.log.Info("Migration progress", "org_id", org.ID, "count", count, "message", msg) + }, + } + + // Execute the migration via legacy migrator + response, err := m.migrator.Migrate(migrationCtx, opts) + if err != nil { + m.log.Error("Migration failed", "org_id", org.ID, "error", err, "duration", time.Since(startTime)) + return fmt.Errorf("migration failed for org %d (%s): %w", org.ID, org.Name, err) + } + + // Validate the migration results + if err := m.validateMigration(sess, response); err != nil { + m.log.Error("Migration validation failed", "org_id", org.ID, "error", err, "duration", time.Since(startTime)) + return fmt.Errorf("migration validation failed for org %d (%s): %w", org.ID, org.Name, err) + } + + m.log.Info("Migration completed for organization", + "org_id", org.ID, + "duration", time.Since(startTime), + "processed", response.Processed, + "summaries", len(response.Summary), + "rejected", len(response.Rejected)) + + return nil +} + +// validateMigration calls the custom validation function if provided +func (m *ResourceMigration) validateMigration(sess *xorm.Session, response *resourcepb.BulkResponse) error { + if m.validationFunc == nil { + m.log.Debug("No validation function provided, skipping validation") + return nil + } + + return m.validationFunc(sess, response, m.log) +} + +// LegacyTableInfo defines how to map a unified storage resource to its legacy table +type LegacyTableInfo struct { + Table string // Legacy table name (e.g., "dashboard", "playlist") + WhereClause string // WHERE clause template with org_id parameter (e.g., "org_id = ? and is_folder = false") +} + +// NewLegacyTableCountValidator creates a ValidationFunc that validates migration by comparing +// counts between legacy tables and unified storage. +// +// This is a helper for the common case of validating that all items from legacy tables +// were successfully migrated to unified storage. +// +// Parameters: +// - legacyTableMap: maps "group/resource" keys to LegacyTableInfo for validation. +// Only resources with mappings will be validated. +// +// Example: +// +// validator := NewLegacyTableCountValidator(map[string]LegacyTableInfo{ +// "dashboard.grafana.app/dashboards": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = false"}, +// "folder.grafana.app/folders": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = true"}, +// }) +func NewLegacyTableCountValidator(legacyTableMap map[string]LegacyTableInfo) ValidationFunc { + return func(sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error { + // Check for rejected items + if len(response.Rejected) > 0 { + log.Warn("Migration had rejected items", "count", len(response.Rejected)) + for i, rejected := range response.Rejected { + if i < 10 { // Log first 10 rejected items + log.Warn("Rejected item", + "namespace", rejected.Key.Namespace, + "group", rejected.Key.Group, + "resource", rejected.Key.Resource, + "name", rejected.Key.Name, + "reason", rejected.Error) + } + } + // Rejections are not fatal - they may be expected for invalid data + } + + // Validate counts for each resource type + for _, summary := range response.Summary { + key := fmt.Sprintf("%s/%s", summary.Group, summary.Resource) + tableInfo, ok := legacyTableMap[key] + if !ok { + log.Debug("No legacy table mapping for resource, skipping count validation", + "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), + "namespace", summary.Namespace) + continue + } + + // Get legacy count + orgID, err := ParseOrgIDFromNamespace(summary.Namespace) + if err != nil { + return fmt.Errorf("invalid namespace %s: %w", summary.Namespace, err) + } + + legacyCount, err := sess.Table(tableInfo.Table).Where(tableInfo.WhereClause, orgID).Count() + if err != nil { + return fmt.Errorf("failed to count %s: %w", tableInfo.Table, err) + } + + // Account for rejected items in validation + expectedCount := summary.Count + int64(len(response.Rejected)) + + log.Info("Count validation", + "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), + "namespace", summary.Namespace, + "legacy_count", legacyCount, + "unified_count", summary.Count, + "rejected", len(response.Rejected), + "history", summary.History) + + // Validate that we migrated all items (allowing for rejected items) + if legacyCount > expectedCount { + return fmt.Errorf("count mismatch for %s.%s in namespace %s: legacy has %d, unified has %d, rejected %d", + summary.Resource, summary.Group, summary.Namespace, + legacyCount, summary.Count, len(response.Rejected)) + } + } + + return nil + } +} + +func ParseOrgIDFromNamespace(namespace string) (int64, error) { + // Use authlib to properly parse all namespace formats including "default" for org 1 + info, err := types.ParseNamespace(namespace) + if err != nil { + return 0, fmt.Errorf("failed to parse namespace: %w", err) + } + return info.OrgID, nil +} + +// orgInfo represents basic organization information +type orgInfo struct { + ID int64 `xorm:"id"` + Name string `xorm:"name"` +} + +// getAllOrgs retrieves all organizations from the database +func (m *ResourceMigration) getAllOrgs(sess *xorm.Session) ([]orgInfo, error) { + var orgs []orgInfo + err := sess.Table("org").Cols("id", "name").Find(&orgs) + if err != nil { + return nil, err + } + return orgs, nil +} diff --git a/pkg/storage/unified/migrations/service.go b/pkg/storage/unified/migrations/service.go new file mode 100644 index 00000000000..5bcb2fdf8c2 --- /dev/null +++ b/pkg/storage/unified/migrations/service.go @@ -0,0 +1,118 @@ +package migrations + +import ( + "context" + "fmt" + "os" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/grafana/grafana/pkg/infra/log" + sqlstoremigrator "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/migrations/contract" + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/migrations") +var logger = log.New("storage.unified.migrations") + +type UnifiedStorageMigrationServiceImpl struct { + migrator UnifiedMigrator + cfg *setting.Cfg + sqlStore db.DB + kv kvstore.KVStore +} + +var _ contract.UnifiedStorageMigrationService = (*UnifiedStorageMigrationServiceImpl)(nil) + +// ProvideUnifiedStorageMigrationService is a Wire provider that creates the migration service. +// The service implements registry.BackgroundService and runs migrations during server startup. +func ProvideUnifiedStorageMigrationService( + migrator UnifiedMigrator, + cfg *setting.Cfg, + sqlStore db.DB, + kv kvstore.KVStore, +) contract.UnifiedStorageMigrationService { + return &UnifiedStorageMigrationServiceImpl{ + migrator: migrator, + cfg: cfg, + sqlStore: sqlStore, + kv: kv, + } +} + +// Run executes unified storage migrations as a background service. +// This blocks until migrations complete. If migrations fail, an error is returned +// which will prevent Grafana from starting. +func (p *UnifiedStorageMigrationServiceImpl) Run(ctx context.Context) error { + // TODO: temporary skip migrations in test environments to prevent integration test timeouts. + if os.Getenv("GRAFANA_TEST_DB") != "" { + return nil + } + + // skip migrations if disabled in config + if p.cfg.DisableDataMigrations { + logger.Info("Data migrations are disabled, skipping") + return nil + } + + // TODO: Re-enable once migrations are ready + // TODO: add guarantee that this only runs once + // return RegisterMigrations(p.migrator, p.cfg, p.sqlStore) + return nil +} + +// RegisterMigrations initializes and registers all unified storage migrations. +// This function is the entry point for all data migrations from legacy storage +// to unified storage. It returns an error if migrations fail, preventing Grafana +// from starting with inconsistent data. +func RegisterMigrations( + migrator UnifiedMigrator, + cfg *setting.Cfg, + sqlStore db.DB, +) error { + ctx, span := tracer.Start(context.Background(), "storage.unified.RegisterMigrations") + defer span.End() + mg := sqlstoremigrator.NewScopedMigrator(sqlStore.GetEngine(), cfg, "unifiedstorage") + mg.AddCreateMigration() + + if err := prometheus.Register(mg); err != nil { + logger.Warn("Failed to register migrator metrics", "error", err) + } + + // Register resource migrations + // To add a new resource type, simply add another migration here with the appropriate resources + registerResourceMigrations(mg, migrator) + + // Run all registered migrations (blocking) + sec := cfg.Raw.Section("database") + if err := mg.RunMigrations(ctx, + sec.Key("migration_locking").MustBool(true), + sec.Key("locking_attempt_timeout_sec").MustInt()); err != nil { + return fmt.Errorf("unified storage data migration failed: %w", err) + } + + logger.Info("Unified storage migrations completed successfully") + return nil +} + +// registerResourceMigrations registers all unified storage resource migrations. +// Add new resource types here by creating additional ResourceMigration instances. +func registerResourceMigrations(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator) { + dashboardsAndFolders := NewResourceMigration( + migrator, + []schema.GroupResource{ + {Group: "folder.grafana.app", Resource: "folders"}, + {Group: "dashboard.grafana.app", Resource: "dashboards"}, + }, + "folders-dashboards", + NewLegacyTableCountValidator(map[string]LegacyTableInfo{ + "folder.grafana.app/folders": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = true"}, + "dashboard.grafana.app/dashboards": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = false"}, + }), + ) + mg.AddMigration("folders and dashboards migration", dashboardsAndFolders) +} From 534ed3421b75505c6a6f376e18c062a7b34833d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Thu, 20 Nov 2025 17:09:49 +0100 Subject: [PATCH 031/378] Fix search by both tags and folders. (#114246) * Fix search by both tags and folders. * Move // nolint:gocyclo to the new method. * Revert unnecessary change. --- pkg/registry/apis/dashboard/search.go | 118 ++--- pkg/registry/apis/dashboard/search_test.go | 491 +++++++++++++++++---- 2 files changed, 470 insertions(+), 139 deletions(-) diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index ec2f6053b95..0f1b83a7e18 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -3,6 +3,7 @@ package dashboard import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -272,7 +273,8 @@ func (s *SearchHandler) DoSortable(w http.ResponseWriter, r *http.Request) { const rootFolder = "general" -// nolint:gocyclo +var errEmptyResults = fmt.Errorf("empty results") + func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { ctx, span := s.tracer.Start(r.Context(), "dashboard.search") defer span.End() @@ -289,6 +291,51 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { return } + searchRequest, err := convertHttpSearchRequestToResourceSearchRequest(queryParams, user, func() ([]string, error) { + return s.getDashboardsUIDsSharedWithUser(ctx, user) + }) + if err != nil { + if errors.Is(err, errEmptyResults) { + s.write(w, dashboardv0alpha1.SearchResults{ + Hits: []dashboardv0alpha1.DashboardHit{}, + }) + } else { + errhttp.Write(ctx, err, w) + } + return + } + + result, err := s.client.Search(ctx, searchRequest) + if err != nil { + errhttp.Write(ctx, err, w) + return + } + + if result != nil { + s.log.Debug("search result hits and cost", "total_hits", result.TotalHits, "query_cost", result.QueryCost) + } + + parsedResults, err := dashboardsearch.ParseResults(result, searchRequest.Offset) + if err != nil { + errhttp.Write(ctx, err, w) + return + } + + if len(searchRequest.SortBy) == 0 { + // default sort by resource descending ( folders then dashboards ) then title + sort.Slice(parsedResults.Hits, func(i, j int) bool { + // Just sorting by resource for now. The rest should be sorted by search score already + return parsedResults.Hits[i].Resource > parsedResults.Hits[j].Resource + }) + } + + s.write(w, parsedResults) +} + +// convertHttpSearchRequestToResourceSearchRequest create ResourceSearchRequest from query parameters. +// Supplied function is used to get dashboards shared with user. +// nolint:gocyclo +func convertHttpSearchRequestToResourceSearchRequest(queryParams url.Values, user identity.Requester, getDashboardsUIDsSharedWithUser func() ([]string, error)) (*resourcepb.ResourceSearchRequest, error) { // get limit and offset from query params limit := 50 offset := 0 @@ -339,6 +386,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { hasDash := len(types) == 0 || slices.Contains(types, "dashboard") hasFolder := len(types) == 0 || slices.Contains(types, "folder") // If both types are present, we need to search both dashboards and folders, by default is nothing is set we also search both. + var err error if (hasDash && hasFolder) || (!hasDash && !hasFolder) { searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), dashboardv0alpha1.DASHBOARD_RESOURCE) if err == nil { @@ -352,8 +400,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), dashboardv0alpha1.DASHBOARD_RESOURCE) } if err != nil { - errhttp.Write(ctx, err, w) - return + return nil, err } // Add sorting @@ -384,20 +431,20 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { // The tags filter if tags, ok := queryParams["tag"]; ok { - searchRequest.Options.Fields = []*resourcepb.Requirement{{ + searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ Key: "tags", Operator: "=", Values: tags, - }} + }) } // The libraryPanel filter if libraryPanel, ok := queryParams["libraryPanel"]; ok { - searchRequest.Options.Fields = []*resourcepb.Requirement{{ + searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ Key: search.DASHBOARD_LIBRARY_PANEL_REFERENCE, Operator: "=", Values: libraryPanel, - }} + }) } // The names filter @@ -406,17 +453,13 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { // Add the folder constraint. Note this does not do recursive search folder := queryParams.Get("folder") if folder == foldermodel.SharedWithMeFolderUID { - dashboardUIDs, err := s.getDashboardsUIDsSharedWithUser(ctx, user) + dashboardUIDs, err := getDashboardsUIDsSharedWithUser() if err != nil { - errhttp.Write(ctx, err, w) - return + return nil, err } if len(dashboardUIDs) == 0 { - s.write(w, dashboardv0alpha1.SearchResults{ - Hits: []dashboardv0alpha1.DashboardHit{}, - }) - return + return nil, errEmptyResults } // hijacks the "name" query param to only search for shared dashboard UIDs names = append(names, dashboardUIDs...) @@ -424,50 +467,21 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { if folder == rootFolder { folder = "" // root folder is empty in the search index } - searchRequest.Options.Fields = []*resourcepb.Requirement{{ + searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ Key: "folder", Operator: "=", Values: []string{folder}, - }} - } - - if len(names) > 0 { - if searchRequest.Options.Fields == nil { - searchRequest.Options.Fields = []*resourcepb.Requirement{} - } - namesFilter := []*resourcepb.Requirement{{ - Key: "name", - Operator: "in", - Values: names, - }} - searchRequest.Options.Fields = append(searchRequest.Options.Fields, namesFilter...) - } - - result, err := s.client.Search(ctx, searchRequest) - if err != nil { - errhttp.Write(ctx, err, w) - return - } - - if result != nil { - s.log.Debug("search result hits and cost", "total_hits", result.TotalHits, "query_cost", result.QueryCost) - } - - parsedResults, err := dashboardsearch.ParseResults(result, searchRequest.Offset) - if err != nil { - errhttp.Write(ctx, err, w) - return - } - - if len(searchRequest.SortBy) == 0 { - // default sort by resource descending ( folders then dashboards ) then title - sort.Slice(parsedResults.Hits, func(i, j int) bool { - // Just sorting by resource for now. The rest should be sorted by search score already - return parsedResults.Hits[i].Resource > parsedResults.Hits[j].Resource }) } - s.write(w, parsedResults) + if len(names) > 0 { + searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ + Key: "name", + Operator: "in", + Values: names, + }) + } + return searchRequest, nil } func (s *SearchHandler) write(w http.ResponseWriter, obj any) { diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index b392cb1606c..ab7f05e7d00 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http/httptest" + "net/url" "testing" "github.com/stretchr/testify/assert" @@ -17,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -282,93 +284,6 @@ func TestSearchHandlerPagination(t *testing.T) { } func TestSearchHandler(t *testing.T) { - t.Run("Multiple comma separated fields will be appended to default dashboard search fields", func(t *testing.T) { - // Create a mock client - mockClient := &MockClient{} - - features := featuremgmt.WithFeatures() - // Initialize the search handler with the mock client - searchHandler := SearchHandler{ - log: log.New("test", "test"), - client: mockClient, - tracer: tracing.NewNoopTracerService(), - features: features, - } - - rr := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/search?field=field1&field=field2&field=field3", nil) - req.Header.Add("content-type", "application/json") - req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) - - searchHandler.DoSearch(rr, req) - - if mockClient.LastSearchRequest == nil { - t.Fatalf("expected Search to be called, but it was not") - } - expectedFields := []string{"title", "folder", "tags", "description", "manager.kind", "manager.id", "field1", "field2", "field3"} - if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) { - t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) - } - }) - - t.Run("Single field will be appended to default dashboard search fields", func(t *testing.T) { - // Create a mock client - mockClient := &MockClient{} - - features := featuremgmt.WithFeatures() - // Initialize the search handler with the mock client - searchHandler := SearchHandler{ - log: log.New("test", "test"), - client: mockClient, - tracer: tracing.NewNoopTracerService(), - features: features, - } - - rr := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/search?field=field1", nil) - req.Header.Add("content-type", "application/json") - req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) - - searchHandler.DoSearch(rr, req) - - if mockClient.LastSearchRequest == nil { - t.Fatalf("expected Search to be called, but it was not") - } - expectedFields := []string{"title", "folder", "tags", "description", "manager.kind", "manager.id", "field1"} - if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) { - t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) - } - }) - - t.Run("Passing no fields will search using default dashboard fields", func(t *testing.T) { - // Create a mock client - mockClient := &MockClient{} - - features := featuremgmt.WithFeatures() - // Initialize the search handler with the mock client - searchHandler := SearchHandler{ - log: log.New("test", "test"), - client: mockClient, - tracer: tracing.NewNoopTracerService(), - features: features, - } - - rr := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/search", nil) - req.Header.Add("content-type", "application/json") - req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) - - searchHandler.DoSearch(rr, req) - - if mockClient.LastSearchRequest == nil { - t.Fatalf("expected Search to be called, but it was not") - } - expectedFields := []string{"title", "folder", "tags", "description", "manager.kind", "manager.id"} - if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) { - t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) - } - }) - t.Run("Sort - default sort by resource", func(t *testing.T) { rows := make([]*resourcepb.ResourceTableRow, len(mockResults)) for i, r := range mockResults { @@ -689,6 +604,408 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { }) } +func TestConvertHttpSearchRequestToResourceSearchRequest(t *testing.T) { + testUser := &user.SignedInUser{ + Namespace: "test-namespace", + OrgID: 1, + } + + dashboardKey := &resourcepb.ResourceKey{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Namespace: "test-namespace", + } + folderKey := &resourcepb.ResourceKey{ + Group: "folder.grafana.app", + Resource: "folders", + Namespace: "test-namespace", + } + defaultFields := []string{"title", "folder", "tags", "description", "manager.kind", "manager.id"} + + tests := map[string]struct { + queryString string + sharedDashboards []string + sharedDashboardsError error + expected *resourcepb.ResourceSearchRequest + expectedError error + }{ + "default values with no query params": { + queryString: "", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "custom limit and offset": { + queryString: "limit=100&offset=50", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 100, + Offset: 50, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "pagination with page parameter": { + queryString: "limit=25&page=3", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 25, + Offset: 50, + Page: 3, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "query string and explain": { + queryString: "query=test-query&explain=true", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "test-query", + Limit: 50, + Offset: 0, + Page: 1, + Explain: true, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "additional fields": { + queryString: "field=custom1&field=custom2", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: append(defaultFields, "custom1", "custom2"), + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "view permission": { + queryString: "permission=view", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Permission: int64(dashboardaccess.PERMISSION_VIEW), + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "edit permission": { + queryString: "permission=Edit", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Permission: int64(dashboardaccess.PERMISSION_EDIT), + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "admin permission": { + queryString: "permission=ADMIN", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Permission: int64(dashboardaccess.PERMISSION_ADMIN), + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "type dashboard only": { + queryString: "type=dashboard", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + }, + }, + "type folder only": { + queryString: "type=folder", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: folderKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + }, + }, + "both types should include federated": { + queryString: "type=dashboard&type=folder", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "sort ascending": { + queryString: "sort=title", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + SortBy: []*resourcepb.ResourceSearchRequest_Sort{{Field: "title", Desc: false}}, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "sort descending": { + queryString: "sort=-title", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + SortBy: []*resourcepb.ResourceSearchRequest_Sort{{Field: "title", Desc: true}}, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "facet fields": { + queryString: "facet=tags&facet=folder", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{Key: dashboardKey}, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Facet: map[string]*resourcepb.ResourceSearchRequest_Facet{ + "tags": {Field: "tags", Limit: 50}, + "folder": {Field: "folder", Limit: 50}, + }, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "tag filter": { + queryString: "tag=tag1&tag=tag2", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{{Key: "tags", Operator: "=", Values: []string{"tag1", "tag2"}}}, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "folder filter": { + queryString: "folder=my-folder", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{{Key: "folder", Operator: "=", Values: []string{"my-folder"}}}, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "tag and folder filter together": { + queryString: "tag=tag1&tag=tag2&folder=my-folder", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{ + {Key: "tags", Operator: "=", Values: []string{"tag1", "tag2"}}, + {Key: "folder", Operator: "=", Values: []string{"my-folder"}}, + }, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "root folder should be converted to empty string": { + queryString: "folder=general", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{{Key: "folder", Operator: "=", Values: []string{""}}}, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "shared with me folder with dashboards": { + queryString: "folder=sharedwithme", + sharedDashboards: []string{"dash1", "dash2", "dash3"}, + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{{Key: "name", Operator: "in", Values: []string{"dash1", "dash2", "dash3"}}}, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "shared with me folder without dashboards returns error": { + queryString: "folder=sharedwithme", + sharedDashboards: []string{}, + expectedError: errEmptyResults, + }, + "name filter": { + queryString: "name=name1&name=name2", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{{Key: "name", Operator: "in", Values: []string{"name1", "name2"}}}, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "comprehensive filter with query, tags, folder, and name": { + queryString: "query=search-term&tag=monitoring&tag=prod&folder=my-folder&name=dash1&name=dash2", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{ + {Key: "tags", Operator: "=", Values: []string{"monitoring", "prod"}}, + {Key: "folder", Operator: "=", Values: []string{"my-folder"}}, + {Key: "name", Operator: "in", Values: []string{"dash1", "dash2"}}, + }, + }, + Query: "search-term", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "libraryPanel filter": { + queryString: "libraryPanel=panel1&libraryPanel=panel2", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{{Key: "reference.LibraryPanel", Operator: "=", Values: []string{"panel1", "panel2"}}}, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + "libraryPanel and tag filter together": { + queryString: "libraryPanel=panel1&tag=monitoring&tag=prod", + expected: &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: dashboardKey, + Fields: []*resourcepb.Requirement{ + {Key: "tags", Operator: "=", Values: []string{"monitoring", "prod"}}, + {Key: "reference.LibraryPanel", Operator: "=", Values: []string{"panel1"}}, + }, + }, + Query: "", + Limit: 50, + Offset: 0, + Page: 1, + Explain: false, + Fields: defaultFields, + Federated: []*resourcepb.ResourceKey{folderKey}, + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + queryParams, err := url.ParseQuery(tt.queryString) + require.NoError(t, err) + + getDashboardsFunc := func() ([]string, error) { + if tt.sharedDashboardsError != nil { + return nil, tt.sharedDashboardsError + } + return tt.sharedDashboards, nil + } + + result, err := convertHttpSearchRequestToResourceSearchRequest(queryParams, testUser, getDashboardsFunc) + + if tt.expectedError != nil { + assert.ErrorIs(t, err, tt.expectedError) + return + } + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, tt.expected, result) + }) + } +} + // MockClient implements the ResourceIndexClient interface for testing type MockClient struct { resourcepb.ResourceIndexClient From 4d25381716585a2a9b2d6bfa8f904bef22bf9421 Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Thu, 20 Nov 2025 13:51:59 -0300 Subject: [PATCH 032/378] DashboardLibrary: Template dashboards public preview (#114230) --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 12 +- pkg/services/featuremgmt/registry.go | 18 +- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.go | 12 +- pkg/services/featuremgmt/toggles_gen.json | 260 ++++++++++-------- 6 files changed, 172 insertions(+), 133 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 6b76c32fd28..a4eca4f9ee7 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -90,6 +90,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage. Default is enabled. | | `sqlExpressions` | Enables SQL Expressions, which can execute SQL queries against data source results. | | `queryLibrary` | Enables Saved queries (query library) feature | +| `dashboardTemplates` | Enables a flow to get started with a new dashboard from a template | | `enableSCIM` | Enables SCIM support for user and group management | | `alertRuleRestore` | Enables the alert rule restore feature | | `azureMonitorLogsBuilderEditor` | Enables the logs builder mode for the Azure Monitor data source | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index acb3b1796de..a7d2378f765 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -502,14 +502,18 @@ export interface FeatureToggles { */ queryLibrary?: boolean; /** - * Enable dashboard library experiments that are production ready + * Displays datasource provisioned dashboards in dashboard empty page, only when coming from datasource configuration page */ dashboardLibrary?: boolean; /** - * Enable suggested dashboards when creating new dashboards + * Displays datasource provisioned and community dashboards in dashboard empty page, only when coming from datasource configuration page */ suggestedDashboards?: boolean; /** + * Enables a flow to get started with a new dashboard from a template + */ + dashboardTemplates?: boolean; + /** * Sets the logs table as default visualisation in logs explore */ logsExploreTableDefaultVisualization?: boolean; @@ -1157,10 +1161,6 @@ export interface FeatureToggles { */ panelTimeSettings?: boolean; /** - * Enable template dashboards - */ - dashboardTemplates?: boolean; - /** * Enables app platform API for annotations * @default false */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index e6587428f18..c56f46a7193 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -825,18 +825,25 @@ var ( }, { Name: "dashboardLibrary", - Description: "Enable dashboard library experiments that are production ready", + Description: "Displays datasource provisioned dashboards in dashboard empty page, only when coming from datasource configuration page", Stage: FeatureStageExperimental, Owner: grafanaSharingSquad, FrontendOnly: false, }, { Name: "suggestedDashboards", - Description: "Enable suggested dashboards when creating new dashboards", + Description: "Displays datasource provisioned and community dashboards in dashboard empty page, only when coming from datasource configuration page", Stage: FeatureStageExperimental, Owner: grafanaSharingSquad, FrontendOnly: false, }, + { + Name: "dashboardTemplates", + Description: "Enables a flow to get started with a new dashboard from a template", + Stage: FeatureStagePublicPreview, + Owner: grafanaSharingSquad, + FrontendOnly: false, + }, { Name: "logsExploreTableDefaultVisualization", Description: "Sets the logs table as default visualisation in logs explore", @@ -1904,13 +1911,6 @@ var ( RequiresRestart: false, HideFromDocs: false, }, - { - Name: "dashboardTemplates", - Description: "Enable template dashboards", - Stage: FeatureStageExperimental, - Owner: grafanaSharingSquad, - FrontendOnly: false, - }, { Name: "kubernetesAnnotations", Description: "Enables app platform API for annotations", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 24f9769ea4f..9465d6992c3 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -115,6 +115,7 @@ grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,fa queryLibrary,preview,@grafana/sharing-squad,false,false,false dashboardLibrary,experimental,@grafana/sharing-squad,false,false,false suggestedDashboards,experimental,@grafana/sharing-squad,false,false,false +dashboardTemplates,preview,@grafana/sharing-squad,false,false,false logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false @@ -258,7 +259,6 @@ pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,f newPanelPadding,experimental,@grafana/dashboards-squad,false,false,false onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false -dashboardTemplates,experimental,@grafana/sharing-squad,false,false,false kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false,false,false awsDatasourcesHttpProxy,experimental,@grafana/aws-datasources,false,false,false transformationsEmptyPlaceholder,preview,@grafana/datapro,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index fdc48861894..059ed11e535 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -348,13 +348,17 @@ const ( FlagQueryLibrary = "queryLibrary" // FlagDashboardLibrary - // Enable dashboard library experiments that are production ready + // Displays datasource provisioned dashboards in dashboard empty page, only when coming from datasource configuration page FlagDashboardLibrary = "dashboardLibrary" // FlagSuggestedDashboards - // Enable suggested dashboards when creating new dashboards + // Displays datasource provisioned and community dashboards in dashboard empty page, only when coming from datasource configuration page FlagSuggestedDashboards = "suggestedDashboards" + // FlagDashboardTemplates + // Enables a flow to get started with a new dashboard from a template + FlagDashboardTemplates = "dashboardTemplates" + // FlagAlertingDisableSendAlertsExternal // Disables the ability to send alerts to an external Alertmanager datasource. FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal" @@ -750,10 +754,6 @@ const ( // Enables a new panel time settings drawer FlagPanelTimeSettings = "panelTimeSettings" - // FlagDashboardTemplates - // Enable template dashboards - FlagDashboardTemplates = "dashboardTemplates" - // FlagKubernetesAnnotations // Enables app platform API for annotations FlagKubernetesAnnotations = "kubernetesAnnotations" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 48eba662c0a..2298b91d854 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -8,7 +8,7 @@ "name": "addFieldFromCalculationStatFunctions", "resourceVersion": "1762442825881", "creationTimestamp": "2023-11-03T14:39:58Z", - "deletionTimestamp": "2025-11-12T14:07:50Z", + "deletionTimestamp": "2025-11-17T15:58:43Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" } @@ -26,7 +26,7 @@ "name": "adhocFiltersInTooltips", "resourceVersion": "1756814786992", "creationTimestamp": "2025-07-29T17:53:43Z", - "deletionTimestamp": "2025-11-11T09:49:48Z", + "deletionTimestamp": "2025-11-12T10:05:30Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-02 12:06:26.992384 +0000 UTC" } @@ -70,7 +70,8 @@ "metadata": { "name": "alertEnrichmentConditional", "resourceVersion": "1757418974334", - "creationTimestamp": "2025-09-09T11:56:14Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enable conditional alert enrichment steps.", @@ -84,7 +85,8 @@ "metadata": { "name": "alertEnrichmentMultiStep", "resourceVersion": "1757418224384", - "creationTimestamp": "2025-09-09T11:43:44Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Allow multiple steps per enrichment.", @@ -249,7 +251,8 @@ "metadata": { "name": "alertingEnrichmentAssistantInvestigations", "resourceVersion": "1757606567075", - "creationTimestamp": "2025-09-11T16:02:47Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Enable Assistant Investigations enrichment type.", @@ -278,7 +281,8 @@ "metadata": { "name": "alertingEnrichmentPerRule", "resourceVersion": "1756206837948", - "creationTimestamp": "2025-08-28T08:30:28Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enable enrichment per rule in the alerting UI.", @@ -319,7 +323,8 @@ "metadata": { "name": "alertingImportAlertmanagerUI", "resourceVersion": "1754585847887", - "creationTimestamp": "2025-08-13T15:28:43Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables the UI to see imported Alertmanager configuration", @@ -572,7 +577,8 @@ "metadata": { "name": "alertingTriage", "resourceVersion": "1763541314825", - "creationTimestamp": "2025-09-01T09:33:33Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" } @@ -603,7 +609,7 @@ "metadata": { "name": "alertingUIUseBackendFilters", "resourceVersion": "1762966218072", - "creationTimestamp": "2025-11-12T16:50:18Z" + "creationTimestamp": "2025-11-13T14:52:14Z" }, "spec": { "description": "Enables the UI to use certain backend-side filters", @@ -616,8 +622,8 @@ "metadata": { "name": "alertingUseNewSimplifiedRoutingHashAlgorithm", "resourceVersion": "1759339813575", - "creationTimestamp": "2025-10-01T17:28:42Z", - "deletionTimestamp": "2025-10-01T17:29:29Z", + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z", "annotations": { "grafana.app/updatedTimestamp": "2025-10-01 17:30:13.575464 +0000 UTC" } @@ -759,7 +765,7 @@ "metadata": { "name": "awsDatasourcesHttpProxy", "resourceVersion": "1762964349996", - "creationTimestamp": "2025-11-12T16:15:47Z", + "creationTimestamp": "2025-11-12T18:51:23Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-12 16:19:09.996919 +0000 UTC" } @@ -840,7 +846,8 @@ "metadata": { "name": "azureResourcePickerUpdates", "resourceVersion": "1754910058337", - "creationTimestamp": "2025-09-02T10:02:01Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables the updated Azure Monitor resource picker", @@ -893,7 +900,8 @@ "metadata": { "name": "cdnPluginsLoadFirst", "resourceVersion": "1758882341746", - "creationTimestamp": "2025-09-26T10:25:41Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Prioritize loading plugins from the CDN before other sources", @@ -920,7 +928,8 @@ "metadata": { "name": "cdnPluginsUrls", "resourceVersion": "1759489886228", - "creationTimestamp": "2025-10-03T11:11:26Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Enable loading plugins via declarative URLs", @@ -1013,7 +1022,7 @@ "name": "correlations", "resourceVersion": "1762442825881", "creationTimestamp": "2022-09-16T13:14:27Z", - "deletionTimestamp": "2025-11-12T13:11:31Z", + "deletionTimestamp": "2025-11-13T09:21:46Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" } @@ -1067,7 +1076,7 @@ "name": "dashboardDsAdHocFiltering", "resourceVersion": "1756814786992", "creationTimestamp": "2025-07-23T08:12:25Z", - "deletionTimestamp": "2025-09-27T19:59:33Z", + "deletionTimestamp": "2025-11-10T17:17:49Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-02 12:06:26.992384 +0000 UTC" } @@ -1096,14 +1105,15 @@ { "metadata": { "name": "dashboardLibrary", - "resourceVersion": "1762521182817", - "creationTimestamp": "2025-09-26T16:02:12Z", + "resourceVersion": "1763643877862", + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z", "annotations": { - "grafana.app/updatedTimestamp": "2025-11-07 13:13:02.817210943 +0000 UTC" + "grafana.app/updatedTimestamp": "2025-11-20 13:04:37.862907 +0000 UTC" } }, "spec": { - "description": "Enable dashboard library experiments that are production ready", + "description": "Displays datasource provisioned dashboards in dashboard empty page, only when coming from datasource configuration page", "stage": "experimental", "codeowner": "@grafana/sharing-squad" } @@ -1180,12 +1190,15 @@ { "metadata": { "name": "dashboardTemplates", - "resourceVersion": "1761575312733", - "creationTimestamp": "2025-10-27T14:28:32Z" + "resourceVersion": "1763643877862", + "creationTimestamp": "2025-10-28T20:05:32Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-20 13:04:37.862907 +0000 UTC" + } }, "spec": { - "description": "Enable template dashboards", - "stage": "experimental", + "description": "Enables a flow to get started with a new dashboard from a template", + "stage": "preview", "codeowner": "@grafana/sharing-squad" } }, @@ -1193,7 +1206,8 @@ "metadata": { "name": "dashboardUndoRedo", "resourceVersion": "1757940426210", - "creationTimestamp": "2025-09-15T12:47:06Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Enables undo/redo in dynamic dashboards", @@ -1341,8 +1355,8 @@ "metadata": { "name": "dskitBackgroundServices", "resourceVersion": "1757339637779", - "creationTimestamp": "2025-09-03T12:20:24Z", - "deletionTimestamp": "2025-09-17T12:19:32Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-08 13:53:57.77994 +0000 UTC" } @@ -1361,7 +1375,7 @@ "name": "editPanelCSVDragAndDrop", "resourceVersion": "1762783224740", "creationTimestamp": "2023-01-24T09:43:44Z", - "deletionTimestamp": "2025-11-10T16:31:10Z", + "deletionTimestamp": "2025-11-12T14:47:44Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-10 14:00:24.740459 +0000 UTC" } @@ -1420,7 +1434,7 @@ "metadata": { "name": "enableDashboardEmptyExtensions", "resourceVersion": "1759194774156", - "creationTimestamp": "2025-09-30T01:12:54Z" + "creationTimestamp": "2025-10-13T07:03:13Z" }, "spec": { "description": "Set this to true to enable all dashboard empty state extensions registered by plugins.", @@ -1477,7 +1491,7 @@ "name": "enablePluginImporter", "resourceVersion": "1753448760331", "creationTimestamp": "2025-07-16T04:42:28Z", - "deletionTimestamp": "2025-10-20T05:42:33Z" + "deletionTimestamp": "2025-10-23T04:18:23Z" }, "spec": { "description": "Set this to true to use the new PluginImporter functionality", @@ -1570,7 +1584,7 @@ "name": "expressionParser", "resourceVersion": "1753448760331", "creationTimestamp": "2024-02-17T00:59:11Z", - "deletionTimestamp": "2025-08-26T13:21:24Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Enable new expression parser", @@ -1584,7 +1598,7 @@ "name": "extensionSidebar", "resourceVersion": "1753448760331", "creationTimestamp": "2025-04-03T10:16:35Z", - "deletionTimestamp": "2025-09-01T10:14:17Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Enables the extension sidebar", @@ -1624,7 +1638,7 @@ "name": "extractFieldsNameDeduplication", "resourceVersion": "1762442825881", "creationTimestamp": "2023-11-02T15:47:42Z", - "deletionTimestamp": "2025-11-11T16:00:57Z", + "deletionTimestamp": "2025-11-12T10:08:13Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" } @@ -1653,7 +1667,8 @@ "metadata": { "name": "favoriteDatasources", "resourceVersion": "1754648387873", - "creationTimestamp": "2025-08-08T13:28:17Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enable favorite datasources", @@ -1680,7 +1695,7 @@ "name": "featureToggleAdminPage", "resourceVersion": "1758022099771", "creationTimestamp": "2023-07-18T20:43:32Z", - "deletionTimestamp": "2025-09-29T13:36:16Z", + "deletionTimestamp": "2025-08-29T14:46:39Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-16 11:28:19.771156 +0000 UTC" } @@ -1724,8 +1739,8 @@ "metadata": { "name": "filterOutBotsFromFrontendLogs", "resourceVersion": "1758000919535", - "creationTimestamp": "2025-09-16T05:35:19Z", - "deletionTimestamp": "2025-10-13T11:09:22Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Filter out bots from collecting data for Frontend Observability", @@ -1755,7 +1770,7 @@ "name": "formatString", "resourceVersion": "1762442825881", "creationTimestamp": "2023-10-13T18:17:12Z", - "deletionTimestamp": "2025-11-12T00:00:00Z", + "deletionTimestamp": "2025-11-17T13:06:30Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" } @@ -1812,7 +1827,8 @@ "metadata": { "name": "grafanaAssistantInProfilesDrilldown", "resourceVersion": "1754572610001", - "creationTimestamp": "2025-08-19T07:54:00Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-08-07 13:16:50.001205 +0000 UTC" } @@ -1857,8 +1873,8 @@ "metadata": { "name": "grafanaPathfinder", "resourceVersion": "1760434668782", - "creationTimestamp": "2025-08-20T12:30:29Z", - "deletionTimestamp": "2025-10-21T16:10:16Z", + "creationTimestamp": "2025-10-14T10:40:40Z", + "deletionTimestamp": "2025-10-22T08:06:21Z", "annotations": { "grafana.app/updatedTimestamp": "2025-10-14 09:37:48.782577 +0000 UTC" } @@ -1888,7 +1904,8 @@ "metadata": { "name": "graphiteBackendMode", "resourceVersion": "1755870507537", - "creationTimestamp": "2025-09-01T15:13:47Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables the Graphite data source full backend mode", @@ -1928,7 +1945,7 @@ "name": "groupToNestedTableTransformation", "resourceVersion": "1762442825881", "creationTimestamp": "2024-02-07T14:28:26Z", - "deletionTimestamp": "2025-11-12T14:33:44Z", + "deletionTimestamp": "2025-11-17T11:57:22Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" } @@ -2046,7 +2063,7 @@ "metadata": { "name": "interactiveLearning", "resourceVersion": "1761063016739", - "creationTimestamp": "2025-10-21T16:10:16Z" + "creationTimestamp": "2025-10-22T08:06:21Z" }, "spec": { "description": "Enables the interactive learning app", @@ -2072,7 +2089,7 @@ "name": "inviteUserExperimental", "resourceVersion": "1753448760331", "creationTimestamp": "2025-03-07T19:09:59Z", - "deletionTimestamp": "2025-11-14T10:29:28Z" + "deletionTimestamp": "2025-11-14T15:33:26Z" }, "spec": { "description": "Renders invite user button along the app", @@ -2086,7 +2103,7 @@ "metadata": { "name": "jaegerEnableGrpcEndpoint", "resourceVersion": "1760451551713", - "creationTimestamp": "2025-10-14T14:19:11Z" + "creationTimestamp": "2025-10-31T18:19:16Z" }, "spec": { "description": "Enable querying trace data through Jaeger's gRPC endpoint (HTTP)", @@ -2164,7 +2181,8 @@ "metadata": { "name": "kubernetesAlertingRules", "resourceVersion": "1754340669702", - "creationTimestamp": "2025-08-04T20:51:09Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Adds support for Kubernetes alerting and recording rules", @@ -2177,7 +2195,7 @@ "metadata": { "name": "kubernetesAnnotations", "resourceVersion": "1761142826172", - "creationTimestamp": "2025-10-22T14:20:26Z" + "creationTimestamp": "2025-11-06T18:22:20Z" }, "spec": { "description": "Enables app platform API for annotations", @@ -2190,7 +2208,8 @@ "metadata": { "name": "kubernetesAuthZHandlerRedirect", "resourceVersion": "1758820248165", - "creationTimestamp": "2025-09-25T17:10:48Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Redirects the traffic from the legacy access control endpoints to the new K8s AuthZ endpoints", @@ -2246,7 +2265,8 @@ "metadata": { "name": "kubernetesAuthzResourcePermissionApis", "resourceVersion": "1754668670559", - "creationTimestamp": "2025-08-11T08:54:36Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Registers AuthZ resource permission /apis endpoints", @@ -2259,7 +2279,7 @@ "metadata": { "name": "kubernetesAuthzZanzanaSync", "resourceVersion": "1758887751768", - "creationTimestamp": "2025-09-26T09:35:02Z", + "creationTimestamp": "2025-10-13T19:37:13Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-26 11:55:51.768754 +0000 UTC" } @@ -2275,7 +2295,8 @@ "metadata": { "name": "kubernetesCorrelations", "resourceVersion": "1757513374180", - "creationTimestamp": "2025-09-10T14:09:34Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Adds support for Kubernetes correlations", @@ -2330,7 +2351,7 @@ "metadata": { "name": "kubernetesLogsDrilldown", "resourceVersion": "1760632282014", - "creationTimestamp": "2025-10-07T19:26:08Z", + "creationTimestamp": "2025-10-16T21:31:42Z", "annotations": { "grafana.app/updatedTimestamp": "2025-10-16 16:31:22.014483 +0000 UTC" } @@ -2346,7 +2367,7 @@ "metadata": { "name": "kubernetesQueryCaching", "resourceVersion": "1760972620939", - "creationTimestamp": "2025-10-20T15:03:40Z" + "creationTimestamp": "2025-10-20T16:11:25Z" }, "spec": { "description": "Adds support for Kubernetes querycaching", @@ -2359,7 +2380,8 @@ "metadata": { "name": "kubernetesShortURLs", "resourceVersion": "1756914263808", - "creationTimestamp": "2025-08-04T12:12:12Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-03 15:44:23.80856 +0000 UTC" } @@ -2388,7 +2410,8 @@ "metadata": { "name": "kubernetesStars", "resourceVersion": "1759149842036", - "creationTimestamp": "2025-09-29T12:44:02Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Routes stars requests from /api to the /apis endpoint", @@ -2428,7 +2451,7 @@ "name": "localizationForPlugins", "resourceVersion": "1753448760331", "creationTimestamp": "2025-03-31T04:38:38Z", - "deletionTimestamp": "2025-09-29T07:10:59Z" + "deletionTimestamp": "2025-08-29T14:46:39Z" }, "spec": { "description": "Enables localization for plugins", @@ -2665,7 +2688,7 @@ "name": "multiTenantFrontend", "resourceVersion": "1753448760331", "creationTimestamp": "2025-04-25T09:24:25Z", - "deletionTimestamp": "2025-09-08T17:06:39Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Register MT frontend", @@ -2702,7 +2725,8 @@ "metadata": { "name": "newClickhouseConfigPageDesign", "resourceVersion": "1754075145003", - "creationTimestamp": "2025-08-05T13:37:28Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables new design for the Clickhouse data source configuration page", @@ -2716,7 +2740,7 @@ "name": "newDashboardSharingComponent", "resourceVersion": "1753448760331", "creationTimestamp": "2024-05-03T15:02:18Z", - "deletionTimestamp": "2025-09-12T17:27:39Z" + "deletionTimestamp": "2025-08-29T14:46:39Z" }, "spec": { "description": "Enables the new sharing drawer design", @@ -2756,7 +2780,7 @@ "metadata": { "name": "newGauge", "resourceVersion": "1760700645318", - "creationTimestamp": "2025-10-17T11:30:45Z" + "creationTimestamp": "2025-10-20T16:33:19Z" }, "spec": { "description": "Enable new gauge visualization", @@ -2783,7 +2807,7 @@ "metadata": { "name": "newLogContext", "resourceVersion": "1754044501326", - "creationTimestamp": "2025-08-01T11:30:17Z" + "creationTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "New Log Context component", @@ -2827,7 +2851,7 @@ "metadata": { "name": "newPanelPadding", "resourceVersion": "1760780310038", - "creationTimestamp": "2025-10-18T09:38:30Z" + "creationTimestamp": "2025-11-12T15:40:46Z" }, "spec": { "description": "Increases panel padding globally", @@ -2853,7 +2877,7 @@ "metadata": { "name": "newVizSuggestions", "resourceVersion": "1762456851857", - "creationTimestamp": "2025-11-06T19:20:51Z" + "creationTimestamp": "2025-11-12T19:26:29Z" }, "spec": { "description": "Enable new visualization suggestions", @@ -2893,7 +2917,7 @@ "metadata": { "name": "onlyStoreActionSets", "resourceVersion": "1759844046154", - "creationTimestamp": "2025-10-07T13:24:26Z", + "creationTimestamp": "2025-10-20T15:02:56Z", "annotations": { "grafana.app/updatedTimestamp": "2025-10-07 13:34:06.15476 +0000 UTC" } @@ -2938,7 +2962,7 @@ "name": "panelMonitoring", "resourceVersion": "1753448760331", "creationTimestamp": "2023-10-09T05:19:08Z", - "deletionTimestamp": "2025-11-06T15:46:51Z" + "deletionTimestamp": "2025-11-07T19:04:42Z" }, "spec": { "description": "Enables panel monitoring through logs and measurements", @@ -2966,7 +2990,7 @@ "metadata": { "name": "panelTimeSettings", "resourceVersion": "1761555646368", - "creationTimestamp": "2025-10-27T09:00:46Z" + "creationTimestamp": "2025-10-29T08:06:23Z" }, "spec": { "description": "Enables a new panel time settings drawer", @@ -3028,7 +3052,7 @@ "name": "pinNavItems", "resourceVersion": "1762958248290", "creationTimestamp": "2024-06-10T11:40:03Z", - "deletionTimestamp": "2025-11-13T14:52:10Z", + "deletionTimestamp": "2025-11-17T12:12:47Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" } @@ -3058,7 +3082,7 @@ "name": "pluginAssetProvider", "resourceVersion": "1753448760331", "creationTimestamp": "2025-07-17T15:20:35Z", - "deletionTimestamp": "2025-10-03T16:13:14Z" + "deletionTimestamp": "2025-10-10T09:35:22Z" }, "spec": { "description": "Allows decoupled core plugins to load from the Grafana CDN", @@ -3073,7 +3097,8 @@ "metadata": { "name": "pluginContainers", "resourceVersion": "1756911074581", - "creationTimestamp": "2025-09-03T14:51:14Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables running plugins in containers", @@ -3087,7 +3112,7 @@ "metadata": { "name": "pluginInstallAPISync", "resourceVersion": "1760543624249", - "creationTimestamp": "2025-10-15T15:53:44Z" + "creationTimestamp": "2025-10-24T12:09:26Z" }, "spec": { "description": "Enable syncing plugin installations to the installs API", @@ -3113,7 +3138,7 @@ "metadata": { "name": "pluginStoreServiceLoading", "resourceVersion": "1761144346944", - "creationTimestamp": "2025-10-17T14:52:48Z", + "creationTimestamp": "2025-10-17T20:01:43Z", "annotations": { "grafana.app/updatedTimestamp": "2025-10-22 14:45:46.944669 +0000 UTC" } @@ -3142,7 +3167,7 @@ "name": "pluginsFrontendSandbox", "resourceVersion": "1753448760331", "creationTimestamp": "2023-06-05T08:51:36Z", - "deletionTimestamp": "2025-11-05T10:06:22Z" + "deletionTimestamp": "2025-11-06T10:06:53Z" }, "spec": { "description": "Enables the plugins frontend sandbox", @@ -3155,7 +3180,7 @@ "name": "pluginsSkipHostEnvVars", "resourceVersion": "1753448760331", "creationTimestamp": "2023-11-15T17:09:14Z", - "deletionTimestamp": "2025-11-04T16:51:13Z" + "deletionTimestamp": "2025-11-13T15:31:57Z" }, "spec": { "description": "Disables passing host environment variable to plugin processes", @@ -3207,7 +3232,7 @@ "name": "preinstallAutoUpdate", "resourceVersion": "1753448760331", "creationTimestamp": "2024-11-07T12:14:25Z", - "deletionTimestamp": "2025-11-07T10:10:01Z" + "deletionTimestamp": "2025-11-10T14:06:30Z" }, "spec": { "description": "Enables automatic updates for pre-installed plugins", @@ -3233,7 +3258,7 @@ "metadata": { "name": "preventPanelChromeOverflow", "resourceVersion": "1760704390127", - "creationTimestamp": "2025-10-17T12:33:10Z" + "creationTimestamp": "2025-10-17T14:40:08Z" }, "spec": { "description": "Restrict PanelChrome contents with overflow: hidden;", @@ -3248,7 +3273,7 @@ "name": "promQLScope", "resourceVersion": "1753448760331", "creationTimestamp": "2024-01-29T20:22:17Z", - "deletionTimestamp": "2025-10-05T00:24:17Z" + "deletionTimestamp": "2025-10-10T14:53:18Z" }, "spec": { "description": "In-development feature that will allow injection of labels into prometheus queries.", @@ -3277,7 +3302,7 @@ "name": "prometheusCodeModeMetricNamesSearch", "resourceVersion": "1753448760331", "creationTimestamp": "2024-04-04T20:38:23Z", - "deletionTimestamp": "2025-08-27T13:11:58Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names", @@ -3303,7 +3328,8 @@ "metadata": { "name": "prometheusTypeMigration", "resourceVersion": "1757089774247", - "creationTimestamp": "2025-08-25T21:53:16Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-05 16:29:34.247055837 +0000 UTC" } @@ -3360,7 +3386,8 @@ "metadata": { "name": "queryCacheRequestDeduplication", "resourceVersion": "1757521912495", - "creationTimestamp": "2025-09-10T16:31:52Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Enable request deduplication when query caching is enabled. Requests issuing the same query will be deduplicated, only the first request to arrive will be executed and the response will be shared with requests arriving while there is a request in-flight", @@ -3456,7 +3483,7 @@ "name": "recordedQueriesMulti", "resourceVersion": "1753448760331", "creationTimestamp": "2023-06-14T12:34:22Z", - "deletionTimestamp": "2025-11-07T17:31:39Z" + "deletionTimestamp": "2025-11-10T20:31:43Z" }, "spec": { "description": "Enables writing multiple items from a single query within Recorded Queries", @@ -3579,7 +3606,8 @@ "metadata": { "name": "restrictedPluginApis", "resourceVersion": "1753776783657", - "creationTimestamp": "2025-09-01T09:57:00Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-07-29 08:13:03.657209 +0000 UTC" } @@ -3623,8 +3651,8 @@ "metadata": { "name": "savedQueries", "resourceVersion": "1756920131554", - "creationTimestamp": "2025-08-25T21:22:09Z", - "deletionTimestamp": "2025-09-18T12:07:31Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-03 17:22:11.554759 +0000 UTC" } @@ -3705,7 +3733,8 @@ "metadata": { "name": "secretsManagementAppPlatformUI", "resourceVersion": "1756816818369", - "creationTimestamp": "2025-09-02T12:40:18Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enable the secrets management app platform UI", @@ -3747,7 +3776,7 @@ "name": "skipTokenRotationIfRecent", "resourceVersion": "1753448760331", "creationTimestamp": "2025-06-03T06:59:40Z", - "deletionTimestamp": "2025-10-22T10:29:12Z" + "deletionTimestamp": "2025-10-23T08:02:41Z" }, "spec": { "description": "Skip token rotation if it was already rotated less than 5 seconds ago", @@ -3762,7 +3791,7 @@ "name": "sqlDatasourceDatabaseSelection", "resourceVersion": "1753448760331", "creationTimestamp": "2023-06-06T16:28:52Z", - "deletionTimestamp": "2025-08-12T13:22:30Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Enables previous SQL data source dataset dropdown behavior", @@ -3832,7 +3861,8 @@ "metadata": { "name": "starsFromAPIServer", "resourceVersion": "1762958248290", - "creationTimestamp": "2025-09-19T10:00:55Z", + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" } @@ -3860,11 +3890,14 @@ { "metadata": { "name": "suggestedDashboards", - "resourceVersion": "1762521182817", - "creationTimestamp": "2025-11-07T13:13:02Z" + "resourceVersion": "1763643877862", + "creationTimestamp": "2025-11-07T13:38:59Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-20 13:04:37.862907 +0000 UTC" + } }, "spec": { - "description": "Enable suggested dashboards when creating new dashboards", + "description": "Displays datasource provisioned and community dashboards in dashboard empty page, only when coming from datasource configuration page", "stage": "experimental", "codeowner": "@grafana/sharing-squad" } @@ -3874,7 +3907,7 @@ "name": "tableNextGen", "resourceVersion": "1753448760331", "creationTimestamp": "2025-03-26T03:57:57Z", - "deletionTimestamp": "2025-08-26T21:25:16Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Allows access to the new react-data-grid based table component.", @@ -3913,7 +3946,8 @@ "metadata": { "name": "teamFolders", "resourceVersion": "1755099058649", - "creationTimestamp": "2025-08-13T16:41:00Z" + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables team folders functionality", @@ -3927,7 +3961,7 @@ "name": "teamHttpHeadersMimir", "resourceVersion": "1753448760331", "creationTimestamp": "2025-01-13T10:42:47Z", - "deletionTimestamp": "2025-08-07T09:04:46Z" + "deletionTimestamp": "2025-07-31T22:56:50Z" }, "spec": { "description": "Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams", @@ -3967,7 +4001,7 @@ "name": "templateVariablesUsesCombobox", "resourceVersion": "1753448760331", "creationTimestamp": "2025-01-31T09:53:13Z", - "deletionTimestamp": "2025-11-12T14:40:39Z" + "deletionTimestamp": "2025-11-13T03:31:18Z" }, "spec": { "description": "Use new **Combobox** component for template variables", @@ -3995,7 +4029,8 @@ "metadata": { "name": "tempoSearchBackendMigration", "resourceVersion": "1758029567165", - "creationTimestamp": "2025-09-16T11:18:16Z", + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-16 13:32:47.165146 +0000 UTC" } @@ -4025,7 +4060,7 @@ "metadata": { "name": "timeRangePan", "resourceVersion": "1762290731154", - "creationTimestamp": "2025-10-24T19:49:53Z", + "creationTimestamp": "2025-11-05T01:39:46Z", "deletionTimestamp": "2025-11-06T17:39:31Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-04 21:12:11.154822 +0000 UTC" @@ -4055,7 +4090,7 @@ "name": "tlsMemcached", "resourceVersion": "1753448760331", "creationTimestamp": "2024-05-09T19:12:08Z", - "deletionTimestamp": "2025-11-11T13:50:51Z" + "deletionTimestamp": "2025-11-12T15:49:28Z" }, "spec": { "description": "Use TLS-enabled memcached in the enterprise caching feature", @@ -4068,7 +4103,7 @@ "metadata": { "name": "transformationsEmptyPlaceholder", "resourceVersion": "1763373021129", - "creationTimestamp": "2025-11-11T13:14:25Z", + "creationTimestamp": "2025-11-17T13:57:05Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-17 09:50:21.129721 +0000 UTC" } @@ -4099,7 +4134,7 @@ "metadata": { "name": "ttlPluginInstanceManager", "resourceVersion": "1763462850634", - "creationTimestamp": "2025-11-18T10:47:30Z" + "creationTimestamp": "2025-11-18T11:17:23Z" }, "spec": { "description": "Enable TTL plugin instance manager", @@ -4181,7 +4216,7 @@ "name": "unifiedStorageHistoryPruner", "resourceVersion": "1753448760331", "creationTimestamp": "2025-03-17T10:36:38Z", - "deletionTimestamp": "2025-11-17T12:35:33Z" + "deletionTimestamp": "2025-11-17T19:47:37Z" }, "spec": { "description": "Enables the unified storage history pruner", @@ -4208,8 +4243,8 @@ "metadata": { "name": "unifiedStorageSearchAfterWriteExperimentalAPI", "resourceVersion": "1755089543487", - "creationTimestamp": "2025-08-13T14:05:15Z", - "deletionTimestamp": "2025-09-10T09:52:56Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-08-13 12:52:23.487521 +0000 UTC" } @@ -4266,8 +4301,8 @@ "metadata": { "name": "unifiedStorageUseFullNgram", "resourceVersion": "1758820248165", - "creationTimestamp": "2025-09-25T17:10:48Z", - "deletionTimestamp": "2025-10-22T12:24:39Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Use full n-gram indexing instead of edge n-gram for unified storage search", @@ -4280,7 +4315,7 @@ "metadata": { "name": "unlimitedLayoutsNesting", "resourceVersion": "1760013838902", - "creationTimestamp": "2025-10-09T12:43:58Z" + "creationTimestamp": "2025-10-10T12:15:54Z" }, "spec": { "description": "Enables unlimited dashboard panel grouping", @@ -4293,7 +4328,8 @@ "metadata": { "name": "useKubernetesShortURLsAPI", "resourceVersion": "1756914263808", - "creationTimestamp": "2025-09-03T10:49:07Z", + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-03 15:44:23.80856 +0000 UTC" } @@ -4309,7 +4345,8 @@ "metadata": { "name": "useMultipleScopeNodesEndpoint", "resourceVersion": "1759237515008", - "creationTimestamp": "2025-09-30T13:05:15Z" + "creationTimestamp": "2025-08-29T14:46:39Z", + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Makes the frontend use the 'names' param for fetching multiple scope nodes at once", @@ -4369,7 +4406,8 @@ "metadata": { "name": "vizActionsAuth", "resourceVersion": "1756904995830", - "creationTimestamp": "2025-08-08T18:59:18Z", + "creationTimestamp": "2025-07-31T22:56:50Z", + "deletionTimestamp": "2025-08-01T11:30:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-03 13:09:55.830412 +0000 UTC" } @@ -4399,7 +4437,7 @@ "metadata": { "name": "zanzanaNoLegacyClient", "resourceVersion": "1761063016739", - "creationTimestamp": "2025-10-20T09:52:55Z", + "creationTimestamp": "2025-10-21T14:03:17Z", "annotations": { "grafana.app/updatedTimestamp": "2025-10-21 16:10:16.739546 +0000 UTC" } From 5ca3c27e4f89d3ad6e25ae249a94fd151d238e16 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Thu, 20 Nov 2025 14:17:53 -0300 Subject: [PATCH 033/378] Chore: Fix playlist flaky test (#114241) --- .../playlist/PlaylistNewPage.test.tsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/public/app/features/playlist/PlaylistNewPage.test.tsx b/public/app/features/playlist/PlaylistNewPage.test.tsx index 22a6bfb0117..fe33fada83c 100644 --- a/public/app/features/playlist/PlaylistNewPage.test.tsx +++ b/public/app/features/playlist/PlaylistNewPage.test.tsx @@ -24,7 +24,21 @@ jest.mock('app/core/components/TagFilter/TagFilter', () => ({ function getTestContext() { jest.clearAllMocks(); - const backendSrvMock = jest.spyOn(backendSrv, 'fetch').mockImplementation(() => of(createFetchResponse({}))); + + // Create separate spies for different HTTP methods + const postSpy = jest.fn(); + const otherSpy = jest.fn(); + + const backendSrvMock = jest.spyOn(backendSrv, 'fetch').mockImplementation((options) => { + if (options.method === 'POST') { + postSpy(options); + return of(createFetchResponse({})); + } + // Handle GET and other methods + otherSpy(options); + return of(createFetchResponse({ items: [] })); + }); + jest.spyOn(backendSrv, 'search').mockResolvedValue([]); const { rerender } = render( @@ -33,7 +47,7 @@ function getTestContext() { ); - return { rerender, backendSrvMock }; + return { rerender, backendSrvMock, postSpy, otherSpy }; } describe('PlaylistNewPage', () => { @@ -47,15 +61,17 @@ describe('PlaylistNewPage', () => { describe('when submitted', () => { it('then correct api should be called', async () => { - const { backendSrvMock } = getTestContext(); + const { postSpy } = getTestContext(); expect(locationService.getLocation().pathname).toEqual('/'); await userEvent.type(screen.getByRole('textbox', { name: selectors.pages.PlaylistForm.name }), 'A new name'); fireEvent.submit(screen.getByRole('button', { name: /save/i })); - await waitFor(() => expect(backendSrvMock).toHaveBeenCalledTimes(1)); - expect(backendSrvMock).toHaveBeenCalledWith( + await waitFor(() => expect(postSpy).toHaveBeenCalledTimes(1)); + + expect(postSpy).toHaveBeenCalledWith( expect.objectContaining({ + method: 'POST', body: expect.objectContaining({ spec: { title: 'A new name', From 496d6f8021eeadd78d87111aaed161e8e0da14be Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 20 Nov 2025 17:33:58 +0000 Subject: [PATCH 034/378] Frontend Service: Avoid double counting boot errors (#114250) avoid double counting boot errors --- pkg/services/frontend/index.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/services/frontend/index.html b/pkg/services/frontend/index.html index 2a8cebebef6..2eb4d82dce3 100644 --- a/pkg/services/frontend/index.html +++ b/pkg/services/frontend/index.html @@ -187,7 +187,15 @@ // Wrap in an IIFE to avoid polluting the global scope. Intentionally global-scope properties // are explicitly assigned to the `window` object. (() => { + // Grafana can only fail to load once + // However, it can fail to load in multiple different places + // To avoid double reporting the error, we use this boolean to check if we've already failed + let hasFailedToBoot = false; window.__grafana_load_failed = function(err) { + if (hasFailedToBoot) { + return; + } + hasFailedToBoot = true; console.error('Failed to load Grafana', err); document.querySelector('.fs-variant-loader').classList.add('fs-hidden'); document.querySelector('.fs-variant-error').classList.remove('fs-hidden'); @@ -356,7 +364,6 @@ document.head.appendChild(cssLink); } - window.__grafana_boot_data_promise = initGrafana() window.__grafana_boot_data_promise.catch((err) => { console.error("__grafana_boot_data_promise rejected", err); From 0b020c5e30fbc4b1f5585cb3107ed60f77c6ea8f Mon Sep 17 00:00:00 2001 From: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:44:06 -0600 Subject: [PATCH 035/378] DOCS: add alias to SQL expressions doc (#114253) added alias to SQL expressions since the doc moved --- .../query-transform-data/sql-expressions/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/visualizations/panels-visualizations/query-transform-data/sql-expressions/index.md b/docs/sources/visualizations/panels-visualizations/query-transform-data/sql-expressions/index.md index 7cb91aaeb7f..ded072448ce 100644 --- a/docs/sources/visualizations/panels-visualizations/query-transform-data/sql-expressions/index.md +++ b/docs/sources/visualizations/panels-visualizations/query-transform-data/sql-expressions/index.md @@ -1,5 +1,6 @@ --- aliases: + - ../../panels-visualizations/query-transform-data/sql-expressions/ # /docs/grafana/next/panels-visualizations/query-transform-data/sql-expressions/ labels: products: - cloud From 6f9c867660c4b76941fe090d01a86a2446719714 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 20 Nov 2025 13:49:46 -0500 Subject: [PATCH 036/378] Timeseries: More nuanced editing of linear threshold to avoid crashes (#112301) * Timeseries: More nuanced editing of linear threshold to avoid crashes * remove useEffect import * remove unused import * update error and invalid field handling to call out error case --- .../src/options/builder/axis.test.tsx | 33 ++++++++++ .../grafana-ui/src/options/builder/axis.tsx | 64 ++++++++++++++++--- public/locales/cs-CZ/grafana.json | 6 +- public/locales/de-DE/grafana.json | 6 +- public/locales/en-US/grafana.json | 8 ++- public/locales/es-ES/grafana.json | 6 +- public/locales/fr-FR/grafana.json | 6 +- public/locales/hu-HU/grafana.json | 6 +- public/locales/id-ID/grafana.json | 6 +- public/locales/it-IT/grafana.json | 6 +- public/locales/ja-JP/grafana.json | 6 +- public/locales/ko-KR/grafana.json | 6 +- public/locales/nl-NL/grafana.json | 6 +- public/locales/pl-PL/grafana.json | 6 +- public/locales/pt-BR/grafana.json | 6 +- public/locales/pt-PT/grafana.json | 6 +- public/locales/ru-RU/grafana.json | 6 +- public/locales/sv-SE/grafana.json | 6 +- public/locales/tr-TR/grafana.json | 6 +- public/locales/zh-Hans/grafana.json | 6 +- public/locales/zh-Hant/grafana.json | 6 +- 21 files changed, 168 insertions(+), 45 deletions(-) create mode 100644 packages/grafana-ui/src/options/builder/axis.test.tsx diff --git a/packages/grafana-ui/src/options/builder/axis.test.tsx b/packages/grafana-ui/src/options/builder/axis.test.tsx new file mode 100644 index 00000000000..0b749ec6f63 --- /dev/null +++ b/packages/grafana-ui/src/options/builder/axis.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { ScaleDistribution } from '@grafana/schema'; + +import { ScaleDistributionEditor } from './axis'; + +describe('ScaleDistributionEditor', () => { + describe('Symlog', () => { + it('linear threshold should not dispatch a change for 0', async () => { + const onChange = jest.fn(); + const origValue = { type: ScaleDistribution.Symlog, log: 10 }; + + render(); + + // so annoying that this doesn't work. + // const el = await screen.findByLabelText('Linear threshold'); + const el = screen.getByTestId('input-wrapper').querySelector('input')!; + + await userEvent.type(el, '0'); + expect(onChange).not.toHaveBeenCalled(); + + await userEvent.type(el, '.'); + expect(onChange).not.toHaveBeenCalled(); + + await userEvent.type(el, '5'); + expect(onChange).toHaveBeenCalledWith({ linearThreshold: 0.5, ...origValue }); + + await userEvent.clear(el); + expect(onChange).toHaveBeenCalledWith(origValue); + }); + }); +}); diff --git a/packages/grafana-ui/src/options/builder/axis.tsx b/packages/grafana-ui/src/options/builder/axis.tsx index d3b6a74ee07..83237cb310c 100644 --- a/packages/grafana-ui/src/options/builder/axis.tsx +++ b/packages/grafana-ui/src/options/builder/axis.tsx @@ -1,3 +1,5 @@ +import { useState } from 'react'; + import { FieldConfigEditorBuilder, FieldType, @@ -126,12 +128,31 @@ const LOG_DISTRIBUTION_OPTIONS: Array> = [ }, ]; +const isValidLinearThreshold = (value: number): string | undefined => { + if (Number.isNaN(value)) { + return t('grafana-ui.axis-builder.linear-threshold.warning.nan', 'Linear threshold must be a number'); + } + if (value === 0) { + return t('grafana-ui.axis-builder.linear-threshold.warning.zero', 'Linear threshold cannot be zero'); + } + return; +}; + /** * @internal */ -export const ScaleDistributionEditor = ({ value, onChange }: StandardEditorProps) => { +export const ScaleDistributionEditor = ({ + value, + onChange, +}: Pick, 'value' | 'onChange'>) => { const type = value?.type ?? ScaleDistribution.Linear; const log = value?.log ?? 2; + + const [localLinearThreshold, setLocalLinearThreshold] = useState( + value?.linearThreshold != null ? String(value.linearThreshold) : '' + ); + const [linearThresholdWarning, setLinearThresholdWarning] = useState(); + const DISTRIBUTION_OPTIONS: Array> = [ { label: t('grafana-ui.builder.axis.scale-distribution-editor.distribution-options.label-linear', 'Linear'), @@ -161,7 +182,7 @@ export const ScaleDistributionEditor = ({ value, onChange }: StandardEditorProps }} /> {(type === ScaleDistribution.Log || type === ScaleDistribution.Symlog) && ( - + { + if (ev.currentTarget.value) { + setLinearThresholdWarning(isValidLinearThreshold(Number(ev.currentTarget.value))); + } + }} onChange={(v) => { - onChange({ - ...value, - linearThreshold: Number(v.currentTarget.value), - }); + setLocalLinearThreshold(v.currentTarget.value); + if (v.currentTarget.value === '') { + const newValue = { ...value }; + delete newValue.linearThreshold; + onChange(newValue); + setLinearThresholdWarning(undefined); + return; + } + + const asNumber = Number(v.currentTarget.value); + if (isValidLinearThreshold(asNumber) == null) { + setLinearThresholdWarning(undefined); + onChange({ + ...value, + linearThreshold: asNumber, + }); + } }} /> diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 70cca883b8b..cc3b5e994c4 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -8661,7 +8661,9 @@ "saving": "Ukládání <1>" }, "axis-builder": { - "linear-threshold": "Lineární práh", + "linear-threshold": { + "label": "Lineární práh" + }, "log-base": "Základ protokolu" }, "builder": { @@ -14694,4 +14696,4 @@ "label-points": "Body" } } -} \ No newline at end of file +} diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index d491c81700b..a4eb397e4f7 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -8589,7 +8589,9 @@ "saving": "<1> wird gespeichert" }, "axis-builder": { - "linear-threshold": "Linearer Schwellenwert", + "linear-threshold": { + "label": "Linearer Schwellenwert" + }, "log-base": "Logarithmische Basis" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Punkte" } } -} \ No newline at end of file +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index d29fe101bd6..ba66da11bbf 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -8589,7 +8589,13 @@ "saving": "Saving <1>" }, "axis-builder": { - "linear-threshold": "Linear threshold", + "linear-threshold": { + "label": "Linear threshold", + "warning": { + "nan": "Linear threshold must be a number", + "zero": "Linear threshold cannot be zero" + } + }, "log-base": "Log base" }, "builder": { diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index adae8f28bf6..933d56ff352 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Guardando <1>" }, "axis-builder": { - "linear-threshold": "Umbral lineal", + "linear-threshold": { + "label": "Umbral lineal" + }, "log-base": "Base de registro" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Puntos" } } -} \ No newline at end of file +} diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index e0648accd63..9bedeb3235c 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Enregistrement en cours <1>" }, "axis-builder": { - "linear-threshold": "Seuil linéaire", + "linear-threshold": { + "label": "Seuil linéaire" + }, "log-base": "Base du journal" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Points" } } -} \ No newline at end of file +} diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 80e9b74c7a7..bccbc9c4437 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Mentés: <1>" }, "axis-builder": { - "linear-threshold": "Lineáris küszöbérték", + "linear-threshold": { + "label": "Lineáris küszöbérték" + }, "log-base": "Naplóbázis" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Pontok" } } -} \ No newline at end of file +} diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 1d0d348f979..e2653a0bbee 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -8553,7 +8553,9 @@ "saving": "Menyimpan <1>" }, "axis-builder": { - "linear-threshold": "Ambang linear", + "linear-threshold": { + "label": "Ambang linear" + }, "log-base": "Basis log" }, "builder": { @@ -14523,4 +14525,4 @@ "label-points": "Poin" } } -} \ No newline at end of file +} diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 31e179978f2..4d9670ae838 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Salvataggio in corso di <1>" }, "axis-builder": { - "linear-threshold": "Soglia lineare", + "linear-threshold": { + "label": "Soglia lineare" + }, "log-base": "Base logaritmica" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Punti" } } -} \ No newline at end of file +} diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 22a4029f62d..ba8d530e4a0 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -8553,7 +8553,9 @@ "saving": "<1>を保存しています" }, "axis-builder": { - "linear-threshold": "線形のしきい値", + "linear-threshold": { + "label": "線形のしきい値" + }, "log-base": "ログベース" }, "builder": { @@ -14523,4 +14525,4 @@ "label-points": "ポイント" } } -} \ No newline at end of file +} diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 3174cc0be90..debe88cc4ac 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -8553,7 +8553,9 @@ "saving": "<1> 저장 중" }, "axis-builder": { - "linear-threshold": "선형 임계값", + "linear-threshold": { + "label": "선형 임계값" + }, "log-base": "로그 베이스" }, "builder": { @@ -14523,4 +14525,4 @@ "label-points": "포인트" } } -} \ No newline at end of file +} diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index ebef81b6199..46a2279ecfb 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Opslaan <1>" }, "axis-builder": { - "linear-threshold": "Lineaire drempel", + "linear-threshold": { + "label": "Lineaire drempel" + }, "log-base": "Logboekbase" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Punten" } } -} \ No newline at end of file +} diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 4ea5e15c281..977a67b7d6c 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -8661,7 +8661,9 @@ "saving": "Zapisywanie <1>" }, "axis-builder": { - "linear-threshold": "Próg liniowy", + "linear-threshold": { + "label": "Próg liniowy" + }, "log-base": "Baza logów" }, "builder": { @@ -14694,4 +14696,4 @@ "label-points": "Punkty" } } -} \ No newline at end of file +} diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index c6b347e785f..efeee889595 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Salvando <1>" }, "axis-builder": { - "linear-threshold": "Limite linear", + "linear-threshold": { + "label": "Limite linear" + }, "log-base": "Base de log" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Pontos" } } -} \ No newline at end of file +} diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 26db09f82dd..fe3265ca211 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -8589,7 +8589,9 @@ "saving": "A guardar <1>" }, "axis-builder": { - "linear-threshold": "Limite linear", + "linear-threshold": { + "label": "Limite linear" + }, "log-base": "Base de registo" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Pontos" } } -} \ No newline at end of file +} diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index dec4add29ca..e4f9fe26d4f 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -8661,7 +8661,9 @@ "saving": "Сохранение <1>" }, "axis-builder": { - "linear-threshold": "Линейный порог", + "linear-threshold": { + "label": "Линейный порог" + }, "log-base": "База журналов" }, "builder": { @@ -14694,4 +14696,4 @@ "label-points": "Точки" } } -} \ No newline at end of file +} diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 65178bb1cc1..aee0dd73037 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -8589,7 +8589,9 @@ "saving": "Sparar <1>" }, "axis-builder": { - "linear-threshold": "Lineär tröskel", + "linear-threshold": { + "label": "Lineär tröskel" + }, "log-base": "Loggbas" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Poäng" } } -} \ No newline at end of file +} diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 9674e80d4de..d70e3969044 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -8589,7 +8589,9 @@ "saving": "<1> kaydediliyor" }, "axis-builder": { - "linear-threshold": "Doğrusal eşik", + "linear-threshold": { + "label": "Doğrusal eşik" + }, "log-base": "Günlük tabanı" }, "builder": { @@ -14580,4 +14582,4 @@ "label-points": "Noktalar" } } -} \ No newline at end of file +} diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index d0302660f99..72c6cd12396 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -8553,7 +8553,9 @@ "saving": "正在保存<1>" }, "axis-builder": { - "linear-threshold": "线性阈值", + "linear-threshold": { + "label": "线性阈值" + }, "log-base": "记录基础" }, "builder": { @@ -14523,4 +14525,4 @@ "label-points": "点" } } -} \ No newline at end of file +} diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index ad6c511d1f8..46758e840b5 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -8553,7 +8553,9 @@ "saving": "正在儲存<1>" }, "axis-builder": { - "linear-threshold": "線性臨界值", + "linear-threshold": { + "label": "線性臨界值" + }, "log-base": "日誌基礎" }, "builder": { @@ -14523,4 +14525,4 @@ "label-points": "點" } } -} \ No newline at end of file +} From ef00dd494061de0764aad659e3d0174117004938 Mon Sep 17 00:00:00 2001 From: Jesse David Peterson Date: Thu, 20 Nov 2025 17:14:27 -0400 Subject: [PATCH 037/378] Dashboard: New experimental time range zoom shortcuts (#114190) * feat(toggle): new feature toggle for time range zoom keyboard shortcuts * feat(keyboard-shortcuts): handle `t +` and `t -` key combinations * test(keyboard-shortcuts): validate time range zoom with `t +`, and `t -` * chore(i18n): update keyboard shortcut translations * refactor(time-range): no-op when timespan is zero instead of defaulting --- .../src/types/featureToggles.gen.ts | 4 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.json | 13 ++ .../core/components/help/HelpModal.test.tsx | 70 +++++++++ public/app/core/components/help/HelpModal.tsx | 45 ++++-- public/app/core/services/keybindingSrv.ts | 18 ++- .../scene/keyboardShortcuts.test.ts | 143 ++++++++++++++++++ .../scene/keyboardShortcuts.ts | 57 ++++++- public/locales/en-US/grafana.json | 1 + 10 files changed, 337 insertions(+), 22 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index a7d2378f765..e5c59000c8d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -669,6 +669,10 @@ export interface FeatureToggles { */ timeRangePan?: boolean; /** + * Enables new keyboard shortcuts for time range zoom operations + */ + newTimeRangeZoomShortcuts?: boolean; + /** * Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. * @default false */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index c56f46a7193..28ff9e1809e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1099,6 +1099,13 @@ var ( FrontendOnly: true, Owner: grafanaDatavizSquad, }, + { + Name: "newTimeRangeZoomShortcuts", + Description: "Enables new keyboard shortcuts for time range zoom operations", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDatavizSquad, + }, { Name: "azureMonitorDisableLogLimit", Description: "Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 9465d6992c3..4ee6e57a9ed 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -152,6 +152,7 @@ pluginsSriChecks,GA,@grafana/plugins-platform-backend,false,false,false unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,false,false timeRangeProvider,experimental,@grafana/grafana-frontend-platform,false,false,false timeRangePan,experimental,@grafana/dataviz-squad,false,false,true +newTimeRangeZoomShortcuts,experimental,@grafana/dataviz-squad,false,false,true azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false playlistsReconciler,experimental,@grafana/grafana-app-platform-squad,false,true,false passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 2298b91d854..bb0e03295a0 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2873,6 +2873,19 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "newTimeRangeZoomShortcuts", + "resourceVersion": "1763646782694", + "creationTimestamp": "2025-11-20T13:53:02Z" + }, + "spec": { + "description": "Enables new keyboard shortcuts for time range zoom operations", + "stage": "experimental", + "codeowner": "@grafana/dataviz-squad", + "frontend": true + } + }, { "metadata": { "name": "newVizSuggestions", diff --git a/public/app/core/components/help/HelpModal.test.tsx b/public/app/core/components/help/HelpModal.test.tsx index d343c5560b0..a410f1cd4d6 100644 --- a/public/app/core/components/help/HelpModal.test.tsx +++ b/public/app/core/components/help/HelpModal.test.tsx @@ -1,6 +1,7 @@ import { renderHook } from '@testing-library/react'; import { useAssistant } from '@grafana/assistant'; +import { config } from '@grafana/runtime'; import { useShortcuts } from './HelpModal'; @@ -148,4 +149,73 @@ describe('useShortcuts', () => { const assistantShortcut = globalCategory!.shortcuts.find((shortcut) => shortcut.keys.includes('ctrl + .')); expect(assistantShortcut).toBeDefined(); }); + + describe('time range zoom shortcuts with feature toggle', () => { + beforeEach(() => { + mockUseAssistant.mockReturnValue({ + isAvailable: false, + openAssistant: jest.fn(), + closeAssistant: jest.fn(), + toggleAssistant: jest.fn(), + }); + }); + + it('should show new zoom shortcuts when feature toggle is enabled', () => { + config.featureToggles.newTimeRangeZoomShortcuts = true; + + const { result } = renderHook(() => useShortcuts()); + + const timeRangeCategory = result.current.find((cat) => cat.category.includes('Time range')); + + const zoomInShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('+')); + const zoomOutShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('-')); + + expect(zoomInShortcut).toBeDefined(); + expect(zoomInShortcut!.isNew).toBe(true); + expect(zoomOutShortcut).toBeDefined(); + expect(zoomOutShortcut!.isNew).toBe(true); + }); + + it('should show legacy t z shortcut when feature toggle is disabled', () => { + config.featureToggles.newTimeRangeZoomShortcuts = false; + + const { result } = renderHook(() => useShortcuts()); + + const timeRangeCategory = result.current.find((cat) => cat.category.includes('Time range')); + + const legacyZoomShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('z')); + const newZoomInShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('+')); + const newZoomOutShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('-')); + + expect(legacyZoomShortcut).toBeDefined(); + expect(newZoomInShortcut).toBeUndefined(); + expect(newZoomOutShortcut).toBeUndefined(); + }); + + it('should not show isNew badge on legacy shortcuts', () => { + config.featureToggles.newTimeRangeZoomShortcuts = false; + + const { result } = renderHook(() => useShortcuts()); + + const timeRangeCategory = result.current.find((cat) => cat.category.includes('Time range')); + + const legacyZoomShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('z')); + + expect(legacyZoomShortcut!.isNew).toBeUndefined(); + }); + + it('should show isNew badge on new shortcuts when feature toggle is enabled', () => { + config.featureToggles.newTimeRangeZoomShortcuts = true; + + const { result } = renderHook(() => useShortcuts()); + + const timeRangeCategory = result.current.find((cat) => cat.category.includes('Time range')); + + const zoomInShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('+')); + const zoomOutShortcut = timeRangeCategory!.shortcuts.find((s) => s.keys.includes('t') && s.keys.includes('-')); + + expect(zoomInShortcut!.isNew).toBe(true); + expect(zoomOutShortcut!.isNew).toBe(true); + }); + }); }); diff --git a/public/app/core/components/help/HelpModal.tsx b/public/app/core/components/help/HelpModal.tsx index 4017443b252..82f39fac28f 100644 --- a/public/app/core/components/help/HelpModal.tsx +++ b/public/app/core/components/help/HelpModal.tsx @@ -2,9 +2,10 @@ import { css } from '@emotion/css'; import { useMemo } from 'react'; import { useAssistant } from '@grafana/assistant'; -import { GrafanaTheme2 } from '@grafana/data'; +import { FeatureState, GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { Grid, Modal, useStyles2, Text } from '@grafana/ui'; +import { config } from '@grafana/runtime'; +import { Grid, Modal, useStyles2, Text, FeatureBadge } from '@grafana/ui'; import { getModKey } from 'app/core/utils/browser'; export interface HelpModalProps { @@ -36,7 +37,7 @@ export const HelpModal = ({ onDismiss }: HelpModalProps): JSX.Element => { - {shortcuts.map(({ keys, description }) => ( + {shortcuts.map(({ keys, description, isNew }) => ( {keys.map((key) => ( @@ -44,9 +45,12 @@ export const HelpModal = ({ onDismiss }: HelpModalProps): JSX.Element => { ))} - - {description} - +
+ + {description} + + {isNew && } +
))} @@ -104,10 +108,25 @@ export const useShortcuts = () => { { category: t('help-modal.shortcuts-category.time-range', 'Time range'), shortcuts: [ - { - keys: ['t', 'z'], - description: t('help-modal.shortcuts-description.zoom-out-time-range', 'Zoom out time range'), - }, + ...(config.featureToggles.newTimeRangeZoomShortcuts + ? [ + { + keys: ['t', '+'], + description: t('help-modal.shortcuts-description.zoom-in-time-range', 'Zoom in time range'), + isNew: true, + }, + { + keys: ['t', '-'], + description: t('help-modal.shortcuts-description.zoom-out-time-range', 'Zoom out time range'), + isNew: true, + }, + ] + : [ + { + keys: ['t', 'z'], + description: t('help-modal.shortcuts-description.zoom-out-time-range', 'Zoom out time range'), + }, + ]), { keys: ['t', '←'], description: t('help-modal.shortcuts-description.move-time-range-back', 'Move time range back'), @@ -277,6 +296,12 @@ function getStyles(theme: GrafanaTheme2) { whiteSpace: 'nowrap', minWidth: 83, // To match column widths with the widest }), + descriptionWrapper: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.75), + flexWrap: 'nowrap', + }), shortcutTableKey: css({ display: 'inline-block', textAlign: 'center', diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 865f461ce84..ea232baffcc 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -1,6 +1,6 @@ import { toggleAssistant, isAssistantAvailable } from '@grafana/assistant'; import { LegacyGraphHoverClearEvent, SetPanelAttentionEvent, locationUtil } from '@grafana/data'; -import { LocationService } from '@grafana/runtime'; +import { LocationService, config } from '@grafana/runtime'; import { appEvents } from 'app/core/app_events'; import { getExploreUrl } from 'app/core/utils/explore'; import { toggleMockApiAndReload, togglePseudoLocale } from 'app/dev-utils'; @@ -231,9 +231,19 @@ export class KeybindingSrv { appEvents.publish(new AbsoluteTimeEvent({ updateUrl })); }); - this.bind('t z', () => { - appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl })); - }); + if (config.featureToggles.newTimeRangeZoomShortcuts) { + this.bind('t +', () => { + appEvents.publish(new ZoomOutEvent({ scale: 0.5, updateUrl })); + }); + + this.bind('t -', () => { + appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl })); + }); + } else { + this.bind('t z', () => { + appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl })); + }); + } this.bind('ctrl+z', () => { appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl })); diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts index 856f8df3193..9a70858f39b 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts @@ -1,4 +1,5 @@ import { LegacyGraphHoverClearEvent } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { behaviors, sceneGraph, SceneTimeRange } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; import { appEvents } from 'app/core/app_events'; @@ -253,4 +254,146 @@ describe('setupKeyboardShortcuts', () => { expect(drBinding).toBeDefined(); }); }); + + describe('time range zoom shortcuts with feature toggle', () => { + describe('when newTimeRangeZoomShortcuts is enabled', () => { + beforeEach(() => { + config.featureToggles.newTimeRangeZoomShortcuts = true; + jest.clearAllMocks(); + }); + + it('should setup t + zoom in shortcut', () => { + setupKeyboardShortcuts(mockScene); + + const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +'); + expect(tPlusBinding).toBeDefined(); + }); + + it('should setup t - zoom out shortcut with keypress type', () => { + setupKeyboardShortcuts(mockScene); + + const tMinusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't -'); + expect(tMinusBinding).toBeDefined(); + expect(tMinusBinding![0].type).toBe('keypress'); + }); + + it('should not setup t z shortcut when feature toggle is on', () => { + setupKeyboardShortcuts(mockScene); + + const tzBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't z'); + expect(tzBinding).toBeUndefined(); + }); + }); + + describe('when newTimeRangeZoomShortcuts is disabled', () => { + beforeEach(() => { + config.featureToggles.newTimeRangeZoomShortcuts = false; + jest.clearAllMocks(); + }); + + it('should setup legacy t z shortcut', () => { + setupKeyboardShortcuts(mockScene); + + const tzBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't z'); + expect(tzBinding).toBeDefined(); + }); + + it('should not setup new zoom shortcuts when feature toggle is off', () => { + setupKeyboardShortcuts(mockScene); + + const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +'); + const tMinusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't -'); + + expect(tPlusBinding).toBeUndefined(); + expect(tMinusBinding).toBeUndefined(); + }); + }); + + describe('zoom handler logic', () => { + let mockTimeRange: ReturnType; + + function createMockTimeRange() { + return { + state: { + value: { + from: { valueOf: () => new Date('2024-01-01 12:00:00').getTime() }, + to: { valueOf: () => new Date('2024-01-01 18:00:00').getTime() }, // 6 hour span + raw: { from: 'now-6h', to: 'now' }, + }, + }, + onTimeRangeChange: jest.fn(), + } satisfies { + state: { + value: { + from: { valueOf: () => number }; + to: { valueOf: () => number }; + raw: { from: string; to: string }; + }; + }; + onTimeRangeChange: jest.Mock; + }; + } + + beforeEach(() => { + config.featureToggles.newTimeRangeZoomShortcuts = true; + mockTimeRange = createMockTimeRange(); + + (sceneGraph.getTimeRange as jest.Mock).mockReturnValue(mockTimeRange); + jest.clearAllMocks(); + }); + + it('should zoom in (scale 0.5) when t + is pressed', () => { + setupKeyboardShortcuts(mockScene); + + const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +'); + const handler = tPlusBinding![0].onTrigger; + + handler(); + + // Scale 0.5 should result in 3 hour span (half of 6) + expect(mockTimeRange.onTimeRangeChange).toHaveBeenCalledWith( + expect.objectContaining({ + from: expect.any(Object), + to: expect.any(Object), + raw: expect.any(Object), + }) + ); + + const call = mockTimeRange.onTimeRangeChange.mock.calls[0][0]; + const newSpan = call.to.valueOf() - call.from.valueOf(); + expect(newSpan).toBe(3 * 60 * 60 * 1000); // 3 hours in milliseconds + }); + + it('should keep center point when zooming in', () => { + setupKeyboardShortcuts(mockScene); + + const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +'); + const handler = tPlusBinding![0].onTrigger; + + const originalCenter = (mockTimeRange.state.value.from.valueOf() + mockTimeRange.state.value.to.valueOf()) / 2; + + handler(); + + const call = mockTimeRange.onTimeRangeChange.mock.calls[0][0]; + const newCenter = (call.from.valueOf() + call.to.valueOf()) / 2; + + expect(newCenter).toBe(originalCenter); + }); + + it('should do nothing when timespan is zero', () => { + mockTimeRange.state.value.from.valueOf = () => new Date('2024-01-01 12:00:00').getTime(); + mockTimeRange.state.value.to.valueOf = () => new Date('2024-01-01 12:00:00').getTime(); // Same time + + setupKeyboardShortcuts(mockScene); + + const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +'); + const handler = tPlusBinding![0].onTrigger; + + handler(); + + // Should not call onTimeRangeChange when timespan is 0 + expect(mockTimeRange.onTimeRangeChange).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index b6ebf75bf44..1113b8a8a8c 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -1,4 +1,4 @@ -import { locationUtil, SetPanelAttentionEvent, LegacyGraphHoverClearEvent } from '@grafana/data'; +import { locationUtil, SetPanelAttentionEvent, LegacyGraphHoverClearEvent, dateTime } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { behaviors, sceneGraph, VizPanel } from '@grafana/scenes'; import { appEvents } from 'app/core/app_events'; @@ -130,13 +130,29 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { onTrigger: () => sceneGraph.getTimeRange(scene).onRefresh(), }); - // Zoom out - keybindings.addBinding({ - key: 't z', - onTrigger: () => { - handleZoomOut(scene); - }, - }); + if (config.featureToggles.newTimeRangeZoomShortcuts) { + keybindings.addBinding({ + key: 't +', + onTrigger: () => { + handleZoom(scene, 0.5); + }, + }); + + keybindings.addBinding({ + key: 't -', + type: 'keypress', // NOTE: Because some browsers/OS identify minus symbol differently. + onTrigger: () => { + handleZoomOut(scene); + }, + }); + } else { + keybindings.addBinding({ + key: 't z', + onTrigger: () => { + handleZoomOut(scene); + }, + }); + } keybindings.addBinding({ key: 'ctrl+z', @@ -266,6 +282,31 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { }; } +function handleZoom(scene: DashboardScene, scale: number) { + const timeRange = sceneGraph.getTimeRange(scene); + const currentRange = timeRange.state.value; + const timespan = currentRange.to.valueOf() - currentRange.from.valueOf(); + + if (timespan === 0) { + return; + } + + const center = currentRange.to.valueOf() - timespan / 2; + const newTimespan = timespan * scale; + + const to = center + newTimespan / 2; + const from = center - newTimespan / 2; + + timeRange.onTimeRangeChange({ + from: dateTime(from), + to: dateTime(to), + raw: { + from: dateTime(from), + to: dateTime(to), + }, + }); +} + function handleZoomOut(scene: DashboardScene) { const timePicker = dashboardSceneGraph.getTimePicker(scene); timePicker?.onZoom(); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index ba66da11bbf..9539dbefc30 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -9297,6 +9297,7 @@ "toggle-panel-edit": "Toggle panel edit view", "toggle-panel-fullscreen": "Toggle panel fullscreen view", "toggle-panel-legend": "Toggle panel legend", + "zoom-in-time-range": "Zoom in time range", "zoom-out-time-range": "Zoom out time range" }, "title": "Shortcuts" From 07d975134d27f1c6bf44e326511bbef1bd5af26b Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 00:39:57 +0000 Subject: [PATCH 038/378] I18n: Download translations from Crowdin (#114278) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 17 +++++++++++++++-- public/locales/de-DE/grafana.json | 17 +++++++++++++++-- public/locales/es-ES/grafana.json | 17 +++++++++++++++-- public/locales/fr-FR/grafana.json | 17 +++++++++++++++-- public/locales/hu-HU/grafana.json | 17 +++++++++++++++-- public/locales/id-ID/grafana.json | 17 +++++++++++++++-- public/locales/it-IT/grafana.json | 17 +++++++++++++++-- public/locales/ja-JP/grafana.json | 17 +++++++++++++++-- public/locales/ko-KR/grafana.json | 17 +++++++++++++++-- public/locales/nl-NL/grafana.json | 17 +++++++++++++++-- public/locales/pl-PL/grafana.json | 17 +++++++++++++++-- public/locales/pt-BR/grafana.json | 17 +++++++++++++++-- public/locales/pt-PT/grafana.json | 17 +++++++++++++++-- public/locales/ru-RU/grafana.json | 17 +++++++++++++++-- public/locales/sv-SE/grafana.json | 17 +++++++++++++++-- public/locales/tr-TR/grafana.json | 17 +++++++++++++++-- public/locales/zh-Hans/grafana.json | 17 +++++++++++++++-- public/locales/zh-Hant/grafana.json | 17 +++++++++++++++-- 18 files changed, 270 insertions(+), 36 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index cc3b5e994c4..01d21c3f657 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -8662,7 +8662,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Lineární práh" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Základ protokolu" }, @@ -9069,6 +9073,11 @@ "series-color-picker-popover": { "y-axis-usage": "Použít pravou osu y" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9360,6 +9369,7 @@ "toggle-panel-edit": "Přepnout zobrazení úprav panelu", "toggle-panel-fullscreen": "Přepnout zobrazení panelu na celou obrazovku", "toggle-panel-legend": "Přepnout legendu panelu", + "zoom-in-time-range": "", "zoom-out-time-range": "Oddálit časový rozsah" }, "title": "Zkratky" @@ -12650,11 +12660,14 @@ "copy-clipboard": "Kopírovat do schránky", "copy-to-clipboard-and-close": "Kopírovat do schránky a zavřít", "description-name-to-easily-identify-the-token": "Název pro snadnou identifikaci tokenu", + "description-no-expiration-disabled": "", "description-token": "Zkopírujte token teď, protože ho znovu neuvidíte. Ztráta tokenu vyžaduje vytvoření nového tokenu.", "generate-token": "Generování tokenu", "label-display-name": "Zobrazované jméno", "label-expiration": "Expirace", "label-expiration-date": "Datum vypršení platnosti", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14696,4 +14709,4 @@ "label-points": "Body" } } -} +} \ No newline at end of file diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index a4eb397e4f7..9a3b21b70aa 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Linearer Schwellenwert" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Logarithmische Basis" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Rechte y-Achse verwenden" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Fenster bearbeiten-Ansicht umschalten", "toggle-panel-fullscreen": "Vollbildansicht des Fensters umschalten", "toggle-panel-legend": "Legende des Fensters umschalten", + "zoom-in-time-range": "", "zoom-out-time-range": "Zeitbereich verkleinern" }, "title": "Shortcuts" @@ -12540,11 +12550,14 @@ "copy-clipboard": "In die Zwischenablage kopieren", "copy-to-clipboard-and-close": "In Zwischenablage kopieren und schließen", "description-name-to-easily-identify-the-token": "Name zur einfachen Identifizierung des Tokens", + "description-no-expiration-disabled": "", "description-token": "Kopieren Sie den Token jetzt, denn Sie werden ihn später nicht mehr sehen können. Wenn Sie einen Token verlieren, müssen Sie einen neuen Token erstellen.", "generate-token": "Token generieren", "label-display-name": "Anzeigename", "label-expiration": "Ablauf", "label-expiration-date": "Ablaufdatum", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Punkte" } } -} +} \ No newline at end of file diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 933d56ff352..b46f38a8442 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Umbral lineal" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Base de registro" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Usar eje y derecho" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Cambiar la vista de edición del panel", "toggle-panel-fullscreen": "Cambiar la vista de pantalla completa del panel", "toggle-panel-legend": "Cambiar leyenda del panel", + "zoom-in-time-range": "", "zoom-out-time-range": "Reducir el intervalo de tiempo" }, "title": "Accesos directos" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Copiar al portapapeles", "copy-to-clipboard-and-close": "Copiar al portapapeles y cerrar", "description-name-to-easily-identify-the-token": "Nombre para identificar fácilmente el token", + "description-no-expiration-disabled": "", "description-token": "Copia el token ahora, ya que no lo volverás a ver. Si lo pierdes, deberás crear uno nuevo.", "generate-token": "Generar token", "label-display-name": "Nombre para mostrar", "label-expiration": "Caducidad", "label-expiration-date": "Fecha de vencimiento", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Puntos" } } -} +} \ No newline at end of file diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 9bedeb3235c..428c251e152 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Seuil linéaire" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Base du journal" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Utiliser l'axe y droit" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Activer/Désactiver la vue Édition du panneau", "toggle-panel-fullscreen": "Activer/Désactiver la vue Plein écran", "toggle-panel-legend": "Activer/désactiver la légende du panneau", + "zoom-in-time-range": "", "zoom-out-time-range": "Dézoomer la plage de temps" }, "title": "Raccourcis" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Copier dans le presse-papiers", "copy-to-clipboard-and-close": "Copier dans le presse-papiers et fermer", "description-name-to-easily-identify-the-token": "Nom pour identifier facilement le jeton", + "description-no-expiration-disabled": "", "description-token": "Copiez le jeton maintenant, car vous ne pourrez plus le revoir. Si vous perdez un jeton, vous devrez 'en créer un nouveau.", "generate-token": "Générer un jeton", "label-display-name": "Pseudo", "label-expiration": "Expiration", "label-expiration-date": "Date d’expiration", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Jeton" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Points" } } -} +} \ No newline at end of file diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index bccbc9c4437..ac46a842a13 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Lineáris küszöbérték" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Naplóbázis" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Jobb y tengely használata" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Panelszerkesztési nézet ki- és bekapcsolása", "toggle-panel-fullscreen": "Panel teljes képernyős nézetének ki- és bekapcsolása", "toggle-panel-legend": "Panel jelmagyarázatának ki- és bekapcsolása", + "zoom-in-time-range": "", "zoom-out-time-range": "Időtartomány kicsinyítése" }, "title": "Billentyűparancsok" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Másolás vágólapra", "copy-to-clipboard-and-close": "Másolás vágólapra és bezárás", "description-name-to-easily-identify-the-token": "Név a token egyszerű azonosításához", + "description-no-expiration-disabled": "", "description-token": "Másolja át most a tokent, mert később nem lesz látható. A token elvesztése esetén új tokent kell létrehozni.", "generate-token": "Token generálása", "label-display-name": "Megjelenített név", "label-expiration": "Lejárat", "label-expiration-date": "Lejárati dátum", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Pontok" } } -} +} \ No newline at end of file diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index e2653a0bbee..b00d4be59e5 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -8554,7 +8554,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Ambang linear" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Basis log" }, @@ -8961,6 +8965,11 @@ "series-color-picker-popover": { "y-axis-usage": "Gunakan sumbu y kanan" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9252,6 +9261,7 @@ "toggle-panel-edit": "Alihkan tampilan edit panel", "toggle-panel-fullscreen": "Alihkan tampilan layar penuh panel", "toggle-panel-legend": "Alihkan legenda panel", + "zoom-in-time-range": "", "zoom-out-time-range": "Perkecil rentang waktu" }, "title": "Pintasan" @@ -12485,11 +12495,14 @@ "copy-clipboard": "Salin ke papan klip", "copy-to-clipboard-and-close": "Salin ke papan klip dan tutup", "description-name-to-easily-identify-the-token": "Nama untuk mengidentifikasi token dengan mudah", + "description-no-expiration-disabled": "", "description-token": "Salin token sekarang karena Anda tidak akan dapat melihatnya lagi. Kehilangan token membutuhkan pembuatan token baru.", "generate-token": "Buat token", "label-display-name": "Nama tampilan", "label-expiration": "Kedaluwarsa", "label-expiration-date": "Tanggal kedaluwarsa", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14525,4 +14538,4 @@ "label-points": "Poin" } } -} +} \ No newline at end of file diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 4d9670ae838..ab7d67dc580 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Soglia lineare" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Base logaritmica" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Usa l'asse y destro" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Attiva/disattiva la vista di modifica del pannello", "toggle-panel-fullscreen": "Attiva/disattiva la vista a schermo intero del pannello", "toggle-panel-legend": "Attiva/disattiva la legenda del pannello", + "zoom-in-time-range": "", "zoom-out-time-range": "Riduci l'intervallo di tempo" }, "title": "Scelte rapide" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Copia negli appunti", "copy-to-clipboard-and-close": "Copia negli appunti e chiudi", "description-name-to-easily-identify-the-token": "Nome per identificare facilmente il token", + "description-no-expiration-disabled": "", "description-token": "Copia il token ora perché non potrai più visualizzarlo. La perdita di un token richiede la creazione di uno nuovo.", "generate-token": "Genera token", "label-display-name": "Visualizza nome", "label-expiration": "Scadenza", "label-expiration-date": "Data di scadenza", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Punti" } } -} +} \ No newline at end of file diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index ba8d530e4a0..7a09ed72d4c 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -8554,7 +8554,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "線形のしきい値" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "ログベース" }, @@ -8961,6 +8965,11 @@ "series-color-picker-popover": { "y-axis-usage": "右y軸を使用" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9252,6 +9261,7 @@ "toggle-panel-edit": "パネル編集ビューを切り替え", "toggle-panel-fullscreen": "パネルの全画面表示を切り替え", "toggle-panel-legend": "パネルの凡例を切り替え", + "zoom-in-time-range": "", "zoom-out-time-range": "時間範囲をズームアウト" }, "title": "ショートカット" @@ -12485,11 +12495,14 @@ "copy-clipboard": "クリップボードにコピー", "copy-to-clipboard-and-close": "クリップボードにコピーして閉じる", "description-name-to-easily-identify-the-token": "トークンを簡単に識別するための名前", + "description-no-expiration-disabled": "", "description-token": "トークンはもう表示できなくなるため、今すぐコピーしてください。トークンを失うと、新しいトークンを作成する必要があります。", "generate-token": "トークンを生成", "label-display-name": "表示名", "label-expiration": "有効期限", "label-expiration-date": "有効期日", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "トークン" }, "get-actions-cell": { @@ -14525,4 +14538,4 @@ "label-points": "ポイント" } } -} +} \ No newline at end of file diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index debe88cc4ac..56dd9df87fb 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -8554,7 +8554,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "선형 임계값" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "로그 베이스" }, @@ -8961,6 +8965,11 @@ "series-color-picker-popover": { "y-axis-usage": "오른쪽 y축 사용" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9252,6 +9261,7 @@ "toggle-panel-edit": "패널 편집 보기 토글", "toggle-panel-fullscreen": "패널 전체 화면 보기 토글", "toggle-panel-legend": "패널 범례 토글", + "zoom-in-time-range": "", "zoom-out-time-range": "시간 범위 확대" }, "title": "단축키" @@ -12485,11 +12495,14 @@ "copy-clipboard": "클립보드로 복사", "copy-to-clipboard-and-close": "클립보드로 복사하고 닫기", "description-name-to-easily-identify-the-token": "토큰을 쉽게 식별할 수 있는 이름", + "description-no-expiration-disabled": "", "description-token": "토큰이 다시 표시되지 않으므로 지금 복사해 두세요. 토큰을 분실하면 새 토큰을 생성해야 합니다.", "generate-token": "토큰 생성", "label-display-name": "표시 이름", "label-expiration": "만료", "label-expiration-date": "만료일", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "토큰" }, "get-actions-cell": { @@ -14525,4 +14538,4 @@ "label-points": "포인트" } } -} +} \ No newline at end of file diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 46a2279ecfb..854505274f8 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Lineaire drempel" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Logboekbase" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Rechter y-as gebruiken" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Paneelbewerkingsweergave in-/uitschakelen", "toggle-panel-fullscreen": "Paneelweergave volledig scherm in-/uitschakelen", "toggle-panel-legend": "Paneellegenda in-/uitschakelen", + "zoom-in-time-range": "", "zoom-out-time-range": "Tijdsbereik uitzoomen" }, "title": "Snelkoppelingen" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Kopiëren naar klembord", "copy-to-clipboard-and-close": "Kopiëren naar klembord en sluiten", "description-name-to-easily-identify-the-token": "Naam om het token gemakkelijk te identificeren", + "description-no-expiration-disabled": "", "description-token": "Kopieer het token nu, want hierna kun je het niet meer zien. Als je een token verliest, moet je een nieuw token maken.", "generate-token": "Token genereren", "label-display-name": "Weergavenaam", "label-expiration": "Vervaldatum", "label-expiration-date": "Vervaldatum", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Punten" } } -} +} \ No newline at end of file diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 977a67b7d6c..f3d06bb2edb 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -8662,7 +8662,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Próg liniowy" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Baza logów" }, @@ -9069,6 +9073,11 @@ "series-color-picker-popover": { "y-axis-usage": "Użyj prawej półosi OY" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9360,6 +9369,7 @@ "toggle-panel-edit": "Przełącz widok edycji panelu", "toggle-panel-fullscreen": "Przełącz widok pełnoekranowy panelu", "toggle-panel-legend": "Przełącz legendę panelu", + "zoom-in-time-range": "", "zoom-out-time-range": "Oddal zakres czasu" }, "title": "Skróty" @@ -12650,11 +12660,14 @@ "copy-clipboard": "Kopiuj do schowka", "copy-to-clipboard-and-close": "Kopiuj do schowka i zamknij", "description-name-to-easily-identify-the-token": "Nazwa ułatwiająca identyfikację tokena", + "description-no-expiration-disabled": "", "description-token": "Skopiuj token teraz, ponieważ później nie będzie go można ponownie wyświetlić. Utrata tokena wymaga utworzenia nowego.", "generate-token": "Wygeneruj token", "label-display-name": "Nazwa wyświetlana", "label-expiration": "Wygaśnięcie", "label-expiration-date": "Data ważności", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14696,4 +14709,4 @@ "label-points": "Punkty" } } -} +} \ No newline at end of file diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index efeee889595..a46882c87c8 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Limite linear" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Base de log" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Usar o eixo y à direita" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Alternar visualização de edição de painel", "toggle-panel-fullscreen": "Alternar visualização de painel em tela cheia", "toggle-panel-legend": "Alternar legenda de painel", + "zoom-in-time-range": "", "zoom-out-time-range": "Diminuir o intervalo de tempo" }, "title": "Atalhos" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Copiar para a área de transferência", "copy-to-clipboard-and-close": "Copiar para a área de transferência e fechar", "description-name-to-easily-identify-the-token": "Nome para identificar facilmente o token", + "description-no-expiration-disabled": "", "description-token": "Copie o token agora, já que você não será capaz de vê-lo novamente. Perder um token requer criar um novo.", "generate-token": "Gerar token", "label-display-name": "Nome de exibição", "label-expiration": "Validade", "label-expiration-date": "Data de vencimento", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Pontos" } } -} +} \ No newline at end of file diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index fe3265ca211..81009be83be 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Limite linear" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Base de registo" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Utilizar o eixo y direito" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Alternar vista de edição do painel", "toggle-panel-fullscreen": "Alternar vista de ecrã inteiro do painel", "toggle-panel-legend": "Alternar legenda do painel", + "zoom-in-time-range": "", "zoom-out-time-range": "Diminuir o zoom do intervalo de tempo" }, "title": "Atalhos" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Copiar para a área de transferência", "copy-to-clipboard-and-close": "Copiar para a área de transferência e fechar", "description-name-to-easily-identify-the-token": "Nome para identificar facilmente o token", + "description-no-expiration-disabled": "", "description-token": "Copie o token agora, pois não poderá vê-lo novamente. Perder um token requer a criação de um novo.", "generate-token": "Gerar token", "label-display-name": "Nome de exibição", "label-expiration": "Validade", "label-expiration-date": "Data de vencimento", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Pontos" } } -} +} \ No newline at end of file diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index e4f9fe26d4f..a1c58056eea 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -8662,7 +8662,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Линейный порог" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "База журналов" }, @@ -9069,6 +9073,11 @@ "series-color-picker-popover": { "y-axis-usage": "Использовать правую ось y" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9360,6 +9369,7 @@ "toggle-panel-edit": "Включение/выключение представления редактирования панели", "toggle-panel-fullscreen": "Включение/выключение полноэкранного представления панели", "toggle-panel-legend": "Включение/выключение условных обозначений панели", + "zoom-in-time-range": "", "zoom-out-time-range": "Уменьшение масштаба временного диапазона" }, "title": "Сочетания клавиш" @@ -12650,11 +12660,14 @@ "copy-clipboard": "Копировать в буфер обмена", "copy-to-clipboard-and-close": "Копировать в буфер обмена и закрыть", "description-name-to-easily-identify-the-token": "Название для удобной идентификации токена", + "description-no-expiration-disabled": "", "description-token": "Скопируйте токен сейчас, так как вы не сможете увидеть его снова. При потере токена требуется создать новый.", "generate-token": "Создать токен", "label-display-name": "Отображаемое имя", "label-expiration": "Срок действия", "label-expiration-date": "Срок действия", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Токен" }, "get-actions-cell": { @@ -14696,4 +14709,4 @@ "label-points": "Точки" } } -} +} \ No newline at end of file diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index aee0dd73037..ba91a108866 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Lineär tröskel" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Loggbas" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Använd höger y-axel" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Växla panelredigeringsvy", "toggle-panel-fullscreen": "Växla panelens helskärmsvy", "toggle-panel-legend": "Växla panelförklaring", + "zoom-in-time-range": "", "zoom-out-time-range": "Zooma ut tidsintervall" }, "title": "Genvägar" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Kopiera till urklippet", "copy-to-clipboard-and-close": "Kopiera till urklippet och stäng", "description-name-to-easily-identify-the-token": "Namn för att enkelt identifiera token", + "description-no-expiration-disabled": "", "description-token": "Kopiera token nu eftersom du inte kommer att kunna visa den igen. Om du förlorar denna token behöver du skapa en ny.", "generate-token": "Generera token", "label-display-name": "Visningsnamn", "label-expiration": "Utgång", "label-expiration-date": "Utgångsdatum", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Token" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Poäng" } } -} +} \ No newline at end of file diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index d70e3969044..6515d038e6a 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -8590,7 +8590,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "Doğrusal eşik" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "Günlük tabanı" }, @@ -8997,6 +9001,11 @@ "series-color-picker-popover": { "y-axis-usage": "Sağ Y eksenini kullan" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9288,6 +9297,7 @@ "toggle-panel-edit": "Panel düzenleme görünümünü aç/kapat", "toggle-panel-fullscreen": "Panel tam ekran görünümünü aç/kapat", "toggle-panel-legend": "Panel açıklamasını aç/kapat", + "zoom-in-time-range": "", "zoom-out-time-range": "Zaman aralığını büyüt" }, "title": "Kısayollar" @@ -12540,11 +12550,14 @@ "copy-clipboard": "Panoya kopyala", "copy-to-clipboard-and-close": "Panoya kopyala ve kapat", "description-name-to-easily-identify-the-token": "Belirteci kolayca tanımlamak için ad", + "description-no-expiration-disabled": "", "description-token": "Bu noktada belirteci kopyalayın çünkü tekrar göremeyeceksiniz. Belirtecin kaybedilmesi yeni bir belirteç oluşturulmasını gerektirir.", "generate-token": "Belirteç oluştur", "label-display-name": "Görünen ad", "label-expiration": "Son", "label-expiration-date": "Son kullanma tarihi", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "Belirteç" }, "get-actions-cell": { @@ -14582,4 +14595,4 @@ "label-points": "Noktalar" } } -} +} \ No newline at end of file diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 72c6cd12396..925a737732c 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -8554,7 +8554,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "线性阈值" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "记录基础" }, @@ -8961,6 +8965,11 @@ "series-color-picker-popover": { "y-axis-usage": "使用右侧 Y 轴" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9252,6 +9261,7 @@ "toggle-panel-edit": "切换面板编辑视图", "toggle-panel-fullscreen": "切换面板全屏视图", "toggle-panel-legend": "切换面板图例", + "zoom-in-time-range": "", "zoom-out-time-range": "缩放时间范围" }, "title": "快捷键" @@ -12485,11 +12495,14 @@ "copy-clipboard": "复制到剪贴板", "copy-to-clipboard-and-close": "复制到剪贴板并关闭", "description-name-to-easily-identify-the-token": "用于轻松识别令牌的名称", + "description-no-expiration-disabled": "", "description-token": "现在就复制令牌,因为您将无法再次看到它。丢失令牌后需要创建一个新令牌。", "generate-token": "生成令牌", "label-display-name": "显示名称", "label-expiration": "到期", "label-expiration-date": "到期日期", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "令牌" }, "get-actions-cell": { @@ -14525,4 +14538,4 @@ "label-points": "点" } } -} +} \ No newline at end of file diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 46758e840b5..c128f8b4360 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -8554,7 +8554,11 @@ }, "axis-builder": { "linear-threshold": { - "label": "線性臨界值" + "label": "", + "warning": { + "nan": "", + "zero": "" + } }, "log-base": "日誌基礎" }, @@ -8961,6 +8965,11 @@ "series-color-picker-popover": { "y-axis-usage": "使用右 y 軸" }, + "sidebar": { + "close": "", + "dock": "", + "undock": "" + }, "slider": { "drag-handle-aria-label": "" }, @@ -9252,6 +9261,7 @@ "toggle-panel-edit": "切換面板編輯檢視", "toggle-panel-fullscreen": "切換面板全螢幕檢視", "toggle-panel-legend": "切換面板圖例", + "zoom-in-time-range": "", "zoom-out-time-range": "縮小時間範圍" }, "title": "捷徑" @@ -12485,11 +12495,14 @@ "copy-clipboard": "複製至剪貼簿", "copy-to-clipboard-and-close": "複製至剪貼簿並關閉", "description-name-to-easily-identify-the-token": "輕鬆識別權杖的名稱", + "description-no-expiration-disabled": "", "description-token": "請立即複製權杖,因為您將無法再次看到它。若您遺失權杖,則需要建立新的權杖。", "generate-token": "產生權杖", "label-display-name": "顯示名稱", "label-expiration": "過期", "label-expiration-date": "過期日期", + "label-no-expiration": "", + "label-set-expiration-date": "", "label-token": "權杖" }, "get-actions-cell": { @@ -14525,4 +14538,4 @@ "label-points": "點" } } -} +} \ No newline at end of file From 6f68b96097821e37dea3f29e22ecd81705a88e6f Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 21 Nov 2025 08:56:17 +0100 Subject: [PATCH 039/378] Alerting: Fix minInterval interpolation when creating rules from panel (#114238) Interpolate minInterval query parameter before query conversion --- .../features/alerting/unified/utils/rule-form.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 3c000d6cab7..077934e21e1 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -21,6 +21,7 @@ import { getQueryRunnerFor, } from 'app/features/dashboard-scene/utils/utils'; import { ExpressionDatasourceUID, ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types'; +import { getTemplateSrv } from 'app/features/templating/template_srv'; import { LokiQuery } from 'app/plugins/datasource/loki/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; import { @@ -744,6 +745,9 @@ export const panelToRuleFormValues = async ( return undefined; } + // Interpolate interval to replace dashboard variables + const interpolatedInterval = panel.interval ? panel.replaceVariables(panel.interval, undefined) : undefined; + const relativeTimeRange = rangeUtil.timeRangeToRelative(rangeUtil.convertRawToRange(dashboard.time)); const queries = await dataQueriesToGrafanaQueries( targets, @@ -751,7 +755,7 @@ export const panelToRuleFormValues = async ( panel.scopedVars || {}, panel.datasource ?? undefined, panel.maxDataPoints ?? undefined, - panel.interval ?? undefined + interpolatedInterval ); // if no alerting capable queries are found, can't create a rule if (!queries.length || !queries.find((query) => query.datasourceUid !== ExpressionDatasourceUID)) { @@ -825,13 +829,19 @@ export const scenesPanelToRuleFormValues = async (vizPanel: VizPanel): Promise

Date: Fri, 21 Nov 2025 09:04:04 +0100 Subject: [PATCH 040/378] fix: handle empty provisioning folder gracefully in stub provisioning (#114273) fix: provisioning folder empty case --- pkg/services/provisioning/stubs.go | 14 +- pkg/services/provisioning/stubs_test.go | 449 ++++++++++++++++++ .../dashboards/multiple.yaml | 24 + .../stub-configs/dashboards/single.yaml | 10 + 4 files changed, 492 insertions(+), 5 deletions(-) create mode 100644 pkg/services/provisioning/stubs_test.go create mode 100644 pkg/services/provisioning/testdata/stub-configs-multiple/dashboards/multiple.yaml create mode 100644 pkg/services/provisioning/testdata/stub-configs/dashboards/single.yaml diff --git a/pkg/services/provisioning/stubs.go b/pkg/services/provisioning/stubs.go index b40e66aac5c..8e8b2078fbc 100644 --- a/pkg/services/provisioning/stubs.go +++ b/pkg/services/provisioning/stubs.go @@ -19,15 +19,19 @@ func ProvideStubProvisioningService(cfg *setting.Cfg) (StubProvisioningService, } func NewStubProvisioning(path string) (StubProvisioningService, error) { - cfgs, err := dashboards.ReadDashboardConfig(filepath.Join(path, "dashboards")) - if err != nil { - return nil, err - } + logger := log.New("provisioning.stub") stub := &stubProvisioning{ path: make(map[string]string), allowUIUpdates: make(map[string]bool), - log: log.New("provisioning.stub"), + log: logger, } + + cfgs, err := dashboards.ReadDashboardConfig(filepath.Join(path, "dashboards")) + if err != nil { + logger.Warn("can't read dashboard provisioning files from directory", "path", filepath.Join(path, "dashboards"), "error", err) + return stub, nil + } + for _, cfg := range cfgs { stub.path[cfg.Name] = cfg.Options["path"].(string) stub.allowUIUpdates[cfg.Name] = cfg.AllowUIUpdates diff --git a/pkg/services/provisioning/stubs_test.go b/pkg/services/provisioning/stubs_test.go new file mode 100644 index 00000000000..374434d9f59 --- /dev/null +++ b/pkg/services/provisioning/stubs_test.go @@ -0,0 +1,449 @@ +package provisioning + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/setting" +) + +func TestNewStubProvisioning(t *testing.T) { + t.Run("should handle non-existent directory gracefully", func(t *testing.T) { + // This tests the bug fix - should not fail when directory doesn't exist + stub, err := NewStubProvisioning("/non-existent-path") + + require.NoError(t, err, "should not return error for non-existent directory") + require.NotNil(t, stub, "should return valid stub") + + // Should return resolved empty path for non-existent configs + // Note: filepath.Abs("") returns current directory + path := stub.GetDashboardProvisionerResolvedPath("any-name") + assert.NotEmpty(t, path, "returns resolved empty path") + + assert.False(t, stub.GetAllowUIUpdatesFromConfig("any-name")) + }) + + t.Run("should handle empty directory", func(t *testing.T) { + testPath := "./testdata/stub-configs-empty" + stub, err := NewStubProvisioning(testPath) + + require.NoError(t, err) + require.NotNil(t, stub) + + // Empty directory should result in empty maps + // Note: filepath.Abs("") returns current directory + path := stub.GetDashboardProvisionerResolvedPath("any-name") + assert.NotEmpty(t, path, "returns resolved empty path") + + assert.False(t, stub.GetAllowUIUpdatesFromConfig("any-name")) + }) + + t.Run("should successfully read single config", func(t *testing.T) { + testPath := "./testdata/stub-configs" + stub, err := NewStubProvisioning(testPath) + + require.NoError(t, err) + require.NotNil(t, stub) + + // Should have the test-dashboard config + path := stub.GetDashboardProvisionerResolvedPath("test-dashboard") + assert.NotEmpty(t, path) + + allowUpdates := stub.GetAllowUIUpdatesFromConfig("test-dashboard") + assert.True(t, allowUpdates, "test-dashboard should allow UI updates") + }) + + t.Run("should successfully read multiple configs", func(t *testing.T) { + testPath := "./testdata/stub-configs-multiple" + stub, err := NewStubProvisioning(testPath) + + require.NoError(t, err) + require.NotNil(t, stub) + + // Check editable-dashboard (allowUiUpdates: true) + allowUpdates := stub.GetAllowUIUpdatesFromConfig("editable-dashboard") + assert.True(t, allowUpdates, "editable-dashboard should allow UI updates") + + // Check readonly-dashboard (allowUiUpdates: false) + allowUpdates = stub.GetAllowUIUpdatesFromConfig("readonly-dashboard") + assert.False(t, allowUpdates, "readonly-dashboard should not allow UI updates") + + // Check default-dashboard (allowUiUpdates not specified, should default to false) + allowUpdates = stub.GetAllowUIUpdatesFromConfig("default-dashboard") + assert.False(t, allowUpdates, "default-dashboard should default to not allowing UI updates") + + // Verify paths are set correctly + path := stub.GetDashboardProvisionerResolvedPath("editable-dashboard") + assert.NotEmpty(t, path) + + path = stub.GetDashboardProvisionerResolvedPath("readonly-dashboard") + assert.NotEmpty(t, path) + }) + + t.Run("should handle invalid YAML gracefully", func(t *testing.T) { + // Create a temporary directory with invalid YAML + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Write invalid YAML + invalidYAML := `this is not valid yaml: [[[` + err = os.WriteFile(filepath.Join(dashboardsDir, "invalid.yaml"), []byte(invalidYAML), 0644) + require.NoError(t, err) + + // Should handle gracefully by returning empty stub + _, err = NewStubProvisioning(tmpDir) + require.NoError(t, err) + }) +} + +func TestGetAllowUIUpdatesFromConfig(t *testing.T) { + t.Run("should return true for config with allowUiUpdates: true", func(t *testing.T) { + testPath := "./testdata/stub-configs-multiple" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + result := stub.GetAllowUIUpdatesFromConfig("editable-dashboard") + assert.True(t, result) + }) + + t.Run("should return false for config with allowUiUpdates: false", func(t *testing.T) { + testPath := "./testdata/stub-configs-multiple" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + result := stub.GetAllowUIUpdatesFromConfig("readonly-dashboard") + assert.False(t, result) + }) + + t.Run("should return false for non-existent config name", func(t *testing.T) { + testPath := "./testdata/stub-configs" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + result := stub.GetAllowUIUpdatesFromConfig("non-existent-config") + assert.False(t, result, "should return false for non-existent config") + }) + + t.Run("should return false when initialized with non-existent directory", func(t *testing.T) { + stub, err := NewStubProvisioning("/non-existent-path") + require.NoError(t, err) + + result := stub.GetAllowUIUpdatesFromConfig("any-name") + assert.False(t, result) + }) +} + +func TestGetDashboardProvisionerResolvedPath(t *testing.T) { + t.Run("should resolve valid path", func(t *testing.T) { + testPath := "./testdata/stub-configs" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + path := stub.GetDashboardProvisionerResolvedPath("test-dashboard") + assert.NotEmpty(t, path) + }) + + t.Run("should handle non-existent config name", func(t *testing.T) { + testPath := "./testdata/stub-configs" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + path := stub.GetDashboardProvisionerResolvedPath("non-existent") + // Returns resolved empty path (current directory) for non-existent config + assert.NotEmpty(t, path, "returns resolved path even for non-existent config") + }) + + t.Run("should handle non-existent path in config", func(t *testing.T) { + testPath := "./testdata/stub-configs" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + // The path in config is /tmp/test-dashboards which likely doesn't exist + // Should still return a path (with warning logged) + path := stub.GetDashboardProvisionerResolvedPath("test-dashboard") + assert.NotEmpty(t, path) + }) + + t.Run("should resolve relative paths to absolute", func(t *testing.T) { + // Create a temporary directory with a config that has a relative path + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Create a subdirectory for the dashboard path + dashPath := filepath.Join(tmpDir, "my-dashboards") + err = os.MkdirAll(dashPath, 0750) + require.NoError(t, err) + + // Write config with absolute path (relative paths in options are not resolved correctly by the stub) + configContent := `apiVersion: 1 +providers: +- name: 'relative-path' + orgId: 1 + type: file + options: + path: ` + dashPath + ` +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + path := stub.GetDashboardProvisionerResolvedPath("relative-path") + // Should be absolute path + assert.True(t, filepath.IsAbs(path), "path should be absolute") + assert.Contains(t, path, "my-dashboards") + }) + + t.Run("should handle symlinks", func(t *testing.T) { + // Create a temporary directory structure with symlink + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Create actual directory + actualDir := filepath.Join(tmpDir, "actual-dashboards") + err = os.MkdirAll(actualDir, 0750) + require.NoError(t, err) + + // Create symlink + symlinkPath := filepath.Join(tmpDir, "linked-dashboards") + err = os.Symlink(actualDir, symlinkPath) + if err != nil { + t.Skip("Cannot create symlinks on this system") + } + + // Write config with symlink path + configContent := `apiVersion: 1 +providers: +- name: 'symlink-path' + orgId: 1 + type: file + options: + path: ` + symlinkPath + ` +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + path := stub.GetDashboardProvisionerResolvedPath("symlink-path") + // Should resolve symlink to actual path + assert.NotEmpty(t, path) + + // EvalSymlinks resolves to the real path, which might include /private prefix on macOS + // Just verify it contains the actual directory name + assert.Contains(t, path, "actual-dashboards", "should resolve symlink to actual directory") + }) + + t.Run("should fallback to original path on EvalSymlinks failure", func(t *testing.T) { + // This is harder to test directly, but we can verify the fallback logic exists + // by checking that even with a broken symlink, we get a path back + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Create config with a path that will fail EvalSymlinks + configContent := `apiVersion: 1 +providers: +- name: 'broken-path' + orgId: 1 + type: file + options: + path: /tmp/test-stub-dashboards +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + path := stub.GetDashboardProvisionerResolvedPath("broken-path") + assert.NotEmpty(t, path, "should return fallback path even if EvalSymlinks fails") + }) +} + +func TestProvideStubProvisioningService(t *testing.T) { + t.Run("should create stub from config", func(t *testing.T) { + cfg := &setting.Cfg{ + ProvisioningPath: "./testdata/stub-configs", + } + + stub, err := ProvideStubProvisioningService(cfg) + require.NoError(t, err) + require.NotNil(t, stub) + + // Verify it works + path := stub.GetDashboardProvisionerResolvedPath("test-dashboard") + assert.NotEmpty(t, path) + }) + + t.Run("should handle non-existent provisioning path", func(t *testing.T) { + cfg := &setting.Cfg{ + ProvisioningPath: "/non-existent-provisioning-path", + } + + stub, err := ProvideStubProvisioningService(cfg) + require.NoError(t, err, "should not fail with non-existent path") + require.NotNil(t, stub) + }) +} + +func TestStubProvisioningEdgeCases(t *testing.T) { + t.Run("should handle empty config name", func(t *testing.T) { + testPath := "./testdata/stub-configs" + stub, err := NewStubProvisioning(testPath) + require.NoError(t, err) + + // Empty config name returns resolved empty path (current directory) + path := stub.GetDashboardProvisionerResolvedPath("") + assert.NotEmpty(t, path) + + allowUpdates := stub.GetAllowUIUpdatesFromConfig("") + assert.False(t, allowUpdates) + }) + + t.Run("should handle special characters in config name", func(t *testing.T) { + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + configContent := `apiVersion: 1 +providers: +- name: 'test-dashboard-with-special-chars-123' + orgId: 1 + type: file + allowUiUpdates: true + options: + path: /tmp/test +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + allowUpdates := stub.GetAllowUIUpdatesFromConfig("test-dashboard-with-special-chars-123") + assert.True(t, allowUpdates) + }) + + t.Run("should handle paths with spaces", func(t *testing.T) { + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Create directory with spaces + pathWithSpaces := filepath.Join(tmpDir, "my dashboard folder") + err = os.MkdirAll(pathWithSpaces, 0750) + require.NoError(t, err) + + configContent := `apiVersion: 1 +providers: +- name: 'space-test' + orgId: 1 + type: file + options: + path: "` + pathWithSpaces + `" +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(configContent), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + path := stub.GetDashboardProvisionerResolvedPath("space-test") + assert.NotEmpty(t, path) + assert.Contains(t, path, "dashboard folder") + }) + + t.Run("should handle multiple YAML files in directory", func(t *testing.T) { + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Create first config file + config1 := `apiVersion: 1 +providers: +- name: 'config1' + orgId: 1 + type: file + allowUiUpdates: true + options: + path: /tmp/config1 +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config1.yaml"), []byte(config1), 0644) + require.NoError(t, err) + + // Create second config file + config2 := `apiVersion: 1 +providers: +- name: 'config2' + orgId: 1 + type: file + allowUiUpdates: false + options: + path: /tmp/config2 +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config2.yaml"), []byte(config2), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + // Both configs should be loaded + assert.True(t, stub.GetAllowUIUpdatesFromConfig("config1")) + assert.False(t, stub.GetAllowUIUpdatesFromConfig("config2")) + + path1 := stub.GetDashboardProvisionerResolvedPath("config1") + path2 := stub.GetDashboardProvisionerResolvedPath("config2") + assert.NotEmpty(t, path1) + assert.NotEmpty(t, path2) + assert.NotEqual(t, path1, path2) + }) + + t.Run("should ignore non-YAML files", func(t *testing.T) { + tmpDir := t.TempDir() + dashboardsDir := filepath.Join(tmpDir, "dashboards") + err := os.MkdirAll(dashboardsDir, 0750) + require.NoError(t, err) + + // Create YAML config + yamlConfig := `apiVersion: 1 +providers: +- name: 'yaml-config' + orgId: 1 + type: file + options: + path: /tmp/test +` + err = os.WriteFile(filepath.Join(dashboardsDir, "config.yaml"), []byte(yamlConfig), 0644) + require.NoError(t, err) + + // Create non-YAML files + err = os.WriteFile(filepath.Join(dashboardsDir, "readme.txt"), []byte("test"), 0644) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(dashboardsDir, "config.json"), []byte("{}"), 0644) + require.NoError(t, err) + + stub, err := NewStubProvisioning(tmpDir) + require.NoError(t, err) + + // Should only load the YAML config + path := stub.GetDashboardProvisionerResolvedPath("yaml-config") + assert.NotEmpty(t, path) + }) +} diff --git a/pkg/services/provisioning/testdata/stub-configs-multiple/dashboards/multiple.yaml b/pkg/services/provisioning/testdata/stub-configs-multiple/dashboards/multiple.yaml new file mode 100644 index 00000000000..e36fc293380 --- /dev/null +++ b/pkg/services/provisioning/testdata/stub-configs-multiple/dashboards/multiple.yaml @@ -0,0 +1,24 @@ +apiVersion: 1 + +providers: +- name: 'editable-dashboard' + orgId: 1 + folder: 'test-folder' + type: file + allowUiUpdates: true + options: + path: /tmp/editable-dashboards + +- name: 'readonly-dashboard' + orgId: 2 + folder: 'readonly-folder' + type: file + allowUiUpdates: false + options: + path: /tmp/readonly-dashboards + +- name: 'default-dashboard' + orgId: 1 + type: file + options: + path: /tmp/default-dashboards diff --git a/pkg/services/provisioning/testdata/stub-configs/dashboards/single.yaml b/pkg/services/provisioning/testdata/stub-configs/dashboards/single.yaml new file mode 100644 index 00000000000..cfbf0a3f1c5 --- /dev/null +++ b/pkg/services/provisioning/testdata/stub-configs/dashboards/single.yaml @@ -0,0 +1,10 @@ +apiVersion: 1 + +providers: +- name: 'test-dashboard' + orgId: 1 + folder: '' + type: file + allowUiUpdates: true + options: + path: /tmp/test-dashboards From e7377a88423a8bb49e96839eb4596fd1dd3cfdb9 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:31:22 +0100 Subject: [PATCH 041/378] Alerting: Update docs for ash AI helper button (#114229) Update docs for ash AI helper button --- .../view-alert-state-history.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/sources/alerting/monitor-status/view-alert-state-history.md b/docs/sources/alerting/monitor-status/view-alert-state-history.md index 553ef0a8fac..156fd6c9f09 100644 --- a/docs/sources/alerting/monitor-status/view-alert-state-history.md +++ b/docs/sources/alerting/monitor-status/view-alert-state-history.md @@ -73,6 +73,37 @@ To access the History page, complete the following steps. {{< figure src="/media/docs/alerting/alerting-alert-history-tab.png" max-width="750px" alt="Alert History tab in Grafana Alerting" >}} +## Use Grafana Assistant to analyze alert state history + +{{< admonition type="note" >}} +This feature is available in Grafana Cloud when Grafana Assistant is enabled. +{{< /admonition >}} + +The **Analyze with Assistant** button provides AI-powered analysis of your alert history to help you understand and troubleshoot alert patterns. Located in the top-right corner of the History page event list, this button uses Grafana Assistant to analyze the events displayed in your current view. + +When you click the AI Triage button, the Grafana Assistant analyzes: + +- Alert state transitions over the selected time range +- Alert instance patterns and frequency +- Common labels and characteristics of firing alerts +- Temporal patterns in alert behavior + +The AI assistant can help you: + +- Identify root causes of alert storms +- Detect patterns in alert firing behavior +- Understand correlations between different alert instances +- Get suggestions for improving alert configurations + +To use the Analyze with Assistant feature: + +1. Navigate to the History page as described above. +2. Filter the events to focus on the alerts you want to analyze using labels, states, or time range. +3. Click the **Analyze with Assistant** button in the top-right corner of the event list. +4. Review the AI-generated analysis and recommendations. + +The AI analysis is based on the currently displayed events, so filtering your view to specific alerts or time periods will result in more focused insights. + ## View from the State history view Use the State history view to get insight into how your individual alert instances behave over time. From 42babc7ce78e7f38dc8d70ef0a4184ea702822d5 Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Fri, 21 Nov 2025 12:14:50 +0200 Subject: [PATCH 042/378] Select: Fix width bug when `maxVisibleValues` is set (#113913) * fix * fix --- packages/grafana-ui/src/components/Select/ValueContainer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-ui/src/components/Select/ValueContainer.tsx b/packages/grafana-ui/src/components/Select/ValueContainer.tsx index aa5cec83c19..6eb8326ae5d 100644 --- a/packages/grafana-ui/src/components/Select/ValueContainer.tsx +++ b/packages/grafana-ui/src/components/Select/ValueContainer.tsx @@ -26,6 +26,7 @@ class UnthemedValueContainer Date: Fri, 21 Nov 2025 04:40:29 -0600 Subject: [PATCH 043/378] Loki: fix status in body not matching http status code (#114201) * fix: status in body not matching http status code --- pkg/tsdb/loki/api.go | 6 ++++- pkg/tsdb/loki/api_test.go | 49 ++++++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index 464a267391a..eb7f72e6570 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -199,6 +199,7 @@ func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery, responseOpts res := backend.DataResponse{ Error: err, ErrorSource: backend.ErrorSourceFromHTTPStatus(resp.StatusCode), + Status: backend.Status(resp.StatusCode), } lp = append(lp, "status", "error", "error", err, "statusSource", res.ErrorSource) api.log.Debug("Error received from Loki", lp...) @@ -214,6 +215,7 @@ func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery, responseOpts iter := jsoniter.Parse(jsoniter.ConfigDefault, resp.Body, 1024) res := converter.ReadPrometheusStyleResult(iter, converter.Options{}) + res.Status = backend.Status(resp.StatusCode) if res.Error != nil { span.RecordError(res.Error) @@ -305,7 +307,9 @@ func (api *LokiAPI) RawQuery(ctx context.Context, resourcePath string) (RawLokiR } body, err = json.Marshal(lokiResponseErr) if err != nil { - return RawLokiResponse{}, err + return RawLokiResponse{ + Status: resp.StatusCode, + }, err } } diff --git a/pkg/tsdb/loki/api_test.go b/pkg/tsdb/loki/api_test.go index 252608780e6..29b15e7fb18 100644 --- a/pkg/tsdb/loki/api_test.go +++ b/pkg/tsdb/loki/api_test.go @@ -250,6 +250,7 @@ func TestApiReturnValues(t *testing.T) { require.True(t, called) require.Equal(t, "gzip", encodedBytes.Encoding) require.Equal(t, []byte("{\"message\":\"foo\"}"), encodedBytes.Body) + require.Equal(t, 400, encodedBytes.Status) }) t.Run("Loki should return the error as is", func(t *testing.T) { @@ -280,6 +281,18 @@ func TestApiReturnValues(t *testing.T) { require.Error(t, err) require.ErrorContains(t, err, "foo") }) + + t.Run("should set status for successful requests", func(t *testing.T) { + called := false + api := makeMockedAPI(200, "application/json", []byte("{\"message\":\"foo\"}"), func(req *http.Request) { + called = true + }) + + res, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryLogsVolume, QueryType: QueryTypeRange}, ResponseOpts{}) + require.NoError(t, err) + require.True(t, called) + require.Equal(t, backend.Status(http.StatusOK), res.Status) + }) } func TestErrorSources(t *testing.T) { @@ -287,7 +300,7 @@ func TestErrorSources(t *testing.T) { t.Run("should set correct error source for downstream errors", func(t *testing.T) { called := false - api := makeMockedAPI(400, "application/json", errorResponse, func(req *http.Request) { + api := makeMockedAPI(http.StatusBadRequest, "application/json", errorResponse, func(req *http.Request) { called = true }) @@ -296,11 +309,12 @@ func TestErrorSources(t *testing.T) { require.True(t, called) require.NotNil(t, res.Error) require.Equal(t, backend.ErrorSourceDownstream, res.ErrorSource) + require.Equal(t, backend.Status(http.StatusBadRequest), res.Status) }) t.Run("should set correct error source for plugin errors", func(t *testing.T) { called := false - api := makeMockedAPI(406, "application/json", errorResponse, func(req *http.Request) { + api := makeMockedAPI(http.StatusNotAcceptable, "application/json", errorResponse, func(req *http.Request) { called = true }) @@ -309,11 +323,12 @@ func TestErrorSources(t *testing.T) { require.True(t, called) require.NotNil(t, res.Error) require.Equal(t, backend.ErrorSourcePlugin, res.ErrorSource) + require.Equal(t, backend.Status(http.StatusNotAcceptable), res.Status) }) t.Run("should set correct error source for server errors", func(t *testing.T) { called := false - api := makeMockedAPI(500, "application/json", errorResponse, func(req *http.Request) { + api := makeMockedAPI(http.StatusInternalServerError, "application/json", errorResponse, func(req *http.Request) { called = true }) @@ -322,11 +337,26 @@ func TestErrorSources(t *testing.T) { require.True(t, called) require.NotNil(t, res.Error) require.Equal(t, backend.ErrorSourceDownstream, res.ErrorSource) + require.Equal(t, backend.Status(http.StatusInternalServerError), res.Status) + }) + + t.Run("should set correct error source for server timeout error", func(t *testing.T) { + called := false + api := makeMockedAPI(http.StatusGatewayTimeout, "application/json", errorResponse, func(req *http.Request) { + called = true + }) + + res, err := api.DataQuery(context.Background(), lokiQuery{QueryType: QueryTypeRange}, ResponseOpts{}) + require.NoError(t, err) + require.True(t, called) + require.NotNil(t, res.Error) + require.Equal(t, backend.ErrorSourceDownstream, res.ErrorSource) + require.Equal(t, backend.Status(http.StatusGatewayTimeout), res.Status) }) t.Run("should handle downstream HTTP errors", func(t *testing.T) { called := false - api := makeMockedAPI(400, "application/json", errorResponse, func(req *http.Request) { + api := makeMockedAPI(http.StatusBadRequest, "application/json", errorResponse, func(req *http.Request) { called = true }) @@ -336,30 +366,33 @@ func TestErrorSources(t *testing.T) { require.NotNil(t, res.Error) require.Equal(t, backend.ErrorSourceDownstream, res.ErrorSource) require.Contains(t, res.Error.Error(), "test error") + require.Equal(t, backend.Status(http.StatusBadRequest), res.Status) }) t.Run("should handle client errors in RawQuery", func(t *testing.T) { called := false - api := makeMockedAPI(400, "application/json", errorResponse, func(req *http.Request) { + api := makeMockedAPI(http.StatusBadRequest, "application/json", errorResponse, func(req *http.Request) { called = true }) res, err := api.RawQuery(context.Background(), "/loki/api/v1/labels") require.NoError(t, err) require.True(t, called) - require.Equal(t, 400, res.Status) + require.Equal(t, http.StatusBadRequest, res.Status) require.Contains(t, string(res.Body), "test error") }) t.Run("should handle server errors in RawQuery", func(t *testing.T) { called := false - api := makeMockedAPI(500, "application/json", errorResponse, func(req *http.Request) { + api := makeMockedAPI(http.StatusInternalServerError, "application/json", errorResponse, func(req *http.Request) { called = true }) - _, err := api.RawQuery(context.Background(), "/loki/api/v1/labels") + res, err := api.RawQuery(context.Background(), "/loki/api/v1/labels") require.Error(t, err) require.True(t, called) require.Contains(t, err.Error(), "test error") + // Status code of 0 gets mapped to InternalServerError (500) + require.Equal(t, 0, res.Status) }) } From e09905df35d0d163ae8597440ace156d8c636ce9 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Fri, 21 Nov 2025 11:41:03 +0100 Subject: [PATCH 044/378] SchemaV2: Add library panel repeat options to v2 schema during conversion (#114109) * Add library panel repeat options to v2 schema during conversion * use any instead of interface{} * change to common.Unstructured instead of byte[] for model field * Fix the tests and let the library panel behavior fetch repeat options in public and scripted dashboards * fix library panel differences between backend and frontend conversion --- .../pkg/migration/conversion/conversion.go | 12 +- .../migration/conversion/conversion_test.go | 27 ++- .../v1beta1.library-panel-repeat-options.json | 93 ++++++++++ ...library-panel-repeat-options.v0alpha1.json | 102 +++++++++++ ...library-panel-repeat-options.v2alpha1.json | 169 ++++++++++++++++++ ....library-panel-repeat-options.v2beta1.json | 169 ++++++++++++++++++ apps/dashboard/pkg/migration/conversion/v0.go | 8 +- .../pkg/migration/conversion/v0_test.go | 18 +- apps/dashboard/pkg/migration/conversion/v1.go | 8 +- .../pkg/migration/conversion/v1_test.go | 8 +- .../conversion/v1beta1_to_v2alpha1.go | 108 +++++++---- .../schemaversion/datasource_utils.go | 57 ++++++ .../pkg/migration/schemaversion/migrations.go | 3 + .../dashboard/pkg/migration/testutil/mocks.go | 99 ++++++++++ pkg/registry/apis/dashboard/datasources.go | 10 ++ .../scene/LibraryPanelBehavior.tsx | 17 +- .../serialization/serialization-test-utils.ts | 96 ++++++++++ .../transformSaveModelV1ToV2.test.ts | 23 ++- .../api/ResponseTransformersToBackend.test.ts | 8 +- 19 files changed, 962 insertions(+), 73 deletions(-) create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.library-panel-repeat-options.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v0alpha1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2alpha1.json create mode 100644 apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2beta1.json create mode 100644 public/app/features/dashboard-scene/serialization/serialization-test-utils.ts diff --git a/apps/dashboard/pkg/migration/conversion/conversion.go b/apps/dashboard/pkg/migration/conversion/conversion.go index 8d20dcd3837..7b614bc2656 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion.go +++ b/apps/dashboard/pkg/migration/conversion/conversion.go @@ -11,11 +11,13 @@ import ( "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) -func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSourceIndexProvider, _ schemaversion.LibraryElementIndexProvider) error { +func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { // Wrap the provider once with 10s caching for all conversions. // This prevents repeated DB queries across multiple conversion calls while allowing // the cache to refresh periodically, making it suitable for long-lived singleton usage. dsIndexProvider = schemaversion.WrapIndexProviderWithCache(dsIndexProvider) + // Wrap library element provider with caching as well + leIndexProvider = schemaversion.WrapLibraryElementProviderWithCache(leIndexProvider) // v0 conversions if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv1.Dashboard)(nil), @@ -26,13 +28,13 @@ func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSo } if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil), withConversionMetrics(dashv0.APIVERSION, dashv2alpha1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V0_to_V2alpha1(a.(*dashv0.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsIndexProvider) + return Convert_V0_to_V2alpha1(a.(*dashv0.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsIndexProvider, leIndexProvider) })); err != nil { return err } if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2beta1.Dashboard)(nil), withConversionMetrics(dashv0.APIVERSION, dashv2beta1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V0_to_V2beta1(a.(*dashv0.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsIndexProvider) + return Convert_V0_to_V2beta1(a.(*dashv0.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsIndexProvider, leIndexProvider) })); err != nil { return err } @@ -46,13 +48,13 @@ func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSo } if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil), withConversionMetrics(dashv1.APIVERSION, dashv2alpha1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V1beta1_to_V2alpha1(a.(*dashv1.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsIndexProvider) + return Convert_V1beta1_to_V2alpha1(a.(*dashv1.Dashboard), b.(*dashv2alpha1.Dashboard), scope, dsIndexProvider, leIndexProvider) })); err != nil { return err } if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2beta1.Dashboard)(nil), withConversionMetrics(dashv1.APIVERSION, dashv2beta1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V1beta1_to_V2beta1(a.(*dashv1.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsIndexProvider) + return Convert_V1beta1_to_V2beta1(a.(*dashv1.Dashboard), b.(*dashv2beta1.Dashboard), scope, dsIndexProvider, leIndexProvider) })); err != nil { return err } diff --git a/apps/dashboard/pkg/migration/conversion/conversion_test.go b/apps/dashboard/pkg/migration/conversion/conversion_test.go index e7cbe2f70ef..5f334b98354 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_test.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_test.go @@ -33,7 +33,8 @@ import ( func TestConversionMatrixExist(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) versions := []metav1.Object{ @@ -86,7 +87,8 @@ func TestDeepCopyValid(t *testing.T) { func TestDashboardConversionToAllVersions(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) // Set up conversion scheme @@ -246,7 +248,8 @@ func TestDashboardConversionToAllVersions(t *testing.T) { func TestMigratedDashboardsConversion(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) // Set up conversion scheme @@ -381,7 +384,8 @@ func testConversion(t *testing.T, convertedDash metav1.Object, filename, outputD func TestConversionMetrics(t *testing.T) { // Initialize migration with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) // Create a test registry for metrics @@ -509,7 +513,8 @@ func TestConversionMetrics(t *testing.T) { // TestConversionMetricsWrapper tests the withConversionMetrics wrapper function func TestConversionMetricsWrapper(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) // Create a test registry for metrics @@ -678,7 +683,8 @@ func TestSchemaVersionExtraction(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Test the schema version extraction logic by creating a wrapper and checking the metrics labels dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) // Create a test registry for metrics @@ -723,7 +729,8 @@ func TestSchemaVersionExtraction(t *testing.T) { // TestConversionLogging tests that conversion-level logging works correctly func TestConversionLogging(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) // Create a test registry for metrics @@ -815,7 +822,8 @@ func TestConversionLogging(t *testing.T) { // TestConversionLogLevels tests that appropriate log levels are used func TestConversionLogLevels(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) t.Run("log levels and structured fields verification", func(t *testing.T) { @@ -887,7 +895,8 @@ func TestConversionLogLevels(t *testing.T) { // TestConversionLoggingFields tests that all expected fields are included in log messages func TestConversionLoggingFields(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() + // Use TestLibraryElementProvider for tests that need library panel models with repeat options + leProvider := migrationtestutil.NewTestLibraryElementProvider() migration.Initialize(dsProvider, leProvider) t.Run("verify all log fields are present", func(t *testing.T) { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.library-panel-repeat-options.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.library-panel-repeat-options.json new file mode 100644 index 00000000000..ec440d2894b --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.library-panel-repeat-options.json @@ -0,0 +1,93 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "library-panel-repeat-options-test", + "labels": { + "test": "library-panel-repeat" + } + }, + "spec": { + "title": "Library Panel Repeat Options Test Dashboard", + "description": "Testing library panel repeat options migration from v1beta1 to v2alpha1", + "tags": ["test", "library-panels", "repeat"], + "schemaVersion": 38, + "panels": [ + { + "id": 1, + "title": "Library Panel with Horizontal Repeat", + "type": "library-panel-ref", + "gridPos": { + "x": 0, + "y": 0, + "w": 12, + "h": 8 + }, + "libraryPanel": { + "uid": "lib-panel-repeat-h", + "name": "Library Panel with Horizontal Repeat" + } + }, + { + "id": 2, + "title": "Library Panel with Vertical Repeat", + "type": "library-panel-ref", + "gridPos": { + "x": 0, + "y": 8, + "w": 6, + "h": 4 + }, + "libraryPanel": { + "uid": "lib-panel-repeat-v", + "name": "Library Panel with Vertical Repeat" + } + }, + { + "id": 3, + "title": "Library Panel Instance Override", + "type": "library-panel-ref", + "gridPos": { + "x": 6, + "y": 8, + "w": 12, + "h": 8 + }, + "libraryPanel": { + "uid": "lib-panel-repeat-h", + "name": "Library Panel with Horizontal Repeat" + }, + "repeat": "instance-var", + "repeatDirection": "v", + "maxPerRow": 5 + }, + { + "id": 4, + "title": "Library Panel without Repeat", + "type": "library-panel-ref", + "gridPos": { + "x": 0, + "y": 12, + "w": 6, + "h": 3 + }, + "libraryPanel": { + "uid": "lib-panel-no-repeat", + "name": "Library Panel without Repeat" + } + } + ], + "time": { + "from": "now-1h", + "to": "now" + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "links": [] + } +} + diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v0alpha1.json new file mode 100644 index 00000000000..4c9f4fc2eaf --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v0alpha1.json @@ -0,0 +1,102 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "library-panel-repeat-options-test", + "labels": { + "test": "library-panel-repeat" + } + }, + "spec": { + "annotations": { + "list": [] + }, + "description": "Testing library panel repeat options migration from v1beta1 to v2alpha1", + "links": [], + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "libraryPanel": { + "name": "Library Panel with Horizontal Repeat", + "uid": "lib-panel-repeat-h" + }, + "title": "Library Panel with Horizontal Repeat", + "type": "library-panel-ref" + }, + { + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 8 + }, + "id": 2, + "libraryPanel": { + "name": "Library Panel with Vertical Repeat", + "uid": "lib-panel-repeat-v" + }, + "title": "Library Panel with Vertical Repeat", + "type": "library-panel-ref" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 6, + "y": 8 + }, + "id": 3, + "libraryPanel": { + "name": "Library Panel with Horizontal Repeat", + "uid": "lib-panel-repeat-h" + }, + "maxPerRow": 5, + "repeat": "instance-var", + "repeatDirection": "v", + "title": "Library Panel Instance Override", + "type": "library-panel-ref" + }, + { + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 12 + }, + "id": 4, + "libraryPanel": { + "name": "Library Panel without Repeat", + "uid": "lib-panel-no-repeat" + }, + "title": "Library Panel without Repeat", + "type": "library-panel-ref" + } + ], + "schemaVersion": 38, + "tags": [ + "test", + "library-panels", + "repeat" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "title": "Library Panel Repeat Options Test Dashboard" + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v1beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2alpha1.json new file mode 100644 index 00000000000..a614f32dda6 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2alpha1.json @@ -0,0 +1,169 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "library-panel-repeat-options-test", + "labels": { + "test": "library-panel-repeat" + } + }, + "spec": { + "annotations": [], + "cursorSync": "Off", + "description": "Testing library panel repeat options migration from v1beta1 to v2alpha1", + "editable": true, + "elements": { + "panel-1": { + "kind": "LibraryPanel", + "spec": { + "id": 1, + "title": "Library Panel with Horizontal Repeat", + "libraryPanel": { + "name": "Library Panel with Horizontal Repeat", + "uid": "lib-panel-repeat-h" + } + } + }, + "panel-2": { + "kind": "LibraryPanel", + "spec": { + "id": 2, + "title": "Library Panel with Vertical Repeat", + "libraryPanel": { + "name": "Library Panel with Vertical Repeat", + "uid": "lib-panel-repeat-v" + } + } + }, + "panel-3": { + "kind": "LibraryPanel", + "spec": { + "id": 3, + "title": "Library Panel Instance Override", + "libraryPanel": { + "name": "Library Panel with Horizontal Repeat", + "uid": "lib-panel-repeat-h" + } + } + }, + "panel-4": { + "kind": "LibraryPanel", + "spec": { + "id": 4, + "title": "Library Panel without Repeat", + "libraryPanel": { + "name": "Library Panel without Repeat", + "uid": "lib-panel-no-repeat" + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "repeat": { + "mode": "variable", + "value": "server", + "direction": "h", + "maxPerRow": 3 + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-2" + }, + "repeat": { + "mode": "variable", + "value": "datacenter", + "direction": "v" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 6, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + }, + "repeat": { + "mode": "variable", + "value": "instance-var", + "direction": "v", + "maxPerRow": 5 + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 12, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "test", + "library-panels", + "repeat" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-1h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Library Panel Repeat Options Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2beta1.json new file mode 100644 index 00000000000..d06d12848e6 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.library-panel-repeat-options.v2beta1.json @@ -0,0 +1,169 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "library-panel-repeat-options-test", + "labels": { + "test": "library-panel-repeat" + } + }, + "spec": { + "annotations": [], + "cursorSync": "Off", + "description": "Testing library panel repeat options migration from v1beta1 to v2alpha1", + "editable": true, + "elements": { + "panel-1": { + "kind": "LibraryPanel", + "spec": { + "id": 1, + "title": "Library Panel with Horizontal Repeat", + "libraryPanel": { + "name": "Library Panel with Horizontal Repeat", + "uid": "lib-panel-repeat-h" + } + } + }, + "panel-2": { + "kind": "LibraryPanel", + "spec": { + "id": 2, + "title": "Library Panel with Vertical Repeat", + "libraryPanel": { + "name": "Library Panel with Vertical Repeat", + "uid": "lib-panel-repeat-v" + } + } + }, + "panel-3": { + "kind": "LibraryPanel", + "spec": { + "id": 3, + "title": "Library Panel Instance Override", + "libraryPanel": { + "name": "Library Panel with Horizontal Repeat", + "uid": "lib-panel-repeat-h" + } + } + }, + "panel-4": { + "kind": "LibraryPanel", + "spec": { + "id": 4, + "title": "Library Panel without Repeat", + "libraryPanel": { + "name": "Library Panel without Repeat", + "uid": "lib-panel-no-repeat" + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "repeat": { + "mode": "variable", + "value": "server", + "direction": "h", + "maxPerRow": 3 + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 8, + "width": 6, + "height": 4, + "element": { + "kind": "ElementReference", + "name": "panel-2" + }, + "repeat": { + "mode": "variable", + "value": "datacenter", + "direction": "v" + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 6, + "y": 8, + "width": 12, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-3" + }, + "repeat": { + "mode": "variable", + "value": "instance-var", + "direction": "v", + "maxPerRow": 5 + } + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 12, + "width": 6, + "height": 3, + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "test", + "library-panels", + "repeat" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-1h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Library Panel Repeat Options Test Dashboard", + "variables": [] + }, + "status": {} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/v0.go b/apps/dashboard/pkg/migration/conversion/v0.go index e182a71a52a..826293d9564 100644 --- a/apps/dashboard/pkg/migration/conversion/v0.go +++ b/apps/dashboard/pkg/migration/conversion/v0.go @@ -25,7 +25,7 @@ func Convert_V0_to_V1beta1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope co return nil } -func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { v1beta1 := &dashv1.Dashboard{} if err := ConvertDashboard_V0_to_V1beta1(in, v1beta1, scope); err != nil { out.Status = dashv2alpha1.DashboardStatus{ @@ -48,7 +48,7 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s return nil } - if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, out, scope, dsIndexProvider); err != nil { + if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, out, scope, dsIndexProvider, leIndexProvider); err != nil { out.Status = dashv2alpha1.DashboardStatus{ Conversion: &dashv2alpha1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv0.VERSION), @@ -72,7 +72,7 @@ func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, s return nil } -func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { v1beta1 := &dashv1.Dashboard{} if err := ConvertDashboard_V0_to_V1beta1(in, v1beta1, scope); err != nil { out.Status = dashv2beta1.DashboardStatus{ @@ -86,7 +86,7 @@ func Convert_V0_to_V2beta1(in *dashv0.Dashboard, out *dashv2beta1.Dashboard, sco } v2alpha1 := &dashv2alpha1.Dashboard{} - if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, v2alpha1, scope, dsIndexProvider); err != nil { + if err := ConvertDashboard_V1beta1_to_V2alpha1(v1beta1, v2alpha1, scope, dsIndexProvider, leIndexProvider); err != nil { out.Status = dashv2beta1.DashboardStatus{ Conversion: &dashv2beta1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv0.VERSION), diff --git a/apps/dashboard/pkg/migration/conversion/v0_test.go b/apps/dashboard/pkg/migration/conversion/v0_test.go index 08bf1c550c9..ca2210f01e8 100644 --- a/apps/dashboard/pkg/migration/conversion/v0_test.go +++ b/apps/dashboard/pkg/migration/conversion/v0_test.go @@ -109,9 +109,9 @@ func TestV0ConversionErrorHandling(t *testing.T) { case *dashv1.Dashboard: err = Convert_V0_to_V1beta1(tt.source, target, nil) case *dashv2alpha1.Dashboard: - err = Convert_V0_to_V2alpha1(tt.source, target, nil, dsProvider) + err = Convert_V0_to_V2alpha1(tt.source, target, nil, dsProvider, leProvider) case *dashv2beta1.Dashboard: - err = Convert_V0_to_V2beta1(tt.source, target, nil, dsProvider) + err = Convert_V0_to_V2beta1(tt.source, target, nil, dsProvider, leProvider) default: t.Fatalf("unexpected target type: %T", target) } @@ -192,7 +192,7 @@ func TestV0ConversionErrorPropagation(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V0_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V0_to_V2beta1(source, target, nil, dsProvider, leProvider) require.Error(t, err, "expected error to be returned on first step failure") require.NotNil(t, target.Status.Conversion) @@ -243,7 +243,7 @@ func TestV0ConversionSuccessPaths(t *testing.T) { } target := &dashv2alpha1.Dashboard{} - err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider) + err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider, leProvider) require.NoError(t, err, "expected successful conversion") // Layout should be set even on success @@ -264,7 +264,7 @@ func TestV0ConversionSuccessPaths(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V0_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V0_to_V2beta1(source, target, nil, dsProvider, leProvider) require.NoError(t, err, "expected successful conversion") }) @@ -293,7 +293,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { } target := &dashv2alpha1.Dashboard{} - err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider) + err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider, leProvider) // Convert_V0_to_V2alpha1 doesn't return error, just sets status require.NoError(t, err, "Convert_V0_to_V2alpha1 doesn't return error") @@ -327,7 +327,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { } target := &dashv2alpha1.Dashboard{} - err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider) + err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider, leProvider) // Convert_V0_to_V2alpha1 doesn't return error, just sets status require.NoError(t, err, "Convert_V0_to_V2alpha1 doesn't return error") @@ -357,7 +357,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V0_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V0_to_V2beta1(source, target, nil, dsProvider, leProvider) // May or may not error depending on dashboard content // But if it does error on second step, status should be set @@ -383,7 +383,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V0_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V0_to_V2beta1(source, target, nil, dsProvider, leProvider) // May or may not error depending on dashboard content // But if it does error on third step, status should be set diff --git a/apps/dashboard/pkg/migration/conversion/v1.go b/apps/dashboard/pkg/migration/conversion/v1.go index 48fdbf93b36..040a9a78741 100644 --- a/apps/dashboard/pkg/migration/conversion/v1.go +++ b/apps/dashboard/pkg/migration/conversion/v1.go @@ -25,8 +25,8 @@ func Convert_V1beta1_to_V0(in *dashv1.Dashboard, out *dashv0.Dashboard, scope co return nil } -func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { - if err := ConvertDashboard_V1beta1_to_V2alpha1(in, out, scope, dsIndexProvider); err != nil { +func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { + if err := ConvertDashboard_V1beta1_to_V2alpha1(in, out, scope, dsIndexProvider, leIndexProvider); err != nil { out.Status = dashv2alpha1.DashboardStatus{ Conversion: &dashv2alpha1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv1.VERSION), @@ -60,9 +60,9 @@ func Convert_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboa return nil } -func Convert_V1beta1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func Convert_V1beta1_to_V2beta1(in *dashv1.Dashboard, out *dashv2beta1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { v2alpha1 := &dashv2alpha1.Dashboard{} - if err := ConvertDashboard_V1beta1_to_V2alpha1(in, v2alpha1, scope, dsIndexProvider); err != nil { + if err := ConvertDashboard_V1beta1_to_V2alpha1(in, v2alpha1, scope, dsIndexProvider, leIndexProvider); err != nil { out.Status = dashv2beta1.DashboardStatus{ Conversion: &dashv2beta1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv1.VERSION), diff --git a/apps/dashboard/pkg/migration/conversion/v1_test.go b/apps/dashboard/pkg/migration/conversion/v1_test.go index c0a9d2e2e6b..4525e974e01 100644 --- a/apps/dashboard/pkg/migration/conversion/v1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v1_test.go @@ -37,7 +37,7 @@ func TestV1ConversionErrorHandling(t *testing.T) { } target := &dashv2alpha1.Dashboard{} - err := Convert_V1beta1_to_V2alpha1(source, target, nil, dsProvider) + err := Convert_V1beta1_to_V2alpha1(source, target, nil, dsProvider, leProvider) // Convert_V1beta1_to_V2alpha1 doesn't return error, just sets status require.NoError(t, err, "Convert_V1beta1_to_V2alpha1 doesn't return error") @@ -64,7 +64,7 @@ func TestV1ConversionErrorHandling(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider, leProvider) // May or may not error depending on dashboard content // But if it does error on first step, status should be set with correct StoredVersion @@ -91,7 +91,7 @@ func TestV1ConversionErrorHandling(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider, leProvider) // May or may not error depending on dashboard content // But if it does error on second step, status should be set with correct StoredVersion @@ -117,7 +117,7 @@ func TestV1ConversionErrorHandling(t *testing.T) { } target := &dashv2beta1.Dashboard{} - err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider) + err := Convert_V1beta1_to_V2beta1(source, target, nil, dsProvider, leProvider) // Should succeed if dashboard is valid if err == nil { diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index 21877cb6361..d20855a9509 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -80,7 +80,7 @@ func prepareV1beta1ConversionContext(in *dashv1.Dashboard, dsIndexProvider schem return ctx, &nsInfo, nil } -func ConvertDashboard_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func ConvertDashboard_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { out.ObjectMeta = in.ObjectMeta out.APIVersion = dashv2alpha1.APIVERSION out.Kind = in.Kind @@ -94,10 +94,10 @@ func ConvertDashboard_V1beta1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha return fmt.Errorf("failed to prepare conversion context: %w", err) } - return convertDashboardSpec_V1beta1_to_V2alpha1(&in.Spec, &out.Spec, scope, ctx, dsIndexProvider) + return convertDashboardSpec_V1beta1_to_V2alpha1(&in.Spec, &out.Spec, scope, ctx, dsIndexProvider, leIndexProvider) } -func convertDashboardSpec_V1beta1_to_V2alpha1(in *dashv1.DashboardSpec, out *dashv2alpha1.DashboardSpec, scope conversion.Scope, ctx context.Context, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func convertDashboardSpec_V1beta1_to_V2alpha1(in *dashv1.DashboardSpec, out *dashv2alpha1.DashboardSpec, scope conversion.Scope, ctx context.Context, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { // Parse the unstructured spec into a dashboard JSON structure dashboardJSON, ok := in.Object["dashboard"] if !ok { @@ -161,7 +161,7 @@ func convertDashboardSpec_V1beta1_to_V2alpha1(in *dashv1.DashboardSpec, out *das out.Links = transformLinks(dashboard) // Transform panels to elements and layout - elements, layout, err := transformPanelsToElementsAndLayout(ctx, dashboard, dsIndexProvider) + elements, layout, err := transformPanelsToElementsAndLayout(ctx, dashboard, dsIndexProvider, leIndexProvider) if err != nil { return fmt.Errorf("failed to transform panels: %w", err) } @@ -387,7 +387,7 @@ func transformLinks(dashboard map[string]interface{}) []dashv2alpha1.DashboardDa // Panel transformation constants const GRID_ROW_HEIGHT = 1 -func transformPanelsToElementsAndLayout(ctx context.Context, dashboard map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { +func transformPanelsToElementsAndLayout(ctx context.Context, dashboard map[string]interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { panels, ok := dashboard["panels"].([]interface{}) if !ok { // Return empty elements and default grid layout @@ -415,13 +415,13 @@ func transformPanelsToElementsAndLayout(ctx context.Context, dashboard map[strin } if hasRowPanels { - return convertToRowsLayout(ctx, panels, dsIndexProvider) + return convertToRowsLayout(ctx, panels, dsIndexProvider, leIndexProvider) } - return convertToGridLayout(ctx, panels, dsIndexProvider) + return convertToGridLayout(ctx, panels, dsIndexProvider, leIndexProvider) } -func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { +func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { elements := make(map[string]dashv2alpha1.DashboardElement) items := make([]dashv2alpha1.DashboardGridLayoutItemKind, 0, len(panels)) @@ -437,7 +437,7 @@ func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvi } elements[elementName] = element - items = append(items, buildGridItemKind(panelMap, elementName, nil)) + items = append(items, buildGridItemKind(ctx, panelMap, elementName, nil, leIndexProvider)) } layout := dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ @@ -452,7 +452,7 @@ func convertToGridLayout(ctx context.Context, panels []interface{}, dsIndexProvi return elements, layout, nil } -func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { +func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) (map[string]dashv2alpha1.DashboardElement, dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, error) { elements := make(map[string]dashv2alpha1.DashboardElement) rows := make([]dashv2alpha1.DashboardRowsLayoutRowKind, 0) @@ -491,7 +491,7 @@ func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvi element, name, err := buildElement(ctx, collapsedPanelMap, dsIndexProvider) if err == nil { elements[name] = element - rowElements = append(rowElements, buildGridItemKind(collapsedPanelMap, name, int64Ptr(yOffsetInRows(collapsedPanelMap, legacyRowY)))) + rowElements = append(rowElements, buildGridItemKind(ctx, collapsedPanelMap, name, int64Ptr(yOffsetInRows(collapsedPanelMap, legacyRowY)), leIndexProvider)) } } } @@ -512,7 +512,7 @@ func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvi if currentRow.Spec.Layout.GridLayoutKind != nil { currentRow.Spec.Layout.GridLayoutKind.Spec.Items = append( currentRow.Spec.Layout.GridLayoutKind.Spec.Items, - buildGridItemKind(panelMap, elementName, int64Ptr(yOffsetInRows(panelMap, legacyRowY))), + buildGridItemKind(ctx, panelMap, elementName, int64Ptr(yOffsetInRows(panelMap, legacyRowY)), leIndexProvider), ) } } else { @@ -521,7 +521,7 @@ func convertToRowsLayout(ctx context.Context, panels []interface{}, dsIndexProvi // The Y position does not matter for the rows layout, but it's used to calculate the position of the panels in the grid layout in the row. legacyRowY = -1 gridItems := []dashv2alpha1.DashboardGridLayoutItemKind{ - buildGridItemKind(panelMap, elementName, int64Ptr(0)), + buildGridItemKind(ctx, panelMap, elementName, int64Ptr(0), leIndexProvider), } hideHeader := true @@ -645,7 +645,7 @@ func buildPanelKind(ctx context.Context, panelMap map[string]interface{}, dsInde return panelKind, nil } -func buildGridItemKind(panelMap map[string]interface{}, elementName string, yOverride *int64) dashv2alpha1.DashboardGridLayoutItemKind { +func buildGridItemKind(ctx context.Context, panelMap map[string]interface{}, elementName string, yOverride *int64, leIndexProvider schemaversion.LibraryElementIndexProvider) dashv2alpha1.DashboardGridLayoutItemKind { // Default grid position (matches frontend PanelModel defaults: w=6, h=3) x, y, width, height := int64(0), int64(0), int64(6), int64(3) @@ -677,34 +677,78 @@ func buildGridItemKind(panelMap map[string]interface{}, elementName string, yOve } // Handle repeat options - if repeat := schemaversion.GetStringValue(panelMap, "repeat"); repeat != "" { - repeatOptions := &dashv2alpha1.DashboardRepeatOptions{ - Mode: "variable", - Value: repeat, - } + // First check if repeat options are set on the panel itself (dashboard instance level) + repeatOptions := getRepeatOptionsFromPanel(panelMap) - if repeatDirection := schemaversion.GetStringValue(panelMap, "repeatDirection"); repeatDirection != "" { - switch repeatDirection { - case "h": - direction := dashv2alpha1.DashboardRepeatOptionsDirectionH - repeatOptions.Direction = &direction - case "v": - direction := dashv2alpha1.DashboardRepeatOptionsDirectionV - repeatOptions.Direction = &direction + // If no repeat options on the panel and it's a library panel, try to get them from the library panel definition + if repeatOptions == nil { + if libraryPanel, ok := panelMap["libraryPanel"].(map[string]interface{}); ok { + libraryPanelUID := schemaversion.GetStringValue(libraryPanel, "uid") + if libraryPanelUID != "" && leIndexProvider != nil { + repeatOptions = getRepeatOptionsFromLibraryPanel(ctx, libraryPanelUID, leIndexProvider) } } + } - if maxPerRow := getIntField(panelMap, "maxPerRow", 0); maxPerRow > 0 { - maxPerRowInt64 := int64(maxPerRow) - repeatOptions.MaxPerRow = &maxPerRowInt64 - } - + if repeatOptions != nil { item.Spec.Repeat = repeatOptions } return item } +// getRepeatOptionsFromPanel extracts repeat options from a panel map (dashboard instance level) +func getRepeatOptionsFromPanel(panelMap map[string]any) *dashv2alpha1.DashboardRepeatOptions { + repeat := schemaversion.GetStringValue(panelMap, "repeat") + if repeat == "" { + return nil + } + + repeatOptions := &dashv2alpha1.DashboardRepeatOptions{ + Mode: "variable", + Value: repeat, + } + + if repeatDirection := schemaversion.GetStringValue(panelMap, "repeatDirection"); repeatDirection != "" { + switch repeatDirection { + case "h": + direction := dashv2alpha1.DashboardRepeatOptionsDirectionH + repeatOptions.Direction = &direction + case "v": + direction := dashv2alpha1.DashboardRepeatOptionsDirectionV + repeatOptions.Direction = &direction + } + } + + if maxPerRow := getIntField(panelMap, "maxPerRow", 0); maxPerRow > 0 { + maxPerRowInt64 := int64(maxPerRow) + repeatOptions.MaxPerRow = &maxPerRowInt64 + } + + return repeatOptions +} + +// getRepeatOptionsFromLibraryPanel retrieves repeat options from a library panel by UID +func getRepeatOptionsFromLibraryPanel(ctx context.Context, libraryPanelUID string, leIndexProvider schemaversion.LibraryElementIndexProvider) *dashv2alpha1.DashboardRepeatOptions { + libraryElements := leIndexProvider.GetLibraryElementInfo(ctx) + + // Find the library panel by UID + var libraryPanelModel map[string]any + for _, elem := range libraryElements { + if elem.UID == libraryPanelUID { + libraryPanelModel = elem.Model.Object + break + } + } + + if libraryPanelModel == nil { + return nil + } + + // Extract repeat options from the library panel model + return getRepeatOptionsFromPanel(libraryPanelModel) +} + func buildRowKind(rowPanelMap map[string]interface{}, elements []dashv2alpha1.DashboardGridLayoutItemKind) *dashv2alpha1.DashboardRowsLayoutRowKind { collapsed := getBoolField(rowPanelMap, "collapsed", false) title := schemaversion.GetStringValue(rowPanelMap, "title") diff --git a/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go b/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go index cc4e388bca3..18bc2fc17c4 100644 --- a/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go +++ b/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go @@ -216,3 +216,60 @@ func MigrateDatasourceNameToRef(nameOrRef interface{}, options map[string]bool, return nil } + +// cachedLibraryElementProvider wraps a LibraryElementIndexProvider with time-based caching. +// This prevents multiple DB queries during operations that may call GetLibraryElementInfo() +// multiple times (e.g., dashboard conversions with many library panel lookups). +// The cache expires after 10 seconds, allowing it to be used as a long-lived singleton +// while still refreshing periodically. +// +// Thread-safe: Uses sync.RWMutex to guarantee safe concurrent access. +type cachedLibraryElementProvider struct { + provider LibraryElementIndexProvider + mu sync.RWMutex + elements []LibraryElementInfo + cachedAt time.Time + cacheTTL time.Duration +} + +// GetLibraryElementInfo returns the cached library elements if they're still valid (< 10s old), otherwise rebuilds the cache. +// Uses RWMutex for efficient concurrent reads when cache is valid. +func (p *cachedLibraryElementProvider) GetLibraryElementInfo(ctx context.Context) []LibraryElementInfo { + // Fast path: check if cache is still valid using read lock + p.mu.RLock() + if p.elements != nil && time.Since(p.cachedAt) < p.cacheTTL { + elements := p.elements + p.mu.RUnlock() + return elements + } + p.mu.RUnlock() + + // Slow path: cache expired or not yet built, acquire write lock + p.mu.Lock() + defer p.mu.Unlock() + + // Double-check: another goroutine might have refreshed the cache + // while we were waiting for the write lock + if p.elements != nil && time.Since(p.cachedAt) < p.cacheTTL { + return p.elements + } + + // Rebuild the cache + p.elements = p.provider.GetLibraryElementInfo(ctx) + p.cachedAt = time.Now() + return p.elements +} + +// WrapLibraryElementProviderWithCache wraps a provider to cache library elements with a 10-second TTL. +// Useful for conversions or migrations that may call GetLibraryElementInfo() multiple times. +// The cache expires after 10 seconds, making it suitable for use as a long-lived singleton +// at the top level of dependency injection while still refreshing periodically. +func WrapLibraryElementProviderWithCache(provider LibraryElementIndexProvider) LibraryElementIndexProvider { + if provider == nil { + return nil + } + return &cachedLibraryElementProvider{ + provider: provider, + cacheTTL: 10 * time.Second, + } +} diff --git a/apps/dashboard/pkg/migration/schemaversion/migrations.go b/apps/dashboard/pkg/migration/schemaversion/migrations.go index 3e55b25b55c..ce786fa0dc0 100644 --- a/apps/dashboard/pkg/migration/schemaversion/migrations.go +++ b/apps/dashboard/pkg/migration/schemaversion/migrations.go @@ -3,6 +3,8 @@ package schemaversion import ( "context" "strconv" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( @@ -34,6 +36,7 @@ type LibraryElementInfo struct { Type string Description string FolderUID string + Model common.Unstructured // JSON model of the library element, used to extract repeat options during migration } type LibraryElementIndexProvider interface { diff --git a/apps/dashboard/pkg/migration/testutil/mocks.go b/apps/dashboard/pkg/migration/testutil/mocks.go index 6c8b4709137..6781dcecfa1 100644 --- a/apps/dashboard/pkg/migration/testutil/mocks.go +++ b/apps/dashboard/pkg/migration/testutil/mocks.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) // EmptyLibraryElementProvider provides an empty library element list for tests @@ -187,3 +188,101 @@ func (p *ConfigurableDataSourceProvider) getDevDashboardDataSources() []schemave }, } } + +// TestLibraryElementProvider provides library elements with models for testing repeat options migration +type TestLibraryElementProvider struct { + elements []schemaversion.LibraryElementInfo +} + +// NewTestLibraryElementProvider creates a new test library element provider with sample library panels +func NewTestLibraryElementProvider() *TestLibraryElementProvider { + // Create library panel models with repeat options + libPanelWithRepeatH := map[string]any{ + "id": 1, + "type": "timeseries", + "title": "Library Panel with Horizontal Repeat", + "repeat": "server", + "repeatDirection": "h", + "maxPerRow": 3, + "gridPos": map[string]any{ + "x": 0, + "y": 0, + "w": 12, + "h": 8, + }, + "targets": []any{}, + "options": map[string]any{}, + } + + libPanelWithRepeatV := map[string]any{ + "id": 2, + "type": "stat", + "title": "Library Panel with Vertical Repeat", + "repeat": "datacenter", + "repeatDirection": "v", + "gridPos": map[string]any{ + "x": 0, + "y": 0, + "w": 6, + "h": 4, + }, + "targets": []any{}, + "options": map[string]any{}, + } + + libPanelWithoutRepeat := map[string]any{ + "id": 3, + "type": "text", + "title": "Library Panel without Repeat", + "gridPos": map[string]any{ + "x": 0, + "y": 0, + "w": 6, + "h": 3, + }, + "targets": []any{}, + "options": map[string]any{}, + } + + // Convert models to Unstructured + modelWithRepeatH := v0alpha1.Unstructured{Object: libPanelWithRepeatH} + modelWithRepeatV := v0alpha1.Unstructured{Object: libPanelWithRepeatV} + modelWithoutRepeat := v0alpha1.Unstructured{Object: libPanelWithoutRepeat} + + return &TestLibraryElementProvider{ + elements: []schemaversion.LibraryElementInfo{ + { + UID: "lib-panel-repeat-h", + Name: "Library Panel with Horizontal Repeat", + Kind: 1, // Panel element + Type: "timeseries", + Description: "A library panel with horizontal repeat options", + FolderUID: "", + Model: modelWithRepeatH, + }, + { + UID: "lib-panel-repeat-v", + Name: "Library Panel with Vertical Repeat", + Kind: 1, // Panel element + Type: "stat", + Description: "A library panel with vertical repeat options", + FolderUID: "", + Model: modelWithRepeatV, + }, + { + UID: "lib-panel-no-repeat", + Name: "Library Panel without Repeat", + Kind: 1, // Panel element + Type: "text", + Description: "A library panel without repeat options", + FolderUID: "", + Model: modelWithoutRepeat, + }, + }, + } +} + +// GetLibraryElementInfo returns the test library elements +func (p *TestLibraryElementProvider) GetLibraryElementInfo(_ context.Context) []schemaversion.LibraryElementInfo { + return p.elements +} diff --git a/pkg/registry/apis/dashboard/datasources.go b/pkg/registry/apis/dashboard/datasources.go index 742dc76aad8..003a2c0de23 100644 --- a/pkg/registry/apis/dashboard/datasources.go +++ b/pkg/registry/apis/dashboard/datasources.go @@ -2,8 +2,10 @@ package dashboard import ( "context" + "encoding/json" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/datasources" @@ -112,6 +114,13 @@ func (l *libraryElementIndexProvider) GetLibraryElementInfo(ctx context.Context) } for _, elem := range result.Elements { + var modelUnstructured v0alpha1.Unstructured + if len(elem.Model) > 0 { + var modelObj map[string]any + if err := json.Unmarshal(elem.Model, &modelObj); err == nil { + modelUnstructured.Object = modelObj + } + } info = append(info, schemaversion.LibraryElementInfo{ UID: elem.UID, Name: elem.Name, @@ -119,6 +128,7 @@ func (l *libraryElementIndexProvider) GetLibraryElementInfo(ctx context.Context) Type: elem.Type, Description: elem.Description, FolderUID: elem.FolderUID, + Model: modelUnstructured, }) } diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx index 74b78bbc323..86b83bd0ed8 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx @@ -8,7 +8,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { getLibraryPanel } from 'app/features/library-panels/state/api'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; -import { getPanelIdForVizPanel } from '../utils/utils'; +import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils'; import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks'; import { panelLinksBehavior } from './PanelMenuBehavior'; @@ -99,8 +99,19 @@ export class LibraryPanelBehavior extends SceneObjectBase +): DashboardV2Spec { + // Deep clone the spec to avoid mutating the original + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const normalized = JSON.parse(JSON.stringify(backendSpec)) as DashboardV2Spec; + + // Create a map of panel ID to whether it has explicit repeat, original width, and if it's a library panel + const panelHasExplicitRepeat = new Map(); + const panelOriginalWidth = new Map(); + const panelIsLibraryPanel = new Map(); + inputPanels.forEach((panel) => { + if (panel.id !== undefined) { + panelHasExplicitRepeat.set(panel.id, !!panel.repeat); + panelIsLibraryPanel.set(panel.id, !!panel.libraryPanel); + if (panel.gridPos?.w !== undefined) { + panelOriginalWidth.set(panel.id, panel.gridPos.w); + } + } + }); + + // Helper to recursively process grid items + function processGridItems(items: GridLayoutItemKind[]): void { + if (!Array.isArray(items)) { + return; + } + + items.forEach((item) => { + if (item.spec?.element?.name) { + // Extract panel ID from element name (format: "panel-{id}") + const match = item.spec.element.name.match(/^panel-(\d+)$/); + if (match) { + const panelId = parseInt(match[1], 10); + const hasExplicitRepeat = panelHasExplicitRepeat.get(panelId); + const isLibraryPanel = panelIsLibraryPanel.get(panelId); + + // Only normalize library panels - check both input panel and element kind + const element = normalized.elements?.[item.spec.element.name]; + const isElementLibraryPanel = element?.kind === 'LibraryPanel'; + + // If this is a library panel item and repeat wasn't explicitly set on the instance, + // remove the repeat property (backend adds it from library panel definition) + // Also restore the original width when removing repeat properties + if ((isLibraryPanel || isElementLibraryPanel) && hasExplicitRepeat === false && item.spec.repeat) { + delete item.spec.repeat; + // Always restore the original width from the input panel + const originalWidth = panelOriginalWidth.get(panelId); + if (originalWidth !== undefined) { + item.spec.width = originalWidth; + } + } + } + } + }); + } + + // Process GridLayout items + if (normalized.layout?.kind === 'GridLayout' && normalized.layout.spec?.items) { + processGridItems(normalized.layout.spec.items); + } + + // Process RowsLayout items + if (normalized.layout?.kind === 'RowsLayout' && normalized.layout.spec?.rows) { + normalized.layout.spec.rows.forEach((row: RowsLayoutRowKind) => { + if (row.spec?.layout?.kind === 'GridLayout' && row.spec.layout.spec?.items) { + processGridItems(row.spec.layout.spec.items); + } + }); + } + + return normalized; +} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts index 9838f889a6d..9dbf8e36f0a 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts @@ -1,6 +1,7 @@ import { readdirSync, readFileSync } from 'fs'; import path from 'path'; +import { normalizeBackendOutputForFrontendComparison } from './serialization-test-utils'; import { transformSaveModelSchemaV2ToScene } from './transformSaveModelSchemaV2ToScene'; import { transformSaveModelToScene } from './transformSaveModelToScene'; import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2'; @@ -229,8 +230,17 @@ describe('V1 to V2 Dashboard Transformation Comparison', () => { expect(frontendOutput).toBeDefined(); expect(backendOutputAfterLoadedByScene).toBeDefined(); + // Normalize backend output to account for differences in library panel repeat handling + // Backend sets repeat from library panel definition, frontend only sets it when explicit on instance + // For migrated dashboards, panels are in the root level, not in spec.panels + const inputPanels = jsonInput.panels || jsonInput.spec?.panels || []; + const normalizedBackendOutput = normalizeBackendOutputForFrontendComparison( + backendOutputAfterLoadedByScene, + inputPanels + ); + // Compare only the spec structures - this is the core transformation - expect(backendOutputAfterLoadedByScene).toEqual(frontendOutput); + expect(normalizedBackendOutput).toEqual(frontendOutput); }); }); @@ -292,8 +302,17 @@ describe('V1 to V2 Dashboard Transformation Comparison', () => { expect(frontendOutput).toBeDefined(); expect(backendOutputAfterLoadedByScene).toBeDefined(); + // Normalize backend output to account for differences in library panel repeat handling + // Backend sets repeat from library panel definition, frontend only sets it when explicit on instance + // For migrated dashboards, panels are in the root level, not in spec.panels + const inputPanels = jsonInput.panels || jsonInput.spec?.panels || []; + const normalizedBackendOutput = normalizeBackendOutputForFrontendComparison( + backendOutputAfterLoadedByScene, + inputPanels + ); + // Compare only the spec structures - this is the core transformation - expect(backendOutputAfterLoadedByScene).toEqual(frontendOutput); + expect(normalizedBackendOutput).toEqual(frontendOutput); }); }); }); diff --git a/public/app/features/dashboard/api/ResponseTransformersToBackend.test.ts b/public/app/features/dashboard/api/ResponseTransformersToBackend.test.ts index d818e4953a3..45a310403e4 100644 --- a/public/app/features/dashboard/api/ResponseTransformersToBackend.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformersToBackend.test.ts @@ -3,6 +3,7 @@ import path from 'path'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; +import { normalizeBackendOutputForFrontendComparison } from 'app/features/dashboard-scene/serialization/serialization-test-utils'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { ensureV2Response } from './ResponseTransformers'; @@ -205,8 +206,13 @@ describe('Backend / Frontend result comparison', () => { expect(frontendOutput.spec).toBeDefined(); expect(backendOutput.spec).toBeDefined(); + // Normalize backend output to account for differences in library panel repeat handling + // Backend sets repeat from library panel definition, frontend only sets it when explicit on instance + const inputPanels = jsonInput.spec?.panels || []; + const normalizedBackendSpec = normalizeBackendOutputForFrontendComparison(backendOutput.spec, inputPanels); + // Compare the spec structures - expect(backendOutput.spec).toEqual(frontendOutput.spec); + expect(normalizedBackendSpec).toEqual(frontendOutput.spec); // Verify the conversion doesn't throw errors and produces a valid structure expect(() => JSON.stringify(frontendOutput)).not.toThrow(); From b17ba6677e3cadb3a4d9f57f247209092b512600 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Fri, 21 Nov 2025 11:51:56 +0100 Subject: [PATCH 045/378] Logs: Feature flag logsInfiniteScrolling removed (#113585) * Remove feature flag definition * Remove feature flag from code * Update imports * LogsNavigationPages: remove deprecated component * Finally deprecate navigation pages * Update tests * Update translations * Remove unused code * More cleanup * Test cleanup * Remove deprecated props * More props removal * Update feature flags * Revert changes --- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 5 - pkg/services/featuremgmt/registry.go | 8 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.json | 3 +- public/app/core/utils/shortLinks.test.ts | 17 -- public/app/core/utils/shortLinks.ts | 8 - public/app/features/explore/Explore.tsx | 1 - .../app/features/explore/Logs/Logs.test.tsx | 62 +---- public/app/features/explore/Logs/Logs.tsx | 86 ++----- .../features/explore/Logs/LogsContainer.tsx | 23 +- .../explore/Logs/LogsNavigation.test.tsx | 138 +---------- .../features/explore/Logs/LogsNavigation.tsx | 224 +----------------- .../explore/Logs/LogsNavigationPages.test.tsx | 54 ----- .../explore/Logs/LogsNavigationPages.tsx | 113 --------- .../logs/components/ControlledLogRows.tsx | 3 +- .../logs/components/InfiniteScroll.test.tsx | 9 - .../logs/components/InfiniteScroll.tsx | 4 +- .../components/panel/InfiniteScroll.test.tsx | 9 - .../logs/components/panel/InfiniteScroll.tsx | 4 +- public/app/features/logs/logsModel.test.ts | 17 +- public/app/features/logs/logsModel.ts | 4 +- public/app/plugins/panel/logs/LogsPanel.tsx | 2 +- public/locales/en-US/grafana.json | 6 +- 24 files changed, 47 insertions(+), 755 deletions(-) delete mode 100644 public/app/features/explore/Logs/LogsNavigationPages.test.tsx delete mode 100644 public/app/features/explore/Logs/LogsNavigationPages.tsx diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index a4eca4f9ee7..ce5adca782c 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -41,7 +41,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | Yes | | `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | Yes | | `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes | -| `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | Yes | | `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | | | `onPremToCloudMigrations` | Enable the Grafana Migration Assistant, which helps you easily migrate various on-prem resources to your Grafana Cloud stack. | Yes | | `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e5c59000c8d..5e9ad9ff8d4 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -381,11 +381,6 @@ export interface FeatureToggles { */ timeComparison?: boolean; /** - * Enables infinite scrolling for the Logs panel in Explore and Dashboards - * @default true - */ - logsInfiniteScrolling?: boolean; - /** * Enables shared crosshair in table panel */ tableSharedCrosshair?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 28ff9e1809e..a557e32087b 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -615,14 +615,6 @@ var ( FrontendOnly: true, Owner: grafanaDatavizSquad, }, - { - Name: "logsInfiniteScrolling", - Description: "Enables infinite scrolling for the Logs panel in Explore and Dashboards", - Stage: FeatureStageGeneralAvailability, - Expression: "true", - FrontendOnly: true, - Owner: grafanaObservabilityLogsSquad, - }, { Name: "tableSharedCrosshair", Description: "Enables shared crosshair in table panel", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 4ee6e57a9ed..99753233108 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -85,7 +85,6 @@ panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true timeComparison,experimental,@grafana/dataviz-squad,false,false,true -logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index bb0e03295a0..d60ad45f64e 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2545,7 +2545,8 @@ "metadata": { "name": "logsInfiniteScrolling", "resourceVersion": "1753448760331", - "creationTimestamp": "2023-11-09T10:54:03Z" + "creationTimestamp": "2023-11-09T10:54:03Z", + "deletionTimestamp": "2025-11-07T10:59:01Z" }, "spec": { "description": "Enables infinite scrolling for the Logs panel in Explore and Dashboards", diff --git a/public/app/core/utils/shortLinks.test.ts b/public/app/core/utils/shortLinks.test.ts index 39a43af6629..f88f302b4db 100644 --- a/public/app/core/utils/shortLinks.test.ts +++ b/public/app/core/utils/shortLinks.test.ts @@ -164,7 +164,6 @@ describe('buildShortUrl', () => { describe('getLogsPermalinkRange', () => { let row: LogRowModel, rows: LogRowModel[]; beforeEach(() => { - config.featureToggles.logsInfiniteScrolling = true; row = createLogRow({ timeEpochMs: 1111112222222, }); @@ -175,22 +174,6 @@ describe('getLogsPermalinkRange', () => { row, ]; }); - afterAll(() => { - config.featureToggles.logsInfiniteScrolling = false; - }); - - it('returns the original range if infinite scrolling is not enabled', () => { - config.featureToggles.logsInfiniteScrolling = false; - const range = { - from: 1111111111111, - to: 1111112222222, - }; - const expectedRange = { - from: new Date(1111111111111).toISOString(), - to: new Date(1111112222222).toISOString(), - }; - expect(getLogsPermalinkRange(row, [row], range)).toEqual(expectedRange); - }); it('returns the range relative to the previous log line', () => { const range = { diff --git a/public/app/core/utils/shortLinks.ts b/public/app/core/utils/shortLinks.ts index 706857c122c..208f2ae0fcc 100644 --- a/public/app/core/utils/shortLinks.ts +++ b/public/app/core/utils/shortLinks.ts @@ -170,14 +170,6 @@ function getPreviousLog(row: LogRowModel, allLogs: LogRowModel[]): LogRowModel | } export function getLogsPermalinkRange(row: LogRowModel, rows: LogRowModel[], absoluteRange: AbsoluteTimeRange) { - const range = { - from: new Date(absoluteRange.from).toISOString(), - to: new Date(absoluteRange.to).toISOString(), - }; - if (!config.featureToggles.logsInfiniteScrolling) { - return range; - } - // With infinite scrolling, the time range of the log line can be after the absolute range or beyond the request line limit, so we need to adjust // Look for the previous sibling log, and use its timestamp const allLogs = rows.filter((logRow) => logRow.dataFrame.refId === row.dataFrame.refId); diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index f8bd275930b..79678f4e15a 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -475,7 +475,6 @@ export class Explore extends PureComponent { onStopScanning={this.onStopScanning} eventBus={this.logsEventBus} splitOpenFn={this.splitOpenFnLogs} - scrollElement={this.scrollElement} isFilterLabelActive={this.isFilterLabelActive} onClickFilterString={this.onClickFilterString} onClickFilterOutString={this.onClickFilterOutString} diff --git a/public/app/features/explore/Logs/Logs.test.tsx b/public/app/features/explore/Logs/Logs.test.tsx index 958a8c589f8..93836280762 100644 --- a/public/app/features/explore/Logs/Logs.test.tsx +++ b/public/app/features/explore/Logs/Logs.test.tsx @@ -65,6 +65,8 @@ describe('Logs', () => { let originalHref = window.location.href; beforeEach(() => { + window.HTMLElement.prototype.scrollIntoView = jest.fn(); + window.HTMLElement.prototype.scroll = jest.fn(); localStorage.clear(); jest.clearAllMocks(); }); @@ -128,9 +130,7 @@ describe('Logs', () => { to: toUtc('2019-01-01 16:00:00'), raw: { from: 'now-1h', to: 'now' }, }} - addResultsToCache={() => {}} onChangeTime={() => {}} - clearCache={() => {}} getFieldLinks={() => { return []; }} @@ -160,39 +160,6 @@ describe('Logs', () => { return { ...rendered, store: fakeStore }; }; - describe('scrolling behavior', () => { - let originalInnerHeight: number; - beforeEach(() => { - originalInnerHeight = window.innerHeight; - window.innerHeight = 1000; - window.HTMLElement.prototype.scrollIntoView = jest.fn(); - window.HTMLElement.prototype.scroll = jest.fn(); - }); - afterEach(() => { - window.innerHeight = originalInnerHeight; - }); - - it('should call `scrollElement.scroll`', () => { - const logs = []; - for (let i = 0; i < 50; i++) { - logs.push(makeLog({ uid: `uid${i}`, rowId: `id${i}`, timeEpochMs: i })); - } - const scrollElementMock = { - scroll: jest.fn(), - scrollTop: 920, - }; - setup( - { scrollElement: scrollElementMock as unknown as HTMLDivElement, panelState: { logs: { id: 'uid47' } } }, - undefined, - logs - ); - - // element.getBoundingClientRect().top will always be 0 for jsdom - // calc will be `scrollElement.scrollTop - window.innerHeight / 2` -> 920 - 500 = 420 - expect(scrollElementMock.scroll).toBeCalledWith({ behavior: 'smooth', top: 420 }); - }); - }); - it('should render logs', () => { setup(); const logsSection = screen.getByTestId('logRows'); @@ -246,9 +213,7 @@ describe('Logs', () => { to: toUtc('2019-01-01 16:00:00'), raw: { from: 'now-1h', to: 'now' }, }} - addResultsToCache={() => {}} onChangeTime={() => {}} - clearCache={() => {}} getFieldLinks={() => { return []; }} @@ -296,9 +261,7 @@ describe('Logs', () => { to: toUtc('2019-01-01 16:00:00'), raw: { from: 'now-1h', to: 'now' }, }} - addResultsToCache={() => {}} onChangeTime={() => {}} - clearCache={() => {}} getFieldLinks={() => { return []; }} @@ -349,9 +312,7 @@ describe('Logs', () => { to: toUtc('2019-01-01 16:00:00'), raw: { from: 'now-1h', to: 'now' }, }} - addResultsToCache={() => {}} onChangeTime={() => {}} - clearCache={() => {}} getFieldLinks={() => { return []; }} @@ -412,22 +373,6 @@ describe('Logs', () => { expect(fakeChangePanelState).toHaveBeenCalledWith('right', 'logs', { logs: {} }); }); - it('should scroll the scrollElement into view if rows contain id', () => { - const panelState = { logs: { id: '3' } }; - const scrollElementMock = { scroll: jest.fn() }; - setup({ loading: false, scrollElement: scrollElementMock as unknown as HTMLDivElement, panelState }); - - expect(scrollElementMock.scroll).toHaveBeenCalled(); - }); - - it('should not scroll the scrollElement into view if rows does not contain id', () => { - const panelState = { logs: { id: 'not-included' } }; - const scrollElementMock = { scroll: jest.fn() }; - setup({ loading: false, scrollElement: scrollElementMock as unknown as HTMLDivElement, panelState }); - - expect(scrollElementMock.scroll).not.toHaveBeenCalled(); - }); - it('should call reportInteraction on permalinkClick', async () => { const panelState = { logs: { id: 'not-included' } }; const rows = [ @@ -479,8 +424,6 @@ describe('Logs', () => { }); it('should call createAndCopyShortLink on permalinkClick - with infinite scrolling', async () => { - const featureToggleValue = config.featureToggles.logsInfiniteScrolling; - config.featureToggles.logsInfiniteScrolling = true; const rows = [ makeLog({ uid: '1', rowId: 'id1', timeEpochMs: 1 }), makeLog({ uid: '2', rowId: 'id2', timeEpochMs: 1 }), @@ -503,7 +446,6 @@ describe('Logs', () => { ) ); expect(createAndCopyShortLink).toHaveBeenCalledWith(expect.stringMatching('visualisationType%22:%22logs')); - config.featureToggles.logsInfiniteScrolling = featureToggleValue; }); }); diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 7820e5eb9e9..055f4d454b7 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -119,11 +119,8 @@ interface Props extends Themeable2 { ) => Promise; getLogRowContextUi?: (row: LogRowModel, runContextQuery?: () => void) => React.ReactNode; getFieldLinks: GetFieldLinksFn; - addResultsToCache: () => void; - clearCache: () => void; eventBus: EventBus; panelState?: ExplorePanelsState; - scrollElement?: HTMLDivElement; isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise; logsFrames?: DataFrame[]; range: TimeRange; @@ -183,8 +180,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { getFieldLinks, theme, logsQueries, - clearCache, - addResultsToCache, exploreId, getRowContext, getLogRowContextUi, @@ -193,7 +188,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { panelState, eventBus, onPinLineCallback, - scrollElement, } = props; const [showLabels, setShowLabels] = useState(store.getBool(SETTINGS_KEYS.showLabels, false)); const [showTime, setShowTime] = useState(store.getBool(SETTINGS_KEYS.showTime, true)); @@ -365,29 +359,17 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { [props.eventBus] ); - const scrollIntoView = useCallback( - (element: HTMLElement) => { - if (config.featureToggles.logsInfiniteScrolling) { - if (logsContainerRef.current) { - topLogsRef.current?.scrollIntoView(); - logsContainerRef.current.scroll({ - behavior: 'smooth', - top: logsContainerRef.current.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, - }); - } + const scrollIntoView = useCallback((element: HTMLElement) => { + if (logsContainerRef.current) { + topLogsRef.current?.scrollIntoView?.(); + logsContainerRef.current.scroll({ + behavior: 'smooth', + top: logsContainerRef.current.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, + }); + } - return; - } - - if (scrollElement) { - scrollElement.scroll({ - behavior: 'smooth', - top: scrollElement.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, - }); - } - }, - [scrollElement] - ); + return; + }, []); const sortOrderChanged = useCallback( (newSortOrder: LogsSortOrder) => { @@ -626,13 +608,11 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { ); const scrollToTopLogs = useCallback(() => { - if (config.featureToggles.logsInfiniteScrolling) { - if (logsContainerRef.current) { - logsContainerRef.current.scroll({ - behavior: 'auto', - top: 0, - }); - } + if (logsContainerRef.current) { + logsContainerRef.current.scroll({ + behavior: 'auto', + top: 0, + }); } topLogsRef.current?.scrollIntoView(); }, []); @@ -684,7 +664,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { ); const { dedupedRows, dedupCount } = useMemo(() => dedupRows(logRows, dedupStrategy), [dedupStrategy, logRows]); - const navigationRange = useMemo(() => createNavigationRange(logRows), [logRows]); const infiniteScrollAvailable = useMemo( () => !logsQueries?.some((query) => 'direction' in query && query.direction === LokiQueryDirection.Scan), [logsQueries] @@ -1060,11 +1039,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { visualisationType === 'logs' && hasData && ( <> -

+
= (props: Props) => { />
- + )} {config.featureToggles.newLogsPanel && visualisationType === 'logs' && ( @@ -1277,7 +1241,7 @@ const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: n }), stickyNavigation: css({ overflow: 'visible', - ...(config.featureToggles.logsInfiniteScrolling && { marginBottom: '0px' }), + marginBottom: '0px', }), logsVolumePanel: css({ marginBottom: theme.spacing(1.5), @@ -1290,17 +1254,3 @@ const dedupRows = (logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) => const dedupCount = dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0); return { dedupedRows, dedupCount }; }; - -const createNavigationRange = (logRows: LogRowModel[]): { from: number; to: number } | undefined => { - if (!logRows || logRows.length === 0) { - return undefined; - } - const firstTimeStamp = logRows[0].timeEpochMs; - const lastTimeStamp = logRows[logRows.length - 1].timeEpochMs; - - if (lastTimeStamp < firstTimeStamp) { - return { from: lastTimeStamp, to: firstTimeStamp }; - } - - return { from: firstTimeStamp, to: lastTimeStamp }; -}; diff --git a/public/app/features/explore/Logs/LogsContainer.tsx b/public/app/features/explore/Logs/LogsContainer.tsx index 13bc78b5849..f93d66c6446 100644 --- a/public/app/features/explore/Logs/LogsContainer.tsx +++ b/public/app/features/explore/Logs/LogsContainer.tsx @@ -31,13 +31,7 @@ import { ExploreItemState } from 'app/types/explore'; import { StoreState } from 'app/types/store'; import { getTimeZone } from '../../profile/state/selectors'; -import { - addResultsToCache, - clearCache, - loadSupplementaryQueryData, - selectIsWaitingForData, - setSupplementaryQueryEnabled, -} from '../state/query'; +import { loadSupplementaryQueryData, selectIsWaitingForData, setSupplementaryQueryEnabled } from '../state/query'; import { updateTimeRange, loadMoreLogs } from '../state/time'; import { LiveTailControls } from '../useLiveTailControls'; import { getFieldLinksForExplore } from '../utils/links'; @@ -58,7 +52,6 @@ interface LogsContainerProps extends PropsFromRedux { onStopScanning: () => void; eventBus: EventBus; splitOpenFn: SplitOpen; - scrollElement?: HTMLDivElement; isFilterLabelActive: (key: string, value: string, refId?: string) => Promise; onClickFilterString: (value: string, refId?: string) => void; onClickFilterOutString: (value: string, refId?: string) => void; @@ -260,14 +253,6 @@ class LogsContainer extends PureComponent { - this.props.addResultsToCache(this.props.exploreId); - }; - - clearCache = () => { - this.props.clearCache(this.props.exploreId); - }; - loadLogsVolumeData = () => { this.props.loadSupplementaryQueryData(this.props.exploreId, SupplementaryQueryType.LogsVolume); }; @@ -298,7 +283,6 @@ class LogsContainer extends PureComponent ({ type LogsNavigationProps = ComponentProps; const defaultProps: LogsNavigationProps = { - absoluteRange: { from: 1637319381811, to: 1637322981811 }, - timeZone: 'local', - queries: [], - loading: false, logsSortOrder: undefined, - visibleRange: { from: 1637322959000, to: 1637322981811 }, - onChangeTime: jest.fn(), scrollToTopLogs: jest.fn(), - addResultsToCache: jest.fn(), - clearCache: jest.fn(), }; const setup = (propOverrides?: Partial) => { @@ -37,132 +26,13 @@ const setup = (propOverrides?: Partial) => { }; describe('LogsNavigation', () => { - it('should always render 3 navigation buttons', () => { + it('should render scroll to top with default logs order', async () => { setup(); - expect(screen.getByTestId('newerLogsButton')).toBeInTheDocument(); - expect(screen.getByTestId('olderLogsButton')).toBeInTheDocument(); + expect(screen.getByTestId('scrollToTop')).toBeInTheDocument(); - }); - it('should render 3 navigation buttons in correct order when default logs order', () => { - const { container } = setup(); - const expectedOrder = ['newerLogsButton', 'olderLogsButton', 'scrollToTop']; - const elements = container.querySelectorAll( - '[data-testid=newerLogsButton],[data-testid=olderLogsButton],[data-testid=scrollToTop]' - ); - expect(Array.from(elements).map((el) => el.getAttribute('data-testid'))).toMatchObject(expectedOrder); - }); + await userEvent.click(screen.getByTestId('scrollToTop')); - it('should render 3 navigation buttons in correct order when flipped logs order', () => { - const { container } = setup({ logsSortOrder: LogsSortOrder.Ascending }); - const expectedOrder = ['olderLogsButton', 'newerLogsButton', 'scrollToTop']; - const elements = container.querySelectorAll( - '[data-testid=newerLogsButton],[data-testid=olderLogsButton],[data-testid=scrollToTop]' - ); - expect(Array.from(elements).map((el) => el.getAttribute('data-testid'))).toMatchObject(expectedOrder); - }); - - it('should disable fetch buttons when logs are loading', () => { - setup({ loading: true }); - const olderLogsButton = screen.getByTestId('olderLogsButton'); - const newerLogsButton = screen.getByTestId('newerLogsButton'); - expect(olderLogsButton).toBeDisabled(); - expect(newerLogsButton).toBeDisabled(); - }); - - it('should render logs navigation pages section', () => { - setup(); - expect(screen.getByTestId('logsNavigationPages')).toBeInTheDocument(); - }); - - it('should correctly request older logs when flipped order', async () => { - const onChangeTimeMock = jest.fn(); - const { rerender } = setup({ onChangeTime: onChangeTimeMock }); - await userEvent.click(screen.getByTestId('olderLogsButton')); - expect(onChangeTimeMock).toHaveBeenCalledWith({ from: 1637319359000, to: 1637322959000 }); - - rerender( - - ); - await userEvent.click(screen.getByTestId('olderLogsButton')); - expect(onChangeTimeMock).toHaveBeenCalledWith({ from: 1637319338000, to: 1637322938000 }); - }); - - it('should correctly display the active page', async () => { - const queries: DataQuery[] = []; - const { rerender } = setup({ - absoluteRange: { from: 1704737384139, to: 1704737684139 }, - visibleRange: { from: 1704737384207, to: 1704737683316 }, - queries, - logsSortOrder: LogsSortOrder.Descending, - }); - - expect(await screen.findByTestId('page1')).toBeInTheDocument(); - expect(screen.getByTestId('page1').firstChild).toHaveClass('selectedBg'); - - expect(screen.queryByTestId('page2')).not.toBeInTheDocument(); - - await userEvent.click(screen.getByTestId('olderLogsButton')); - - rerender( - - ); - - expect(await screen.findByTestId('page1')).toBeInTheDocument(); - expect(screen.getByTestId('page1').firstChild).not.toHaveClass('selectedBg'); - - expect(await screen.findByTestId('page2')).toBeInTheDocument(); - expect(screen.getByTestId('page2').firstChild).toHaveClass('selectedBg'); - - expect(screen.queryByTestId('page3')).not.toBeInTheDocument(); - }); - - it('should reset the scroll when pagination is clicked', async () => { - const scrollToTopLogsMock = jest.fn(); - setup({ scrollToTopLogs: scrollToTopLogsMock }); - - expect(scrollToTopLogsMock).not.toHaveBeenCalled(); - await userEvent.click(screen.getByTestId('olderLogsButton')); - expect(scrollToTopLogsMock).toHaveBeenCalled(); - }); - - it('should not trigger actions while loading', async () => { - const scrollToTopLogs = jest.fn(); - const changeTimeMock = jest.fn(); - setup({ scrollToTopLogs, onChangeTime: changeTimeMock, loading: true }); - - expect(scrollToTopLogs).not.toHaveBeenCalled(); - expect(changeTimeMock).not.toHaveBeenCalled(); - await userEvent.click(screen.getByTestId('olderLogsButton')); - await userEvent.click(screen.getByTestId('newerLogsButton')); - expect(scrollToTopLogs).not.toHaveBeenCalled(); - expect(changeTimeMock).not.toHaveBeenCalled(); - }); - - it('should not add results to cache unless pagination is used', async () => { - const addResultsToCache = jest.fn(); - setup({ addResultsToCache }); - - expect(addResultsToCache).not.toHaveBeenCalled(); - expect(screen.getByTestId('olderLogsButton')).not.toBeDisabled(); - expect(screen.getByTestId('newerLogsButton')).toBeDisabled(); - - await userEvent.click(screen.getByTestId('olderLogsButton')); - await userEvent.click(screen.getByTestId('newerLogsButton')); - - expect(addResultsToCache).toHaveBeenCalledTimes(1); + expect(defaultProps.scrollToTopLogs).toHaveBeenCalledTimes(1); }); }); diff --git a/public/app/features/explore/Logs/LogsNavigation.tsx b/public/app/features/explore/Logs/LogsNavigation.tsx index 47a092b7138..b9b1c0c2c97 100644 --- a/public/app/features/explore/Logs/LogsNavigation.tsx +++ b/public/app/features/explore/Logs/LogsNavigation.tsx @@ -1,220 +1,30 @@ import { css } from '@emotion/css'; -import { isEqual } from 'lodash'; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { memo, useCallback } from 'react'; -import { AbsoluteTimeRange, GrafanaTheme2, LogsSortOrder } from '@grafana/data'; -import { Trans, t } from '@grafana/i18n'; +import { GrafanaTheme2, LogsSortOrder } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; -import { DataQuery, TimeZone } from '@grafana/schema'; -import { Button, Icon, Spinner, useTheme2 } from '@grafana/ui'; +import { Button, Icon, useTheme2 } from '@grafana/ui'; import { getChromeHeaderLevelHeight } from 'app/core/components/AppChrome/TopBar/useChromeHeaderHeight'; -import { LogsNavigationPages } from './LogsNavigationPages'; - type Props = { - absoluteRange: AbsoluteTimeRange; - timeZone: TimeZone; - queries: DataQuery[]; - loading: boolean; - visibleRange: AbsoluteTimeRange; logsSortOrder?: LogsSortOrder | null; - onChangeTime: (range: AbsoluteTimeRange) => void; scrollToTopLogs: () => void; scrollToBottomLogs?: () => void; - addResultsToCache: () => void; - clearCache: () => void; }; -export type LogsPage = { - logsRange: AbsoluteTimeRange; - queryRange: AbsoluteTimeRange; -}; - -function LogsNavigation({ - absoluteRange, - logsSortOrder, - timeZone, - loading, - onChangeTime, - scrollToTopLogs, - scrollToBottomLogs, - visibleRange, - queries, - clearCache, - addResultsToCache, -}: Props) { - const [pages, setPages] = useState([]); - - // These refs are to determine, if we want to clear up logs navigation when totally new query is run - const expectedQueriesRef = useRef(); - const expectedRangeRef = useRef(); - // This ref is to store range span for future queres based on firstly selected time range - // e.g. if last 5 min selected, always run 5 min range - const rangeSpanRef = useRef(0); - - const currentPageIndex = useMemo( - () => - pages.findIndex((page) => { - return page.queryRange.to === absoluteRange.to; - }), - [absoluteRange.to, pages] - ); - +function LogsNavigation({ logsSortOrder, scrollToTopLogs }: Props) { const oldestLogsFirst = logsSortOrder === LogsSortOrder.Ascending; - const onFirstPage = oldestLogsFirst ? currentPageIndex === pages.length - 1 : currentPageIndex === 0; - const onLastPage = oldestLogsFirst ? currentPageIndex === 0 : currentPageIndex === pages.length - 1; const theme = useTheme2(); const styles = getStyles(theme, oldestLogsFirst); - // Main effect to set pages and index - useEffect(() => { - const newPage = { logsRange: visibleRange, queryRange: absoluteRange }; - let newPages: LogsPage[] = []; - // We want to start new pagination if queries change or if absolute range is different than expected - if (!isEqual(expectedRangeRef.current, absoluteRange) || !isEqual(expectedQueriesRef.current, queries)) { - clearCache(); - setPages([newPage]); - expectedQueriesRef.current = queries; - rangeSpanRef.current = absoluteRange.to - absoluteRange.from; - } else { - setPages((pages) => { - // Remove duplicates with new query - newPages = pages.filter((page) => !isEqual(newPage.queryRange, page.queryRange)); - // Sort pages based on logsOrder so they visually align with displayed logs - newPages = [...newPages, newPage].sort((a, b) => sortPages(a, b, logsSortOrder)); - return newPages; - }); - } - }, [visibleRange, absoluteRange, logsSortOrder, queries, clearCache, addResultsToCache]); - - const changeTime = useCallback( - ({ from, to }: AbsoluteTimeRange) => { - addResultsToCache(); - expectedRangeRef.current = { from, to }; - onChangeTime({ from, to }); - }, - [onChangeTime, addResultsToCache] - ); - - const sortPages = (a: LogsPage, b: LogsPage, logsSortOrder?: LogsSortOrder | null) => { - if (logsSortOrder === LogsSortOrder.Ascending) { - return a.queryRange.to > b.queryRange.to ? 1 : -1; - } - return a.queryRange.to > b.queryRange.to ? -1 : 1; - }; - - const olderLogsButton = ( - - ); - - const newerLogsButton = ( - - ); - - const onPageClick = useCallback( - (page: LogsPage, pageNumber: number) => { - reportInteraction('grafana_explore_logs_pagination_clicked', { - pageType: 'page', - pageNumber, - }); - changeTime({ from: page.queryRange.from, to: page.queryRange.to }); - scrollToTopLogs(); - }, - [changeTime, scrollToTopLogs] - ); - const onScrollToTopClick = useCallback(() => { reportInteraction('grafana_explore_logs_scroll_top_clicked'); scrollToTopLogs(); }, [scrollToTopLogs]); - const onScrollToBottomClick = useCallback(() => { - reportInteraction('grafana_explore_logs_scroll_bottom_clicked'); - scrollToBottomLogs?.(); - }, [scrollToBottomLogs]); - return (
- {!config.featureToggles.logsInfiniteScrolling && ( - <> - {oldestLogsFirst ? olderLogsButton : newerLogsButton} - - {oldestLogsFirst ? newerLogsButton : olderLogsButton} - - )} - {scrollToBottomLogs && ( - - )} - ))} -
-
- - ); -} - -const getStyles = (theme: GrafanaTheme2, loading: boolean) => { - return { - pagesWrapper: css({ - height: '100%', - paddingLeft: theme.spacing(0.5), - display: 'flex', - flexDirection: 'column', - '&::after': { - content: "''", - display: 'block', - background: `repeating-linear-gradient(135deg, ${theme.colors.background.primary}, ${theme.colors.background.primary} 5px, ${theme.colors.background.secondary} 5px, ${theme.colors.background.secondary} 15px)`, - width: '3px', - height: 'inherit', - marginBottom: theme.spacing(1), - }, - }), - pagesContainer: css({ - display: 'flex', - padding: 0, - flexDirection: 'column', - }), - page: css({ - display: 'flex', - margin: theme.spacing(2, 0), - cursor: loading ? 'auto' : 'pointer', - whiteSpace: 'normal', - '.selectedBg': { - background: theme.colors.primary.main, - }, - '.selectedText': { - color: theme.colors.primary.main, - }, - }), - line: css({ - width: '3px', - height: '100%', - alignItems: 'center', - background: theme.colors.text.secondary, - }), - time: css({ - width: '60px', - minHeight: '80px', - fontSize: theme.v1.typography.size.sm, - paddingLeft: theme.spacing(0.5), - display: 'flex', - alignItems: 'center', - }), - }; -}; diff --git a/public/app/features/logs/components/ControlledLogRows.tsx b/public/app/features/logs/components/ControlledLogRows.tsx index 4094d4f515c..874234837ac 100644 --- a/public/app/features/logs/components/ControlledLogRows.tsx +++ b/public/app/features/logs/components/ControlledLogRows.tsx @@ -13,7 +13,6 @@ import { SplitOpen, TimeRange, } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { LogsVisualisationType } from '../../explore/Logs/Logs'; @@ -150,7 +149,7 @@ const LogRowsComponent = forwardRef { - config.featureToggles.logsInfiniteScrolling = true; -}); -afterAll(() => { - config.featureToggles.logsInfiniteScrolling = originalState; -}); - describe('InfiniteScroll', () => { test('Wraps components without adding DOM elements', async () => { const { container } = render( diff --git a/public/app/features/logs/components/InfiniteScroll.tsx b/public/app/features/logs/components/InfiniteScroll.tsx index e21ca4a4447..3c4ab5001b1 100644 --- a/public/app/features/logs/components/InfiniteScroll.tsx +++ b/public/app/features/logs/components/InfiniteScroll.tsx @@ -4,7 +4,7 @@ import { ReactNode, MutableRefObject, useCallback, useEffect, useRef, useState } import { AbsoluteTimeRange, CoreApp, LogRowModel, TimeRange, rangeUtil } from '@grafana/data'; // import { convertRawToRange, isRelativeTime, isRelativeTimeRange } from '@grafana/data/internal'; import { Trans } from '@grafana/i18n'; -import { config, reportInteraction } from '@grafana/runtime'; +import { reportInteraction } from '@grafana/runtime'; import { LogsSortOrder, TimeZone } from '@grafana/schema'; import { Button, Icon } from '@grafana/ui'; @@ -86,7 +86,7 @@ export const InfiniteScroll = ({ } function handleScroll(event: Event | WheelEvent) { - if (!scrollElement || !loadMoreLogs || !rows.length || loading || !config.featureToggles.logsInfiniteScrolling) { + if (!scrollElement || !loadMoreLogs || !rows.length || loading) { return; } const scrollDirection = shouldLoadMore(event, lastEvent.current, countRef, scrollElement, lastScroll.current); diff --git a/public/app/features/logs/components/panel/InfiniteScroll.test.tsx b/public/app/features/logs/components/panel/InfiniteScroll.test.tsx index 631c2831e9c..2507a3b5176 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.test.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.test.tsx @@ -2,7 +2,6 @@ import { act, render, screen } from '@testing-library/react'; import { VariableSizeList } from 'react-window'; import { createTheme, dateTimeForTimeZone, rangeUtil } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { LogsSortOrder } from '@grafana/schema'; import { ScrollDirection, SCROLLING_THRESHOLD } from '../InfiniteScroll'; @@ -110,14 +109,6 @@ function setup( return { element, events, scrollTo, wheel }; } -const originalState = config.featureToggles.logsInfiniteScrolling; -beforeAll(() => { - config.featureToggles.logsInfiniteScrolling = true; -}); -afterAll(() => { - config.featureToggles.logsInfiniteScrolling = originalState; -}); - describe('InfiniteScroll', () => { describe.each([LogsSortOrder.Descending, LogsSortOrder.Ascending])( 'When the sort order is descending', diff --git a/public/app/features/logs/components/panel/InfiniteScroll.tsx b/public/app/features/logs/components/panel/InfiniteScroll.tsx index 45cc616ea61..3c7de64b6be 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.tsx @@ -4,7 +4,7 @@ import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window' import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { config, reportInteraction } from '@grafana/runtime'; +import { reportInteraction } from '@grafana/runtime'; import { Spinner, useStyles2 } from '@grafana/ui'; import { canScrollBottom, canScrollTop, getVisibleRange, ScrollDirection, shouldLoadMore } from '../InfiniteScroll'; @@ -139,7 +139,7 @@ export const InfiniteScroll = ({ ); useEffect(() => { - if (!scrollElement || !loadMore || !config.featureToggles.logsInfiniteScrolling) { + if (!scrollElement || !loadMore) { return; } diff --git a/public/app/features/logs/logsModel.test.ts b/public/app/features/logs/logsModel.test.ts index f6221b6c6af..23d0badb4f5 100644 --- a/public/app/features/logs/logsModel.test.ts +++ b/public/app/features/logs/logsModel.test.ts @@ -21,7 +21,6 @@ import { sortDataFrame, toDataFrame, } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen'; import { getMockFrames } from 'app/plugins/datasource/loki/mocks/frames'; @@ -292,7 +291,7 @@ describe('dataFrameToLogsModel', () => { expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta![0]).toMatchObject({ label: '', - value: `2 lines returned`, + value: `2 lines displayed`, kind: LogsMetaKind.String, }); expect(logsModel.meta![1]).toMatchObject({ @@ -374,7 +373,7 @@ describe('dataFrameToLogsModel', () => { expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta![0]).toMatchObject({ label: '', - value: `2 lines returned`, + value: `2 lines displayed`, kind: LogsMetaKind.String, }); expect(logsModel.meta![1]).toMatchObject({ @@ -386,9 +385,7 @@ describe('dataFrameToLogsModel', () => { }); }); - it('with infinite scrolling enabled it should return expected logs model', () => { - config.featureToggles.logsInfiniteScrolling = true; - + it('it should return expected logs model', () => { const series: DataFrame[] = [ createDataFrame({ fields: [ @@ -421,8 +418,6 @@ describe('dataFrameToLogsModel', () => { value: `1 line displayed`, kind: LogsMetaKind.String, }); - - config.featureToggles.logsInfiniteScrolling = false; }); it('given one series with limit as custom meta property should return correct limit', () => { @@ -430,7 +425,7 @@ describe('dataFrameToLogsModel', () => { const logsModel = dataFrameToLogsModel(series, 1); expect(logsModel.meta![0]).toMatchObject({ label: '', - value: `2 lines returned`, + value: `2 lines displayed`, kind: LogsMetaKind.String, }); }); @@ -639,7 +634,7 @@ describe('dataFrameToLogsModel', () => { expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta![0]).toMatchObject({ label: '', - value: `2 lines returned`, + value: `2 lines displayed`, kind: LogsMetaKind.String, }); expect(logsModel.meta![1]).toMatchObject({ @@ -758,7 +753,7 @@ describe('dataFrameToLogsModel', () => { expect(logsModel.meta).toHaveLength(3); expect(logsModel.meta![0]).toMatchObject({ label: '', - value: `2 lines returned`, + value: `2 lines displayed`, kind: LogsMetaKind.String, }); expect(logsModel.meta![1]).toMatchObject({ diff --git a/public/app/features/logs/logsModel.ts b/public/app/features/logs/logsModel.ts index 87cdc031ffc..e60887518fe 100644 --- a/public/app/features/logs/logsModel.ts +++ b/public/app/features/logs/logsModel.ts @@ -41,7 +41,6 @@ import { } from '@grafana/data'; import { SIPrefix } from '@grafana/data/internal'; import { t } from '@grafana/i18n'; -import { config } from '@grafana/runtime'; import { BarAlignment, GraphDrawStyle, StackingMode } from '@grafana/schema'; import { colors } from '@grafana/ui'; import { getThemeColor } from 'app/core/utils/colors'; @@ -576,8 +575,7 @@ function adjustMetaInfo(logsModel: LogsModel, visibleRangeMs?: number, requested metaLimitValue = `${limit} lines shown — ${coverage}% (${rangeUtil.msRangeToTimeString(visibleRangeMs)}) of ${rangeUtil.msRangeToTimeString(requestedRangeMs)}`; } } else { - const description = config.featureToggles.logsInfiniteScrolling ? 'displayed' : 'returned'; - metaLimitValue = `${logsModel.rows.length} ${logsModel.rows.length > 1 ? 'lines' : 'line'} ${description}`; + metaLimitValue = `${logsModel.rows.length} ${logsModel.rows.length > 1 ? 'lines' : 'line'} displayed`; } logsModelMeta[limitIndex] = { diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index c2659859b3e..191e503fd90 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -464,7 +464,7 @@ export const LogsPanel = ({ const loadMoreLogs = useCallback( async (scrollRange: AbsoluteTimeRange) => { - if (!data.request || !config.featureToggles.logsInfiniteScrolling || loadingRef.current) { + if (!data.request || loadingRef.current) { return; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 9539dbefc30..cc900022bb4 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -9973,11 +9973,7 @@ "wrap-lines": "Wrap lines" }, "logs-navigation": { - "newer-logs": "Newer logs", - "older-logs": "Older logs", - "scroll-bottom": "Scroll to bottom", - "scroll-top": "Scroll to top", - "start-of-range": "Start of range" + "scroll-top": "Scroll to top" }, "logs-panel": { "render-common-labels": { From af684248b5c2ccdc1adf1601ec84d004bc03303f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Sencer=20=C3=96zcan?= <32759850+mustafasencer@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:21:19 +0100 Subject: [PATCH 046/378] feat: add reindex trigger as a migration validation (#114283) * feat: add search validation * chore: remove comment * fix: remove local test * fix: revert rebuild index portion * chore: remove unnecessary comments * refactor: validator * fix: revert setting --- pkg/server/wire_gen.go | 4 +- pkg/storage/unified/migrations/migrator.go | 1 - .../unified/migrations/resource_migration.go | 148 +++--------------- pkg/storage/unified/migrations/service.go | 23 ++- pkg/storage/unified/migrations/validator.go | 99 ++++++++++++ 5 files changed, 129 insertions(+), 146 deletions(-) create mode 100644 pkg/storage/unified/migrations/validator.go diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 0cb27791c19..716cd7a9c50 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -534,7 +534,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } migrationDashboardAccessor := legacy.ProvideMigratorDashboardAccessor(legacyDatabaseProvider, stubProvisioningService, accessControl, featureToggles) unifiedMigrator := migrations2.ProvideUnifiedMigrator(migrationDashboardAccessor, resourceClient) - unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore) + unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore, resourceClient) dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg, unifiedStorageMigrationService) if err != nil { return nil, err @@ -1181,7 +1181,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } migrationDashboardAccessor := legacy.ProvideMigratorDashboardAccessor(legacyDatabaseProvider, stubProvisioningService, accessControl, featureToggles) unifiedMigrator := migrations2.ProvideUnifiedMigrator(migrationDashboardAccessor, resourceClient) - unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore) + unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore, resourceClient) dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg, unifiedStorageMigrationService) if err != nil { return nil, err diff --git a/pkg/storage/unified/migrations/migrator.go b/pkg/storage/unified/migrations/migrator.go index bdbb69f98a1..d95aa59ace7 100644 --- a/pkg/storage/unified/migrations/migrator.go +++ b/pkg/storage/unified/migrations/migrator.go @@ -144,7 +144,6 @@ func newUnifiedMigrator( } } -// migrate function -- works for a single kind type migratorFunc = func(ctx context.Context, orgId int64, opts legacy.MigrateOptions, stream resourcepb.BulkStore_BulkProcessClient) (*legacy.BlobStoreInfo, error) func (m *unifiedMigration) Migrate(ctx context.Context, opts legacy.MigrateOptions) (*resourcepb.BulkResponse, error) { diff --git a/pkg/storage/unified/migrations/resource_migration.go b/pkg/storage/unified/migrations/resource_migration.go index 663bf030f67..cf67d220ec3 100644 --- a/pkg/storage/unified/migrations/resource_migration.go +++ b/pkg/storage/unified/migrations/resource_migration.go @@ -16,65 +16,34 @@ import ( ) // ValidationFunc is a function that validates migration results. -// It receives the database session, migration response, and logger for reporting. -// Return an error if validation fails, nil if validation passes or is skipped. -type ValidationFunc func(sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error + +type Validator interface { + Validate(ctx context.Context, sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error +} // ResourceMigration handles migration of specific resource types from legacy to unified storage. -// It implements migrator.CodeMigration and provides a generic, extensible way to migrate any -// resource type by: -// -// 1. Iterating through all organizations -// 2. For each org, delegating to LegacyMigrator to read from legacy and write to unified storage -// 3. Validating migration results using the provided validation function (if any) -// -// To add a new resource type migration, simply create a new ResourceMigration instance in -// service.go with the appropriate schema.GroupResource specifications and optional validation function. type ResourceMigration struct { migrator.MigrationBase - migrator UnifiedMigrator - resources []schema.GroupResource - migrationID string - validationFunc ValidationFunc // Optional: custom validation logic for this migration - log log.Logger + migrator UnifiedMigrator + resources []schema.GroupResource + migrationID string + validator Validator // Optional: custom validation logic for this migration + log log.Logger } // NewResourceMigration creates a new migration for the specified resources. -// This is the primary way to register new resource migrations. -// -// Parameters: -// - legacyMigrator: handles reading from legacy storage and writing to unified storage -// - resources: list of GroupResource to migrate -// - migrationID: unique identifier for this migration -// - validationFunc: optional validation function to verify migration results. -// If nil, no validation will be performed. -// -// Example with legacy table count validation: -// -// NewResourceMigration( -// migrator, -// []schema.GroupResource{{Group: "playlist.grafana.app", Resource: "playlists"}}, -// "playlists", -// NewLegacyTableCountValidator(map[string]LegacyTableInfo{ -// "playlist.grafana.app/playlists": {Table: "playlist", WhereClause: "org_id = ?"}, -// }), -// ) -// -// Example without validation: -// -// NewResourceMigration(migrator, resources, "new-resource", nil) func NewResourceMigration( migrator UnifiedMigrator, resources []schema.GroupResource, migrationID string, - validationFunc ValidationFunc, + validator Validator, ) *ResourceMigration { return &ResourceMigration{ - migrator: migrator, - resources: resources, - migrationID: migrationID, - validationFunc: validationFunc, - log: log.New("storage.unified.resource_migration." + migrationID), + migrator: migrator, + resources: resources, + migrationID: migrationID, + validator: validator, + log: log.New("storage.unified.resource_migration." + migrationID), } } @@ -139,7 +108,7 @@ func (m *ResourceMigration) migrateOrg(ctx context.Context, sess *xorm.Session, } // Validate the migration results - if err := m.validateMigration(sess, response); err != nil { + if err := m.validateMigration(migrationCtx, sess, response); err != nil { m.log.Error("Migration validation failed", "org_id", org.ID, "error", err, "duration", time.Since(startTime)) return fmt.Errorf("migration validation failed for org %d (%s): %w", org.ID, org.Name, err) } @@ -155,13 +124,13 @@ func (m *ResourceMigration) migrateOrg(ctx context.Context, sess *xorm.Session, } // validateMigration calls the custom validation function if provided -func (m *ResourceMigration) validateMigration(sess *xorm.Session, response *resourcepb.BulkResponse) error { - if m.validationFunc == nil { +func (m *ResourceMigration) validateMigration(ctx context.Context, sess *xorm.Session, response *resourcepb.BulkResponse) error { + if m.validator == nil { m.log.Debug("No validation function provided, skipping validation") return nil } - return m.validationFunc(sess, response, m.log) + return m.validator.Validate(ctx, sess, response, m.log) } // LegacyTableInfo defines how to map a unified storage resource to its legacy table @@ -170,85 +139,6 @@ type LegacyTableInfo struct { WhereClause string // WHERE clause template with org_id parameter (e.g., "org_id = ? and is_folder = false") } -// NewLegacyTableCountValidator creates a ValidationFunc that validates migration by comparing -// counts between legacy tables and unified storage. -// -// This is a helper for the common case of validating that all items from legacy tables -// were successfully migrated to unified storage. -// -// Parameters: -// - legacyTableMap: maps "group/resource" keys to LegacyTableInfo for validation. -// Only resources with mappings will be validated. -// -// Example: -// -// validator := NewLegacyTableCountValidator(map[string]LegacyTableInfo{ -// "dashboard.grafana.app/dashboards": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = false"}, -// "folder.grafana.app/folders": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = true"}, -// }) -func NewLegacyTableCountValidator(legacyTableMap map[string]LegacyTableInfo) ValidationFunc { - return func(sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error { - // Check for rejected items - if len(response.Rejected) > 0 { - log.Warn("Migration had rejected items", "count", len(response.Rejected)) - for i, rejected := range response.Rejected { - if i < 10 { // Log first 10 rejected items - log.Warn("Rejected item", - "namespace", rejected.Key.Namespace, - "group", rejected.Key.Group, - "resource", rejected.Key.Resource, - "name", rejected.Key.Name, - "reason", rejected.Error) - } - } - // Rejections are not fatal - they may be expected for invalid data - } - - // Validate counts for each resource type - for _, summary := range response.Summary { - key := fmt.Sprintf("%s/%s", summary.Group, summary.Resource) - tableInfo, ok := legacyTableMap[key] - if !ok { - log.Debug("No legacy table mapping for resource, skipping count validation", - "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), - "namespace", summary.Namespace) - continue - } - - // Get legacy count - orgID, err := ParseOrgIDFromNamespace(summary.Namespace) - if err != nil { - return fmt.Errorf("invalid namespace %s: %w", summary.Namespace, err) - } - - legacyCount, err := sess.Table(tableInfo.Table).Where(tableInfo.WhereClause, orgID).Count() - if err != nil { - return fmt.Errorf("failed to count %s: %w", tableInfo.Table, err) - } - - // Account for rejected items in validation - expectedCount := summary.Count + int64(len(response.Rejected)) - - log.Info("Count validation", - "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), - "namespace", summary.Namespace, - "legacy_count", legacyCount, - "unified_count", summary.Count, - "rejected", len(response.Rejected), - "history", summary.History) - - // Validate that we migrated all items (allowing for rejected items) - if legacyCount > expectedCount { - return fmt.Errorf("count mismatch for %s.%s in namespace %s: legacy has %d, unified has %d, rejected %d", - summary.Resource, summary.Group, summary.Namespace, - legacyCount, summary.Count, len(response.Rejected)) - } - } - - return nil - } -} - func ParseOrgIDFromNamespace(namespace string) (int64, error) { // Use authlib to properly parse all namespace formats including "default" for org 1 info, err := types.ParseNamespace(namespace) diff --git a/pkg/storage/unified/migrations/service.go b/pkg/storage/unified/migrations/service.go index 5bcb2fdf8c2..699be937d72 100644 --- a/pkg/storage/unified/migrations/service.go +++ b/pkg/storage/unified/migrations/service.go @@ -11,6 +11,7 @@ import ( sqlstoremigrator "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/migrations/contract" + "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" "k8s.io/apimachinery/pkg/runtime/schema" @@ -24,29 +25,28 @@ type UnifiedStorageMigrationServiceImpl struct { cfg *setting.Cfg sqlStore db.DB kv kvstore.KVStore + client resource.ResourceClient } var _ contract.UnifiedStorageMigrationService = (*UnifiedStorageMigrationServiceImpl)(nil) // ProvideUnifiedStorageMigrationService is a Wire provider that creates the migration service. -// The service implements registry.BackgroundService and runs migrations during server startup. func ProvideUnifiedStorageMigrationService( migrator UnifiedMigrator, cfg *setting.Cfg, sqlStore db.DB, kv kvstore.KVStore, + client resource.ResourceClient, ) contract.UnifiedStorageMigrationService { return &UnifiedStorageMigrationServiceImpl{ migrator: migrator, cfg: cfg, sqlStore: sqlStore, kv: kv, + client: client, } } -// Run executes unified storage migrations as a background service. -// This blocks until migrations complete. If migrations fail, an error is returned -// which will prevent Grafana from starting. func (p *UnifiedStorageMigrationServiceImpl) Run(ctx context.Context) error { // TODO: temporary skip migrations in test environments to prevent integration test timeouts. if os.Getenv("GRAFANA_TEST_DB") != "" { @@ -61,18 +61,15 @@ func (p *UnifiedStorageMigrationServiceImpl) Run(ctx context.Context) error { // TODO: Re-enable once migrations are ready // TODO: add guarantee that this only runs once - // return RegisterMigrations(p.migrator, p.cfg, p.sqlStore) + // return RegisterMigrations(p.migrator, p.cfg, p.sqlStore, p.client) return nil } -// RegisterMigrations initializes and registers all unified storage migrations. -// This function is the entry point for all data migrations from legacy storage -// to unified storage. It returns an error if migrations fail, preventing Grafana -// from starting with inconsistent data. func RegisterMigrations( migrator UnifiedMigrator, cfg *setting.Cfg, sqlStore db.DB, + client resource.ResourceClient, ) error { ctx, span := tracer.Start(context.Background(), "storage.unified.RegisterMigrations") defer span.End() @@ -85,7 +82,7 @@ func RegisterMigrations( // Register resource migrations // To add a new resource type, simply add another migration here with the appropriate resources - registerResourceMigrations(mg, migrator) + registerResourceMigrations(mg, migrator, client) // Run all registered migrations (blocking) sec := cfg.Raw.Section("database") @@ -99,9 +96,7 @@ func RegisterMigrations( return nil } -// registerResourceMigrations registers all unified storage resource migrations. -// Add new resource types here by creating additional ResourceMigration instances. -func registerResourceMigrations(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator) { +func registerResourceMigrations(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) { dashboardsAndFolders := NewResourceMigration( migrator, []schema.GroupResource{ @@ -109,7 +104,7 @@ func registerResourceMigrations(mg *sqlstoremigrator.Migrator, migrator UnifiedM {Group: "dashboard.grafana.app", Resource: "dashboards"}, }, "folders-dashboards", - NewLegacyTableCountValidator(map[string]LegacyTableInfo{ + NewCountValidator(client, map[string]LegacyTableInfo{ "folder.grafana.app/folders": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = true"}, "dashboard.grafana.app/dashboards": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = false"}, }), diff --git a/pkg/storage/unified/migrations/validator.go b/pkg/storage/unified/migrations/validator.go new file mode 100644 index 00000000000..6deef77bb6d --- /dev/null +++ b/pkg/storage/unified/migrations/validator.go @@ -0,0 +1,99 @@ +package migrations + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/util/xorm" +) + +type CountValidator struct { + client resourcepb.ResourceIndexClient + legacyTableMap map[string]LegacyTableInfo +} + +func NewCountValidator(client resourcepb.ResourceIndexClient, legacyTableMap map[string]LegacyTableInfo) Validator { + return &CountValidator{client: client, legacyTableMap: legacyTableMap} +} + +func (v *CountValidator) Validate(ctx context.Context, sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error { + if len(response.Rejected) > 0 { + log.Warn("Migration had rejected items", "count", len(response.Rejected)) + for i, rejected := range response.Rejected { + if i < 10 { // Log first 10 rejected items + log.Warn("Rejected item", + "namespace", rejected.Key.Namespace, + "group", rejected.Key.Group, + "resource", rejected.Key.Resource, + "name", rejected.Key.Name, + "reason", rejected.Error) + } + } + // Rejections are not fatal - they may be expected for invalid data + } + + // Validate counts for each resource type + for _, summary := range response.Summary { + key := fmt.Sprintf("%s/%s", summary.Group, summary.Resource) + tableInfo, ok := v.legacyTableMap[key] + if !ok { + log.Debug("No legacy table mapping for resource, skipping count validation", + "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), + "namespace", summary.Namespace) + continue + } + + // Get legacy count from database + orgID, err := ParseOrgIDFromNamespace(summary.Namespace) + if err != nil { + return fmt.Errorf("invalid namespace %s: %w", summary.Namespace, err) + } + + legacyCount, err := sess.Table(tableInfo.Table).Where(tableInfo.WhereClause, orgID).Count() + if err != nil { + return fmt.Errorf("failed to count %s: %w", tableInfo.Table, err) + } + + // Get unified storage count using GetStats API + statsResp, err := v.client.GetStats(ctx, &resourcepb.ResourceStatsRequest{ + Namespace: summary.Namespace, + Kinds: []string{fmt.Sprintf("%s/%s", summary.Group, summary.Resource)}, + }) + if err != nil { + return fmt.Errorf("failed to get stats for %s/%s in namespace %s: %w", + summary.Group, summary.Resource, summary.Namespace, err) + } + + // Find the count for this specific resource type + var unifiedCount int64 + for _, stat := range statsResp.Stats { + if stat.Group == summary.Group && stat.Resource == summary.Resource { + unifiedCount = stat.Count + break + } + } + + // Account for rejected items in validation + expectedCount := unifiedCount + int64(len(response.Rejected)) + + log.Info("Count validation", + "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), + "namespace", summary.Namespace, + "legacy_count", legacyCount, + "unified_count", unifiedCount, + "migration_summary_count", summary.Count, + "rejected", len(response.Rejected), + "history", summary.History) + + // Validate that we migrated all items (allowing for rejected items) + if legacyCount > expectedCount { + return fmt.Errorf("count mismatch for %s.%s in namespace %s: legacy has %d, unified has %d, rejected %d", + summary.Resource, summary.Group, summary.Namespace, + legacyCount, unifiedCount, len(response.Rejected)) + } + } + + return nil +} From b366217aa6604e04f9f155dc641a1c3b0265963a Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:40:44 +0000 Subject: [PATCH 047/378] SchemaV2ToScenes: Adjust ad hoc multi value check in v2 (#114291) adjust ad hoc multi value check in v2 --- .../serialization/transformSaveModelSchemaV2ToScene.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 4e9e8f7fc81..0534ad76e56 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -319,7 +319,9 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S defaultKeys: variable.spec.defaultKeys, useQueriesAsFilterForOptions: true, layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined, - supportsMultiValueOperators: Boolean(getDataSourceSrv().getInstanceSettings(ds)?.meta.multiValueFilterOperators), + supportsMultiValueOperators: Boolean( + getDataSourceSrv().getInstanceSettings({ type: ds.type })?.meta.multiValueFilterOperators + ), }); } if (variable.kind === defaultCustomVariableKind().kind) { @@ -517,7 +519,7 @@ export function createVariablesForSnapshot(dashboard: DashboardV2Spec): SceneVar useQueriesAsFilterForOptions: true, layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined, supportsMultiValueOperators: Boolean( - getDataSourceSrv().getInstanceSettings(ds)?.meta.multiValueFilterOperators + getDataSourceSrv().getInstanceSettings({ type: ds.type })?.meta.multiValueFilterOperators ), }); } From 203549ce3caa9aab6a5f70103e9e4d78b0831e81 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Fri, 21 Nov 2025 12:53:15 +0100 Subject: [PATCH 048/378] Chore: Fix enterprise imports drift (#114288) * Chore: Fix enterprise imports drift * Fix indirect dependency --- go.mod | 2 +- pkg/extensions/enterprise_imports.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index d8df0cd6bd7..eb9780e649f 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( github.com/crewjam/saml v0.4.14 // @grafana/identity-access-team github.com/dgraph-io/badger/v4 v4.7.0 // @grafana/grafana-search-and-storage github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/grafana-backend-group + github.com/docker/go-connections v0.6.0 // @grafana/grafana-app-platform-squad github.com/dolthub/go-mysql-server v0.19.1-0.20250410182021-5632d67cd46e // @grafana/grafana-datasources-core-services github.com/dolthub/vitess v0.0.0-20250930230441-70c2c6a98e33 // @grafana/grafana-datasources-core-services github.com/dustin/go-humanize v1.0.1 // @grafana/observability-traces-and-profiling @@ -405,7 +406,6 @@ require ( github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.4.0+incompatible // indirect - github.com/docker/go-connections v0.6.0 // indirect; @grafana/grafana-app-platform-squad github.com/docker/go-units v0.5.0 // indirect github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 // indirect github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index 113c2f8e4bb..472652cc103 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -15,6 +15,7 @@ import ( _ "github.com/blugelabs/bluge" _ "github.com/blugelabs/bluge_segment_api" _ "github.com/crewjam/saml" + _ "github.com/docker/go-connections/nat" _ "github.com/go-jose/go-jose/v4" _ "github.com/gobwas/glob" _ "github.com/googleapis/gax-go/v2" @@ -30,6 +31,7 @@ import ( _ "github.com/spf13/cobra" // used by the standalone apiserver cli _ "github.com/spyzhov/ajson" _ "github.com/stretchr/testify/require" + _ "github.com/testcontainers/testcontainers-go" _ "gocloud.dev/secrets/awskms" _ "gocloud.dev/secrets/azurekeyvault" _ "gocloud.dev/secrets/gcpkms" @@ -54,9 +56,7 @@ import ( _ "github.com/grafana/e2e" _ "github.com/grafana/gofpdf" _ "github.com/grafana/gomemcache/memcache" - _ "github.com/grafana/tempo/pkg/traceql" - _ "github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v1beta1" _ "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1" - _ "github.com/testcontainers/testcontainers-go" + _ "github.com/grafana/tempo/pkg/traceql" ) From 669382c21269343e1b4690623f39d7bd413895d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Fri, 21 Nov 2025 14:18:32 +0100 Subject: [PATCH 049/378] datasources: ClearAuthHeadersMiddleware: refactor (#113707) * refactor: extract logic * directly use the setting.cfg in the middleware * more granular config handling, per section * fixed unit test * refactor code to avoid lint error --- pkg/services/contexthandler/contexthandler.go | 48 +- .../clear_auth_headers_middleware.go | 17 +- .../clear_auth_headers_middleware_test.go | 553 ++++++------------ .../pluginsintegration/pluginsintegration.go | 2 +- 4 files changed, 230 insertions(+), 390 deletions(-) diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 52b1488e71e..8c48121f925 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -207,6 +207,33 @@ type AuthHTTPHeaderList struct { Items []string } +func GetAuthHTTPHeaders(jwtAuth *setting.AuthJWTSettings, authProxy *setting.AuthProxySettings) []string { + var items []string + + // used by basic auth, api keys and potentially jwt auth + items = append(items, "Authorization") + + // remove X-Grafana-Device-Id as it is only used for auth in authn clients. + items = append(items, "X-Grafana-Device-Id") + + // if jwt is enabled we add it to the list. We can ignore in case it is set to Authorization + if jwtAuth.Enabled && jwtAuth.HeaderName != "" && jwtAuth.HeaderName != "Authorization" { + items = append(items, jwtAuth.HeaderName) + } + + // if auth proxy is enabled add the main proxy header and all configured headers + if authProxy.Enabled { + items = append(items, authProxy.HeaderName) + for _, header := range authProxy.Headers { + if header != "" { + items = append(items, header) + } + } + } + + return items +} + // WithAuthHTTPHeaders returns a new context in which all possible configured auth header will be included // and later retrievable by AuthHTTPHeaderListFromContext. func WithAuthHTTPHeaders(ctx context.Context, cfg *setting.Cfg) context.Context { @@ -217,26 +244,7 @@ func WithAuthHTTPHeaders(ctx context.Context, cfg *setting.Cfg) context.Context } } - // used by basic auth, api keys and potentially jwt auth - list.Items = append(list.Items, "Authorization") - - // remove X-Grafana-Device-Id as it is only used for auth in authn clients. - list.Items = append(list.Items, "X-Grafana-Device-Id") - - // if jwt is enabled we add it to the list. We can ignore in case it is set to Authorization - if cfg.JWTAuth.Enabled && cfg.JWTAuth.HeaderName != "" && cfg.JWTAuth.HeaderName != "Authorization" { - list.Items = append(list.Items, cfg.JWTAuth.HeaderName) - } - - // if auth proxy is enabled add the main proxy header and all configured headers - if cfg.AuthProxy.Enabled { - list.Items = append(list.Items, cfg.AuthProxy.HeaderName) - for _, header := range cfg.AuthProxy.Headers { - if header != "" { - list.Items = append(list.Items, header) - } - } - } + list.Items = append(list.Items, GetAuthHTTPHeaders(&cfg.JWTAuth, &cfg.AuthProxy)...) return context.WithValue(ctx, authHTTPHeaderListKey, list) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go index 5c93e1da565..1321c1fc474 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go @@ -6,21 +6,26 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/setting" ) // NewClearAuthHeadersMiddleware creates a new backend.HandlerMiddleware // that will clear any outgoing HTTP headers that was part of the incoming // HTTP request and used when authenticating to Grafana. -func NewClearAuthHeadersMiddleware() backend.HandlerMiddleware { +func NewClearAuthHeadersMiddleware(cfgJWTAuth *setting.AuthJWTSettings, cfgAuthProxy *setting.AuthProxySettings) backend.HandlerMiddleware { return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &ClearAuthHeadersMiddleware{ - BaseHandler: backend.NewBaseHandler(next), + BaseHandler: backend.NewBaseHandler(next), + cfgJWTAuth: cfgJWTAuth, + cfgAuthProxy: cfgAuthProxy, } }) } type ClearAuthHeadersMiddleware struct { backend.BaseHandler + cfgJWTAuth *setting.AuthJWTSettings + cfgAuthProxy *setting.AuthProxySettings } func (m *ClearAuthHeadersMiddleware) clearHeaders(ctx context.Context, h backend.ForwardHTTPHeaders) { @@ -30,11 +35,9 @@ func (m *ClearAuthHeadersMiddleware) clearHeaders(ctx context.Context, h backend return } - list := contexthandler.AuthHTTPHeaderListFromContext(ctx) - if list != nil { - for _, k := range list.Items { - h.DeleteHTTPHeader(k) - } + items := contexthandler.GetAuthHTTPHeaders(m.cfgJWTAuth, m.cfgAuthProxy) + for _, k := range items { + h.DeleteHTTPHeader(k) } } diff --git a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go index 5ca047881e3..aa9bc3aa469 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" - "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -16,397 +15,227 @@ import ( func TestClearAuthHeadersMiddleware(t *testing.T) { const otherHeader = "test" - t.Run("When no auth headers in reqContext", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, "/some/thing", nil) - require.NoError(t, err) + req, err := http.NewRequest(http.MethodGet, "/some/thing", nil) + require.NoError(t, err) - req.Header.Set(otherHeader, "test") + t.Run("When requests are for a datasource", func(t *testing.T) { + cfg := setting.NewCfg() + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware(&cfg.JWTAuth, &cfg.AuthProxy)), + ) - t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := handlertest.NewHandlerMiddlewareTest(t, - WithReqContext(req, &user.SignedInUser{}), - handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), - ) + pluginCtx := backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + } - pluginCtx := backend.PluginContext{ - DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, - } - - t.Run("No auth headers to clear when calling QueryData", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.QueryDataReq) - require.Len(t, cdt.QueryDataReq.Headers, 1) - require.Empty(t, cdt.QueryDataReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling CallResource", func(t *testing.T) { - err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ - PluginContext: pluginCtx, - Headers: map[string][]string{otherHeader: {"test"}}, - }, nopCallResourceSender) - require.NoError(t, err) - require.NotNil(t, cdt.CallResourceReq) - require.Len(t, cdt.CallResourceReq.Headers, 1) - require.Equal(t, http.Header{http.CanonicalHeaderKey(otherHeader): {"test"}}, cdt.CallResourceReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling CheckHealth", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.CheckHealthReq) - require.Len(t, cdt.CheckHealthReq.Headers, 1) - require.Empty(t, cdt.CheckHealthReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling SubscribeStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.SubscribeStream(req.Context(), &backend.SubscribeStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.SubscribeStreamReq) - require.Len(t, cdt.SubscribeStreamReq.Headers, 1) - require.Empty(t, cdt.SubscribeStreamReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling PublishStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.PublishStream(req.Context(), &backend.PublishStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.PublishStreamReq) - require.Len(t, cdt.PublishStreamReq.Headers, 1) - require.Empty(t, cdt.PublishStreamReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling RunStream", func(t *testing.T) { - err = cdt.MiddlewareHandler.RunStream(req.Context(), &backend.RunStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }, &backend.StreamSender{}) - require.NoError(t, err) - require.NotNil(t, cdt.RunStreamReq) - require.Len(t, cdt.RunStreamReq.Headers, 1) - require.Empty(t, cdt.RunStreamReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling QueryData", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.QueryDataReq) + require.Len(t, cdt.QueryDataReq.Headers, 1) + require.Equal(t, "test", cdt.QueryDataReq.Headers[otherHeader]) + require.Empty(t, cdt.QueryDataReq.GetHTTPHeaders()) }) - t.Run("And requests are for an app", func(t *testing.T) { - cdt := handlertest.NewHandlerMiddlewareTest(t, - WithReqContext(req, &user.SignedInUser{}), - handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), - ) + t.Run("Should clear auth headers when calling CallResource", func(t *testing.T) { + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ + PluginContext: pluginCtx, + Headers: map[string][]string{ + otherHeader: {"test"}, + "Authorization": {"secret"}, + "X-Grafana-Device-Id": {"secret"}, + }, + }, nopCallResourceSender) + require.NoError(t, err) + require.NotNil(t, cdt.CallResourceReq) + require.Len(t, cdt.CallResourceReq.Headers, 1) + require.Equal(t, []string{"test"}, cdt.CallResourceReq.Headers[otherHeader]) + require.Equal(t, "test", cdt.CallResourceReq.GetHTTPHeader(otherHeader)) + }) - pluginCtx := backend.PluginContext{ - AppInstanceSettings: &backend.AppInstanceSettings{}, - } - - t.Run("No auth headers to clear when calling QueryData", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.QueryDataReq) - require.Len(t, cdt.QueryDataReq.Headers, 1) - require.Equal(t, "test", cdt.QueryDataReq.Headers[otherHeader]) - require.Empty(t, cdt.QueryDataReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling CheckHealth", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.CheckHealthReq) + require.Len(t, cdt.CheckHealthReq.Headers, 1) + require.Equal(t, "test", cdt.CheckHealthReq.Headers[otherHeader]) + require.Empty(t, cdt.CheckHealthReq.GetHTTPHeaders()) + }) - t.Run("No auth headers to clear when calling CallResource", func(t *testing.T) { - err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ - PluginContext: pluginCtx, - Headers: map[string][]string{otherHeader: {"test"}}, - }, nopCallResourceSender) - require.NoError(t, err) - require.NotNil(t, cdt.CallResourceReq) - require.Len(t, cdt.CallResourceReq.Headers, 1) - require.Equal(t, []string{"test"}, cdt.CallResourceReq.Headers[otherHeader]) - require.Equal(t, http.Header{http.CanonicalHeaderKey(otherHeader): {"test"}}, cdt.CallResourceReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling SubscribeStream", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.SubscribeStream(req.Context(), &backend.SubscribeStreamRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.SubscribeStreamReq) + require.Len(t, cdt.SubscribeStreamReq.Headers, 1) + require.Equal(t, "test", cdt.SubscribeStreamReq.Headers[otherHeader]) + require.Empty(t, cdt.SubscribeStreamReq.GetHTTPHeaders()) + }) - t.Run("No auth headers to clear when calling CheckHealth", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.CheckHealthReq) - require.Len(t, cdt.CheckHealthReq.Headers, 1) - require.Equal(t, "test", cdt.CheckHealthReq.Headers[otherHeader]) - require.Empty(t, cdt.CheckHealthReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling PublishStream", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.PublishStream(req.Context(), &backend.PublishStreamRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.PublishStreamReq) + require.Len(t, cdt.PublishStreamReq.Headers, 1) + require.Equal(t, "test", cdt.PublishStreamReq.Headers[otherHeader]) + require.Empty(t, cdt.PublishStreamReq.GetHTTPHeaders()) + }) - t.Run("No auth headers to clear when calling SubscribeStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.SubscribeStream(req.Context(), &backend.SubscribeStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.SubscribeStreamReq) - require.Len(t, cdt.SubscribeStreamReq.Headers, 1) - require.Equal(t, "test", cdt.SubscribeStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.SubscribeStreamReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling PublishStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.PublishStream(req.Context(), &backend.PublishStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }) - require.NoError(t, err) - require.NotNil(t, cdt.PublishStreamReq) - require.Len(t, cdt.PublishStreamReq.Headers, 1) - require.Equal(t, "test", cdt.PublishStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.PublishStreamReq.GetHTTPHeaders()) - }) - - t.Run("No auth headers to clear when calling RunStream", func(t *testing.T) { - err = cdt.MiddlewareHandler.RunStream(req.Context(), &backend.RunStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{otherHeader: "test"}, - }, &backend.StreamSender{}) - require.NoError(t, err) - require.NotNil(t, cdt.RunStreamReq) - require.Len(t, cdt.RunStreamReq.Headers, 1) - require.Equal(t, "test", cdt.RunStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.RunStreamReq.GetHTTPHeaders()) - }) + t.Run("Should clear auth headers when calling RunStream", func(t *testing.T) { + err = cdt.MiddlewareHandler.RunStream(req.Context(), &backend.RunStreamRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, + }, &backend.StreamSender{}) + require.NoError(t, err) + require.NotNil(t, cdt.RunStreamReq) + require.Len(t, cdt.RunStreamReq.Headers, 1) + require.Equal(t, "test", cdt.RunStreamReq.Headers[otherHeader]) + require.Empty(t, cdt.RunStreamReq.GetHTTPHeaders()) }) }) - t.Run("When auth headers in reqContext", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, "/some/thing", nil) - require.NoError(t, err) + t.Run("When requests are for an app", func(t *testing.T) { + cfg := setting.NewCfg() + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware(&cfg.JWTAuth, &cfg.AuthProxy)), + ) - t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := handlertest.NewHandlerMiddlewareTest(t, - WithReqContext(req, &user.SignedInUser{}), - handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), - ) + req.Header.Set("Authorization", "val") - req := req.WithContext(contexthandler.WithAuthHTTPHeaders(req.Context(), setting.NewCfg())) + const otherHeader = "x-Other" + req.Header.Set(otherHeader, "test") - pluginCtx := backend.PluginContext{ - DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, - } + pluginCtx := backend.PluginContext{ + AppInstanceSettings: &backend.AppInstanceSettings{}, + } - t.Run("Should clear auth headers when calling QueryData", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.QueryDataReq) - require.Len(t, cdt.QueryDataReq.Headers, 1) - require.Equal(t, "test", cdt.QueryDataReq.Headers[otherHeader]) - require.Empty(t, cdt.QueryDataReq.GetHTTPHeaders()) - }) - - t.Run("Should clear auth headers when calling CallResource", func(t *testing.T) { - err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ - PluginContext: pluginCtx, - Headers: map[string][]string{ - otherHeader: {"test"}, - "Authorization": {"secret"}, - "X-Grafana-Device-Id": {"secret"}, - }, - }, nopCallResourceSender) - require.NoError(t, err) - require.NotNil(t, cdt.CallResourceReq) - require.Len(t, cdt.CallResourceReq.Headers, 1) - require.Equal(t, []string{"test"}, cdt.CallResourceReq.Headers[otherHeader]) - require.Equal(t, "test", cdt.CallResourceReq.GetHTTPHeader(otherHeader)) - }) - - t.Run("Should clear auth headers when calling CheckHealth", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.CheckHealthReq) - require.Len(t, cdt.CheckHealthReq.Headers, 1) - require.Equal(t, "test", cdt.CheckHealthReq.Headers[otherHeader]) - require.Empty(t, cdt.CheckHealthReq.GetHTTPHeaders()) - }) - - t.Run("Should clear auth headers when calling SubscribeStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.SubscribeStream(req.Context(), &backend.SubscribeStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.SubscribeStreamReq) - require.Len(t, cdt.SubscribeStreamReq.Headers, 1) - require.Equal(t, "test", cdt.SubscribeStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.SubscribeStreamReq.GetHTTPHeaders()) - }) - - t.Run("Should clear auth headers when calling PublishStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.PublishStream(req.Context(), &backend.PublishStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.PublishStreamReq) - require.Len(t, cdt.PublishStreamReq.Headers, 1) - require.Equal(t, "test", cdt.PublishStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.PublishStreamReq.GetHTTPHeaders()) - }) - - t.Run("Should clear auth headers when calling RunStream", func(t *testing.T) { - err = cdt.MiddlewareHandler.RunStream(req.Context(), &backend.RunStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }, &backend.StreamSender{}) - require.NoError(t, err) - require.NotNil(t, cdt.RunStreamReq) - require.Len(t, cdt.RunStreamReq.Headers, 1) - require.Equal(t, "test", cdt.RunStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.RunStreamReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling QueryData", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.QueryDataReq) + require.Len(t, cdt.QueryDataReq.Headers, 1) + require.Equal(t, "test", cdt.QueryDataReq.Headers[otherHeader]) + require.Empty(t, cdt.QueryDataReq.GetHTTPHeaders()) }) - t.Run("And requests are for an app", func(t *testing.T) { - cdt := handlertest.NewHandlerMiddlewareTest(t, - WithReqContext(req, &user.SignedInUser{}), - handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), - ) + t.Run("Should clear auth headers when calling CallResource", func(t *testing.T) { + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ + PluginContext: pluginCtx, + Headers: map[string][]string{ + otherHeader: {"test"}, + "Authorization": {"secret"}, + "X-Grafana-Device-Id": {"secret"}, + }, + }, nopCallResourceSender) + require.NoError(t, err) + require.NotNil(t, cdt.CallResourceReq) + require.Len(t, cdt.CallResourceReq.Headers, 1) + require.Equal(t, []string{"test"}, cdt.CallResourceReq.Headers[otherHeader]) + require.Equal(t, "test", cdt.CallResourceReq.GetHTTPHeader(otherHeader)) + }) - req := req.WithContext(contexthandler.WithAuthHTTPHeaders(req.Context(), setting.NewCfg())) - req.Header.Set("Authorization", "val") - - const otherHeader = "x-Other" - req.Header.Set(otherHeader, "test") - - pluginCtx := backend.PluginContext{ - AppInstanceSettings: &backend.AppInstanceSettings{}, - } - - t.Run("Should clear auth headers when calling QueryData", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.QueryDataReq) - require.Len(t, cdt.QueryDataReq.Headers, 1) - require.Equal(t, "test", cdt.QueryDataReq.Headers[otherHeader]) - require.Empty(t, cdt.QueryDataReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling CheckHealth", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.CheckHealthReq) + require.Len(t, cdt.CheckHealthReq.Headers, 1) + require.Equal(t, "test", cdt.CheckHealthReq.Headers[otherHeader]) + require.Empty(t, cdt.CheckHealthReq.GetHTTPHeaders()) + }) - t.Run("Should clear auth headers when calling CallResource", func(t *testing.T) { - err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ - PluginContext: pluginCtx, - Headers: map[string][]string{ - otherHeader: {"test"}, - "Authorization": {"secret"}, - "X-Grafana-Device-Id": {"secret"}, - }, - }, nopCallResourceSender) - require.NoError(t, err) - require.NotNil(t, cdt.CallResourceReq) - require.Len(t, cdt.CallResourceReq.Headers, 1) - require.Equal(t, []string{"test"}, cdt.CallResourceReq.Headers[otherHeader]) - require.Equal(t, "test", cdt.CallResourceReq.GetHTTPHeader(otherHeader)) + t.Run("Should clear auth headers when calling SubscribeStream", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.SubscribeStream(req.Context(), &backend.SubscribeStreamRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.SubscribeStreamReq) + require.Len(t, cdt.SubscribeStreamReq.Headers, 1) + require.Equal(t, "test", cdt.SubscribeStreamReq.Headers[otherHeader]) + require.Empty(t, cdt.SubscribeStreamReq.GetHTTPHeaders()) + }) - t.Run("Should clear auth headers when calling CheckHealth", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.CheckHealthReq) - require.Len(t, cdt.CheckHealthReq.Headers, 1) - require.Equal(t, "test", cdt.CheckHealthReq.Headers[otherHeader]) - require.Empty(t, cdt.CheckHealthReq.GetHTTPHeaders()) + t.Run("Should clear auth headers when calling PublishStream", func(t *testing.T) { + _, err = cdt.MiddlewareHandler.PublishStream(req.Context(), &backend.PublishStreamRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, }) + require.NoError(t, err) + require.NotNil(t, cdt.PublishStreamReq) + require.Len(t, cdt.PublishStreamReq.Headers, 1) + require.Equal(t, "test", cdt.PublishStreamReq.Headers[otherHeader]) + require.Empty(t, cdt.PublishStreamReq.GetHTTPHeaders()) + }) - t.Run("Should clear auth headers when calling SubscribeStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.SubscribeStream(req.Context(), &backend.SubscribeStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.SubscribeStreamReq) - require.Len(t, cdt.SubscribeStreamReq.Headers, 1) - require.Equal(t, "test", cdt.SubscribeStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.SubscribeStreamReq.GetHTTPHeaders()) - }) - - t.Run("Should clear auth headers when calling PublishStream", func(t *testing.T) { - _, err = cdt.MiddlewareHandler.PublishStream(req.Context(), &backend.PublishStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }) - require.NoError(t, err) - require.NotNil(t, cdt.PublishStreamReq) - require.Len(t, cdt.PublishStreamReq.Headers, 1) - require.Equal(t, "test", cdt.PublishStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.PublishStreamReq.GetHTTPHeaders()) - }) - - t.Run("Should clear auth headers when calling RunStream", func(t *testing.T) { - err = cdt.MiddlewareHandler.RunStream(req.Context(), &backend.RunStreamRequest{ - PluginContext: pluginCtx, - Headers: map[string]string{ - otherHeader: "test", - "Authorization": "secret", - "X-Grafana-Device-Id": "secret", - }, - }, &backend.StreamSender{}) - require.NoError(t, err) - require.NotNil(t, cdt.RunStreamReq) - require.Len(t, cdt.RunStreamReq.Headers, 1) - require.Equal(t, "test", cdt.RunStreamReq.Headers[otherHeader]) - require.Empty(t, cdt.RunStreamReq.GetHTTPHeaders()) - }) + t.Run("Should clear auth headers when calling RunStream", func(t *testing.T) { + err = cdt.MiddlewareHandler.RunStream(req.Context(), &backend.RunStreamRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{ + otherHeader: "test", + "Authorization": "secret", + "X-Grafana-Device-Id": "secret", + }, + }, &backend.StreamSender{}) + require.NoError(t, err) + require.NotNil(t, cdt.RunStreamReq) + require.Len(t, cdt.RunStreamReq.Headers, 1) + require.Equal(t, "test", cdt.RunStreamReq.Headers[otherHeader]) + require.Empty(t, cdt.RunStreamReq.GetHTTPHeaders()) }) }) } diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index a510ead1910..9c0cf4d10c6 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -200,7 +200,7 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken middlewares = append(middlewares, clientmiddleware.NewTracingHeaderMiddleware(), - clientmiddleware.NewClearAuthHeadersMiddleware(), + clientmiddleware.NewClearAuthHeadersMiddleware(&cfg.JWTAuth, &cfg.AuthProxy), clientmiddleware.NewOAuthTokenMiddleware(oAuthTokenService), clientmiddleware.NewCookiesMiddleware(skipCookiesNames), clientmiddleware.NewCachingMiddleware(cachingServiceClient), From c0f6d90971bb7fa4f2bc0c77a8339d199ca39306 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Fri, 21 Nov 2025 07:19:55 -0600 Subject: [PATCH 050/378] Loki: Set Query limits context header (#114112) * feat(lokiQueryLimitsContext): send full query range & expr on initial split requests --- .../src/types/featureToggles.gen.ts | 6 +- .../dataquery/x/LokiDataQuery_types.gen.ts | 8 + pkg/services/featuremgmt/registry.go | 9 +- pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.json | 1587 ++++------------- pkg/tsdb/loki/api.go | 11 + pkg/tsdb/loki/api_test.go | 52 + .../kinds/dataquery/types_dataquery_gen.go | 13 + pkg/tsdb/loki/parse_query.go | 15 + pkg/tsdb/loki/parse_query_test.go | 69 + pkg/tsdb/loki/types.go | 6 + public/app/features/explore/Logs/Logs.tsx | 1 + .../explore/Logs/LogsVolumePanelList.tsx | 38 +- .../Logs/utils/logsVolumeResponse.test.ts | 70 +- .../explore/Logs/utils/logsVolumeResponse.ts | 28 +- .../app/plugins/datasource/loki/dataquery.cue | 8 + .../plugins/datasource/loki/dataquery.gen.ts | 8 + .../plugins/datasource/loki/mergeResponses.ts | 7 + .../plugins/datasource/loki/mocks/frames.ts | 28 + .../datasource/loki/querySplitting.test.ts | 195 +- .../plugins/datasource/loki/querySplitting.ts | 51 +- .../app/plugins/datasource/loki/queryUtils.ts | 20 + .../plugins/datasource/loki/responseUtils.ts | 22 +- .../loki/shardQuerySplitting.test.ts | 134 +- .../datasource/loki/shardQuerySplitting.ts | 30 +- public/locales/en-US/grafana.json | 6 +- 26 files changed, 1088 insertions(+), 1335 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 5e9ad9ff8d4..09c123d527a 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -596,7 +596,7 @@ export interface FeatureToggles { */ alertingPrometheusRulesPrimary?: boolean; /** - * Used in Logs Drilldown to split queries into multiple queries based on the number of shards + * Deprecated. Replace with lokiShardSplitting. Used in Logs Drilldown to split queries into multiple queries based on the number of shards */ exploreLogsShardSplitting?: boolean; /** @@ -1178,6 +1178,10 @@ export interface FeatureToggles { */ ttlPluginInstanceManager?: boolean; /** + * Send X-Loki-Query-Limits-Context header to Loki on first split request + */ + lokiQueryLimitsContext?: boolean; + /** * Enables the new version of rudderstack * @default false */ diff --git a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts index f8f056a3c8d..2e3c4991c65 100644 --- a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts @@ -50,6 +50,14 @@ export interface LokiDataQuery extends common.DataQuery { * Used to override the name of the series. */ legendFormat?: string; + /** + * The full query plan for split/shard queries. Encoded and sent to Loki via `X-Loki-Query-Limits-Context` header. Requires "lokiQueryLimitsContext" feature flag + */ + limitsContext?: { + expr: string; + from: number; + to: number; + }; /** * Used to limit the number of log rows returned. */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a557e32087b..26690f92481 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -983,7 +983,7 @@ var ( }, { Name: "exploreLogsShardSplitting", - Description: "Used in Logs Drilldown to split queries into multiple queries based on the number of shards", + Description: "Deprecated. Replace with lokiShardSplitting. Used in Logs Drilldown to split queries into multiple queries based on the number of shards", Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, @@ -1938,6 +1938,13 @@ var ( FrontendOnly: true, Owner: grafanaPluginsPlatformSquad, }, + { + Name: "lokiQueryLimitsContext", + Description: "Send X-Loki-Query-Limits-Context header to Loki on first split request", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaObservabilityLogsSquad, + }, { Name: "rudderstackUpgrade", Description: "Enables the new version of rudderstack", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 99753233108..d0a2674aa55 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -263,4 +263,5 @@ kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false awsDatasourcesHttpProxy,experimental,@grafana/aws-datasources,false,false,false transformationsEmptyPlaceholder,preview,@grafana/datapro,false,false,true ttlPluginInstanceManager,experimental,@grafana/plugins-platform-backend,false,false,true +lokiQueryLimitsContext,experimental,@grafana/observability-logs,false,false,true rudderstackUpgrade,experimental,@grafana/grafana-frontend-platform,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index d60ad45f64e..1f9315705ce 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3,46 +3,10 @@ "apiVersion": "featuretoggle.grafana.app/v0alpha1", "metadata": {}, "items": [ - { - "metadata": { - "name": "addFieldFromCalculationStatFunctions", - "resourceVersion": "1762442825881", - "creationTimestamp": "2023-11-03T14:39:58Z", - "deletionTimestamp": "2025-11-17T15:58:43Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" - } - }, - "spec": { - "description": "Add cumulative and window functions to the add field from calculation transformation", - "stage": "GA", - "codeowner": "@grafana/datapro", - "frontend": true, - "expression": "true" - } - }, - { - "metadata": { - "name": "adhocFiltersInTooltips", - "resourceVersion": "1756814786992", - "creationTimestamp": "2025-07-29T17:53:43Z", - "deletionTimestamp": "2025-11-12T10:05:30Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-02 12:06:26.992384 +0000 UTC" - } - }, - "spec": { - "description": "Enable adhoc filter buttons in visualization tooltips", - "stage": "GA", - "codeowner": "@grafana/datapro", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "aiGeneratedDashboardChanges", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-03-05T12:01:31Z" }, "spec": { @@ -55,7 +19,7 @@ { "metadata": { "name": "alertEnrichment", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-06T12:16:07Z" }, "spec": { @@ -69,7 +33,7 @@ { "metadata": { "name": "alertEnrichmentConditional", - "resourceVersion": "1757418974334", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -84,7 +48,7 @@ { "metadata": { "name": "alertEnrichmentMultiStep", - "resourceVersion": "1757418224384", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -99,7 +63,7 @@ { "metadata": { "name": "alertRuleRestore", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-05T14:15:26Z" }, "spec": { @@ -112,7 +76,7 @@ { "metadata": { "name": "alertRuleUseFiredAtForStartsAt", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-22T11:16:38Z" }, "spec": { @@ -125,7 +89,7 @@ { "metadata": { "name": "alertingAIAnalyzeCentralStateHistory", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-16T16:42:42Z" }, "spec": { @@ -139,7 +103,7 @@ { "metadata": { "name": "alertingAIFeedback", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-23T12:38:09Z" }, "spec": { @@ -153,7 +117,7 @@ { "metadata": { "name": "alertingAIGenAlertRules", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-16T16:42:42Z" }, "spec": { @@ -167,7 +131,7 @@ { "metadata": { "name": "alertingAIGenTemplates", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-16T16:42:42Z" }, "spec": { @@ -181,7 +145,7 @@ { "metadata": { "name": "alertingAIImproveAlertRules", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-16T16:42:42Z" }, "spec": { @@ -195,7 +159,7 @@ { "metadata": { "name": "alertingBacktesting", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-12-14T14:44:14Z" }, "spec": { @@ -207,7 +171,7 @@ { "metadata": { "name": "alertingBulkActionsInUI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-24T14:49:59Z" }, "spec": { @@ -222,11 +186,8 @@ { "metadata": { "name": "alertingCentralAlertHistory", - "resourceVersion": "1763541314825", - "creationTimestamp": "2024-05-29T15:01:38Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2024-05-29T15:01:38Z" }, "spec": { "description": "Enables the new central alert history.", @@ -237,7 +198,7 @@ { "metadata": { "name": "alertingDisableSendAlertsExternal", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-05-23T12:29:19Z" }, "spec": { @@ -250,7 +211,7 @@ { "metadata": { "name": "alertingEnrichmentAssistantInvestigations", - "resourceVersion": "1757606567075", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -262,25 +223,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "alertingEnrichmentAssistantInvestigationsUI", - "resourceVersion": "1757541861834", - "creationTimestamp": "2025-09-10T22:04:21Z", - "deletionTimestamp": "2025-09-11T16:02:47Z" - }, - "spec": { - "description": "Enable Assistant Investigations enrichment type in the UI.", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad", - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "alertingEnrichmentPerRule", - "resourceVersion": "1756206837948", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -295,7 +241,7 @@ { "metadata": { "name": "alertingFilterV2", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-11T11:29:26Z" }, "spec": { @@ -308,7 +254,7 @@ { "metadata": { "name": "alertingImportAlertmanagerAPI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-10T08:32:50Z" }, "spec": { @@ -322,7 +268,7 @@ { "metadata": { "name": "alertingImportAlertmanagerUI", - "resourceVersion": "1754585847887", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -337,7 +283,7 @@ { "metadata": { "name": "alertingImportYAMLUI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-05-21T15:59:41Z" }, "spec": { @@ -351,7 +297,7 @@ { "metadata": { "name": "alertingJiraIntegration", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-02-14T12:22:04Z" }, "spec": { @@ -365,7 +311,7 @@ { "metadata": { "name": "alertingListViewV2", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-05-24T14:40:49Z" }, "spec": { @@ -378,7 +324,7 @@ { "metadata": { "name": "alertingListViewV2PreviewToggle", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-22T08:50:34Z" }, "spec": { @@ -391,7 +337,7 @@ { "metadata": { "name": "alertingMigrationUI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-14T16:40:05Z" }, "spec": { @@ -405,7 +351,7 @@ { "metadata": { "name": "alertingNotificationHistory", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-17T13:26:26Z" }, "spec": { @@ -419,7 +365,7 @@ { "metadata": { "name": "alertingNotificationsStepMode", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-22T11:07:45Z" }, "spec": { @@ -433,7 +379,7 @@ { "metadata": { "name": "alertingPrometheusRulesPrimary", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-27T12:27:16Z" }, "spec": { @@ -446,7 +392,7 @@ { "metadata": { "name": "alertingProvenanceLockWrites", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-23T18:16:06Z" }, "spec": { @@ -459,7 +405,7 @@ { "metadata": { "name": "alertingQueryAndExpressionsStepMode", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-26T06:33:14Z" }, "spec": { @@ -473,7 +419,7 @@ { "metadata": { "name": "alertingQueryOptimization", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-10T20:52:58Z" }, "spec": { @@ -483,25 +429,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "alertingRuleNotificationMessageSectionExtension", - "resourceVersion": "1756193222535", - "creationTimestamp": "2025-08-26T07:27:02Z", - "deletionTimestamp": "2025-08-26T11:13:57Z" - }, - "spec": { - "description": "Enable rule notification message section extension.", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad", - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "alertingRulePermanentlyDelete", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-03T11:18:25Z" }, "spec": { @@ -516,11 +447,8 @@ { "metadata": { "name": "alertingRuleRecoverDeleted", - "resourceVersion": "1763541314825", - "creationTimestamp": "2025-03-27T14:39:26Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-03-27T14:39:26Z" }, "spec": { "description": "Enables the UI functionality to recover and view deleted alert rules", @@ -533,7 +461,7 @@ { "metadata": { "name": "alertingRuleVersionHistoryRestore", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-02-17T12:25:32Z" }, "spec": { @@ -548,11 +476,8 @@ { "metadata": { "name": "alertingSaveStateCompressed", - "resourceVersion": "1759485036332", - "creationTimestamp": "2025-01-27T17:47:33Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-03 09:50:36.332762 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-01-27T17:47:33Z" }, "spec": { "description": "Enables the compressed protobuf-based alert state storage. Default is enabled.", @@ -564,7 +489,7 @@ { "metadata": { "name": "alertingSaveStatePeriodic", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-23T16:03:30Z" }, "spec": { @@ -576,12 +501,9 @@ { "metadata": { "name": "alertingTriage", - "resourceVersion": "1763541314825", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 08:35:14.825756 +0000 UTC" - } + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables the alerting triage feature", @@ -594,7 +516,7 @@ { "metadata": { "name": "alertingUIOptimizeReducer", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-18T10:59:00Z" }, "spec": { @@ -608,7 +530,7 @@ { "metadata": { "name": "alertingUIUseBackendFilters", - "resourceVersion": "1762966218072", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-11-13T14:52:14Z" }, "spec": { @@ -621,12 +543,9 @@ { "metadata": { "name": "alertingUseNewSimplifiedRoutingHashAlgorithm", - "resourceVersion": "1759339813575", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-01 17:30:13.575464 +0000 UTC" - } + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "", @@ -637,29 +556,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "alertingUseOldSimplifiedRoutingHashAlgorithm", - "resourceVersion": "1759339782639", - "creationTimestamp": "2025-10-01T17:29:29Z", - "deletionTimestamp": "2025-10-01T17:30:13Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-01 17:29:42.63941 +0000 UTC" - } - }, - "spec": { - "description": "", - "stage": "deprecated", - "codeowner": "@grafana/alerting-squad", - "requiresRestart": true, - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "alertmanagerRemotePrimary", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -671,7 +571,7 @@ { "metadata": { "name": "alertmanagerRemoteSecondary", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -683,11 +583,8 @@ { "metadata": { "name": "alertmanagerRemoteSecondaryWithRemoteState", - "resourceVersion": "1753776005753", - "creationTimestamp": "2025-07-25T15:06:59Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-29 08:00:05.753498 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-07-25T15:06:59Z" }, "spec": { "description": "Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.", @@ -699,7 +596,7 @@ { "metadata": { "name": "annotationPermissionUpdate", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-31T13:30:13Z" }, "spec": { @@ -712,7 +609,7 @@ { "metadata": { "name": "appPlatformGrpcClientAuth", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-14T10:47:18Z" }, "spec": { @@ -725,7 +622,7 @@ { "metadata": { "name": "assetSriChecks", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-04T10:56:35Z" }, "spec": { @@ -738,7 +635,7 @@ { "metadata": { "name": "authZGRPCServer", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-13T09:41:35Z" }, "spec": { @@ -751,7 +648,7 @@ { "metadata": { "name": "awsAsyncQueryCaching", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-07-21T15:34:07Z" }, "spec": { @@ -764,11 +661,8 @@ { "metadata": { "name": "awsDatasourcesHttpProxy", - "resourceVersion": "1762964349996", - "creationTimestamp": "2025-11-12T18:51:23Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 16:19:09.996919 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-12T18:51:23Z" }, "spec": { "description": "Enables http proxy settings for aws datasources", @@ -780,7 +674,7 @@ { "metadata": { "name": "awsDatasourcesTempCredentials", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-07-06T15:06:11Z" }, "spec": { @@ -793,7 +687,7 @@ { "metadata": { "name": "azureMonitorDisableLogLimit", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-24T13:32:09Z" }, "spec": { @@ -806,7 +700,7 @@ { "metadata": { "name": "azureMonitorEnableUserAuth", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-27T14:01:54Z" }, "spec": { @@ -819,7 +713,7 @@ { "metadata": { "name": "azureMonitorLogsBuilderEditor", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-02T14:15:25Z" }, "spec": { @@ -832,7 +726,7 @@ { "metadata": { "name": "azureMonitorPrometheusExemplars", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-06T16:53:17Z" }, "spec": { @@ -845,7 +739,7 @@ { "metadata": { "name": "azureResourcePickerUpdates", - "resourceVersion": "1754910058337", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -860,7 +754,7 @@ { "metadata": { "name": "cachingOptimizeSerializationMemoryUsage", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-12T16:56:49Z" }, "spec": { @@ -872,9 +766,8 @@ { "metadata": { "name": "canvasPanelNesting", - "resourceVersion": "1753448760331", - "creationTimestamp": "2022-05-31T19:03:34Z", - "deletionTimestamp": "2025-11-06T14:43:27Z" + "resourceVersion": "1763727063618", + "creationTimestamp": "2022-05-31T19:03:34Z" }, "spec": { "description": "Allow elements nesting", @@ -886,7 +779,7 @@ { "metadata": { "name": "canvasPanelPanZoom", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-02T19:52:21Z" }, "spec": { @@ -899,7 +792,7 @@ { "metadata": { "name": "cdnPluginsLoadFirst", - "resourceVersion": "1758882341746", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -910,24 +803,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "cdnPluginsLoadedFirst", - "resourceVersion": "1758881920003", - "creationTimestamp": "2025-09-26T10:18:40Z", - "deletionTimestamp": "2025-09-26T10:25:41Z" - }, - "spec": { - "description": "Prioritize loading plugins from the CDN before other sources", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "expression": "false" - } - }, { "metadata": { "name": "cdnPluginsUrls", - "resourceVersion": "1759489886228", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -941,7 +820,7 @@ { "metadata": { "name": "cloudRBACRoles", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-10T13:19:01Z" }, "spec": { @@ -955,7 +834,7 @@ { "metadata": { "name": "cloudWatchBatchQueries", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-20T19:09:41Z" }, "spec": { @@ -967,7 +846,7 @@ { "metadata": { "name": "cloudWatchCrossAccountQuerying", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-11-28T11:39:12Z" }, "spec": { @@ -980,7 +859,7 @@ { "metadata": { "name": "cloudWatchNewLabelParsing", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-05T15:57:56Z" }, "spec": { @@ -993,7 +872,7 @@ { "metadata": { "name": "cloudWatchRoundUpEndTime", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-27T15:10:28Z" }, "spec": { @@ -1006,7 +885,7 @@ { "metadata": { "name": "configurableSchedulerTick", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-07-26T16:44:12Z" }, "spec": { @@ -1017,27 +896,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "correlations", - "resourceVersion": "1762442825881", - "creationTimestamp": "2022-09-16T13:14:27Z", - "deletionTimestamp": "2025-11-13T09:21:46Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" - } - }, - "spec": { - "description": "Correlations page", - "stage": "GA", - "codeowner": "@grafana/datapro", - "expression": "true" - } - }, { "metadata": { "name": "crashDetection", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-12T15:07:27Z" }, "spec": { @@ -1050,7 +912,7 @@ { "metadata": { "name": "dashboardDisableSchemaValidationV1", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-11T16:52:46Z" }, "spec": { @@ -1062,7 +924,7 @@ { "metadata": { "name": "dashboardDisableSchemaValidationV2", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-11T16:52:46Z" }, "spec": { @@ -1071,28 +933,10 @@ "codeowner": "@grafana/grafana-app-platform-squad" } }, - { - "metadata": { - "name": "dashboardDsAdHocFiltering", - "resourceVersion": "1756814786992", - "creationTimestamp": "2025-07-23T08:12:25Z", - "deletionTimestamp": "2025-11-10T17:17:49Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-02 12:06:26.992384 +0000 UTC" - } - }, - "spec": { - "description": "Enables adhoc filtering support for the dashboard datasource", - "stage": "GA", - "codeowner": "@grafana/datapro", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "dashboardLevelTimeMacros", - "resourceVersion": "1753435849295", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T09:49:07Z" }, "spec": { @@ -1105,12 +949,9 @@ { "metadata": { "name": "dashboardLibrary", - "resourceVersion": "1763643877862", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-20 13:04:37.862907 +0000 UTC" - } + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Displays datasource provisioned dashboards in dashboard empty page, only when coming from datasource configuration page", @@ -1121,11 +962,8 @@ { "metadata": { "name": "dashboardNewLayouts", - "resourceVersion": "1763533458962", - "creationTimestamp": "2024-10-23T08:55:45Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 06:24:18.962973 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2024-10-23T08:55:45Z" }, "spec": { "description": "Enables experimental new dashboard layouts", @@ -1136,7 +974,7 @@ { "metadata": { "name": "dashboardScene", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-11-13T08:51:21Z" }, "spec": { @@ -1150,7 +988,7 @@ { "metadata": { "name": "dashboardSceneForViewers", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-11-02T19:02:25Z" }, "spec": { @@ -1164,7 +1002,7 @@ { "metadata": { "name": "dashboardSceneSolo", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-02-11T08:08:47Z" }, "spec": { @@ -1178,7 +1016,7 @@ { "metadata": { "name": "dashboardSchemaValidationLogging", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-11T16:52:46Z" }, "spec": { @@ -1190,11 +1028,8 @@ { "metadata": { "name": "dashboardTemplates", - "resourceVersion": "1763643877862", - "creationTimestamp": "2025-10-28T20:05:32Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-20 13:04:37.862907 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-10-28T20:05:32Z" }, "spec": { "description": "Enables a flow to get started with a new dashboard from a template", @@ -1205,7 +1040,7 @@ { "metadata": { "name": "dashboardUndoRedo", - "resourceVersion": "1757940426210", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -1219,7 +1054,7 @@ { "metadata": { "name": "dashgpt", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-08-30T20:22:05Z" }, "spec": { @@ -1233,7 +1068,7 @@ { "metadata": { "name": "dataplaneAggregator", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-08-09T08:41:07Z" }, "spec": { @@ -1243,25 +1078,10 @@ "requiresRestart": true } }, - { - "metadata": { - "name": "dataplaneFrontendFallback", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-04-07T21:13:19Z", - "deletionTimestamp": "2025-11-06T17:39:31Z" - }, - "spec": { - "description": "Support dataplane contract field name change for transformations and field name matchers where the name is different", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "datasourceAPIServers", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-19T08:28:27Z" }, "spec": { @@ -1274,7 +1094,7 @@ { "metadata": { "name": "datasourceConnectionsTab", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-01-21T17:39:48Z" }, "spec": { @@ -1287,7 +1107,7 @@ { "metadata": { "name": "datasourceQueryTypes", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-05-23T16:46:28Z" }, "spec": { @@ -1300,7 +1120,7 @@ { "metadata": { "name": "disableClassicHTTPHistogram", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-18T19:37:44Z" }, "spec": { @@ -1313,7 +1133,7 @@ { "metadata": { "name": "disableEnvelopeEncryption", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-05-24T08:34:47Z" }, "spec": { @@ -1326,7 +1146,7 @@ { "metadata": { "name": "disableNumericMetricsSortingInExpressions", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-16T14:52:47Z" }, "spec": { @@ -1339,11 +1159,8 @@ { "metadata": { "name": "disableSSEDataplane", - "resourceVersion": "1762552416963", - "creationTimestamp": "2023-04-12T16:24:34Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-07 21:53:36.963146843 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2023-04-12T16:24:34Z" }, "spec": { "description": "Disables dataplane specific processing in server side expressions.", @@ -1351,50 +1168,11 @@ "codeowner": "@grafana/grafana-datasources-core-services" } }, - { - "metadata": { - "name": "dskitBackgroundServices", - "resourceVersion": "1757339637779", - "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-08 13:53:57.77994 +0000 UTC" - } - }, - "spec": { - "description": "Enables dskit background service wrapper", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "requiresRestart": true, - "hideFromDocs": true, - "expression": "false" - } - }, - { - "metadata": { - "name": "editPanelCSVDragAndDrop", - "resourceVersion": "1762783224740", - "creationTimestamp": "2023-01-24T09:43:44Z", - "deletionTimestamp": "2025-11-12T14:47:44Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-10 14:00:24.740459 +0000 UTC" - } - }, - "spec": { - "description": "Enables drag and drop for CSV and Excel files", - "stage": "experimental", - "codeowner": "@grafana/dataviz-squad", - "frontend": true - } - }, { "metadata": { "name": "elasticsearchCrossClusterSearch", - "resourceVersion": "1757441088767", - "creationTimestamp": "2024-12-12T22:20:04Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-09 18:04:48.76781 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2024-12-12T22:20:04Z" }, "spec": { "description": "Enables cross cluster search in the Elasticsearch data source", @@ -1406,7 +1184,7 @@ { "metadata": { "name": "elasticsearchImprovedParsing", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-01-15T17:05:54Z" }, "spec": { @@ -1418,7 +1196,7 @@ { "metadata": { "name": "enableAppChromeExtensions", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-30T04:32:08Z" }, "spec": { @@ -1433,7 +1211,7 @@ { "metadata": { "name": "enableDashboardEmptyExtensions", - "resourceVersion": "1759194774156", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-13T07:03:13Z" }, "spec": { @@ -1448,11 +1226,8 @@ { "metadata": { "name": "enableDatagridEditing", - "resourceVersion": "1763539789149", - "creationTimestamp": "2023-04-24T14:46:31Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 08:09:49.149669 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2023-04-24T14:46:31Z" }, "spec": { "description": "Enables the edit functionality in the datagrid panel", @@ -1463,7 +1238,7 @@ { "metadata": { "name": "enableExtensionsAdminPage", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-05T15:55:10Z" }, "spec": { @@ -1476,7 +1251,7 @@ { "metadata": { "name": "enableNativeHTTPHistogram", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-03T18:23:55Z" }, "spec": { @@ -1486,26 +1261,10 @@ "requiresRestart": true } }, - { - "metadata": { - "name": "enablePluginImporter", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-07-16T04:42:28Z", - "deletionTimestamp": "2025-10-23T04:18:23Z" - }, - "spec": { - "description": "Set this to true to use the new PluginImporter functionality", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true, - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "enableSCIM", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-07T14:38:46Z" }, "spec": { @@ -1517,7 +1276,7 @@ { "metadata": { "name": "enableScopesInMetricsExplore", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-06T13:11:33Z" }, "spec": { @@ -1530,7 +1289,7 @@ { "metadata": { "name": "exploreLogsAggregatedMetrics", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1543,7 +1302,7 @@ { "metadata": { "name": "exploreLogsLimitedTimeRange", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1556,11 +1315,11 @@ { "metadata": { "name": "exploreLogsShardSplitting", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { - "description": "Used in Logs Drilldown to split queries into multiple queries based on the number of shards", + "description": "Deprecated. Replace with lokiShardSplitting. Used in Logs Drilldown to split queries into multiple queries based on the number of shards", "stage": "experimental", "codeowner": "@grafana/observability-logs", "frontend": true @@ -1569,7 +1328,7 @@ { "metadata": { "name": "exploreMetricsRelatedLogs", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-05T16:28:43Z" }, "spec": { @@ -1579,38 +1338,10 @@ "frontend": true } }, - { - "metadata": { - "name": "expressionParser", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-02-17T00:59:11Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Enable new expression parser", - "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresRestart": true - } - }, - { - "metadata": { - "name": "extensionSidebar", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-04-03T10:16:35Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Enables the extension sidebar", - "stage": "experimental", - "codeowner": "@grafana/observability-logs", - "frontend": true - } - }, { "metadata": { "name": "externalServiceAccounts", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-09-28T07:26:37Z" }, "spec": { @@ -1622,7 +1353,7 @@ { "metadata": { "name": "extraThemes", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-05-10T13:37:04Z", "deletionTimestamp": "2025-05-20T08:18:08Z" }, @@ -1633,27 +1364,10 @@ "frontend": true } }, - { - "metadata": { - "name": "extractFieldsNameDeduplication", - "resourceVersion": "1762442825881", - "creationTimestamp": "2023-11-02T15:47:42Z", - "deletionTimestamp": "2025-11-12T10:08:13Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" - } - }, - "spec": { - "description": "Make sure extracted field names are unique in the dataframe", - "stage": "experimental", - "codeowner": "@grafana/datapro", - "frontend": true - } - }, { "metadata": { "name": "faroDatasourceSelector", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-05-05T00:35:10Z" }, "spec": { @@ -1666,7 +1380,7 @@ { "metadata": { "name": "favoriteDatasources", - "resourceVersion": "1754648387873", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -1680,7 +1394,7 @@ { "metadata": { "name": "featureHighlights", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-02-03T11:53:23Z" }, "spec": { @@ -1690,42 +1404,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "featureToggleAdminPage", - "resourceVersion": "1758022099771", - "creationTimestamp": "2023-07-18T20:43:32Z", - "deletionTimestamp": "2025-08-29T14:46:39Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-16 11:28:19.771156 +0000 UTC" - } - }, - "spec": { - "description": "Enable admin page for managing feature toggles from the Grafana front-end. Grafana Cloud only.", - "stage": "experimental", - "codeowner": "@grafana/grafana-backend-services-squad", - "requiresRestart": true, - "hideFromDocs": true - } - }, - { - "metadata": { - "name": "feedbackButton", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-12-02T17:08:15Z", - "deletionTimestamp": "2025-11-20T09:53:52Z" - }, - "spec": { - "description": "Enables a button to send feedback from the Grafana UI", - "stage": "experimental", - "codeowner": "@grafana/grafana-operator-experience-squad", - "hideFromDocs": true - } - }, { "metadata": { "name": "fetchRulesUsingPost", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-01-29T12:17:44Z" }, "spec": { @@ -1735,25 +1417,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "filterOutBotsFromFrontendLogs", - "resourceVersion": "1758000919535", - "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z" - }, - "spec": { - "description": "Filter out bots from collecting data for Frontend Observability", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true, - "expression": "false" - } - }, { "metadata": { "name": "foldersAppPlatformAPI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-03T14:15:23Z" }, "spec": { @@ -1765,28 +1432,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "formatString", - "resourceVersion": "1762442825881", - "creationTimestamp": "2023-10-13T18:17:12Z", - "deletionTimestamp": "2025-11-17T13:06:30Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" - } - }, - "spec": { - "description": "Enable format string transformer", - "stage": "GA", - "codeowner": "@grafana/datapro", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "grafanaAPIServerEnsureKubectlAccess", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-12-06T20:21:21Z" }, "spec": { @@ -1800,7 +1449,7 @@ { "metadata": { "name": "grafanaAPIServerWithExperimentalAPIs", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-10-06T18:55:22Z" }, "spec": { @@ -1814,7 +1463,7 @@ { "metadata": { "name": "grafanaAdvisor", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-01-20T10:08:00Z" }, "spec": { @@ -1826,12 +1475,9 @@ { "metadata": { "name": "grafanaAssistantInProfilesDrilldown", - "resourceVersion": "1754572610001", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-08-07 13:16:50.001205 +0000 UTC" - } + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables integration with Grafana Assistant in Profiles Drilldown", @@ -1841,24 +1487,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "grafanaAssistantInProfilesDrillfown", - "resourceVersion": "1754034112469", - "creationTimestamp": "2025-08-01T07:41:52Z", - "deletionTimestamp": "2025-08-01T07:43:17Z" - }, - "spec": { - "description": "Enables interation with Grafana Assitant in Profiles Drilldown", - "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", - "frontend": true - } - }, { "metadata": { "name": "grafanaManagedRecordingRules", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-22T17:53:16Z", "deletionTimestamp": "2025-05-19T10:15:49Z" }, @@ -1869,26 +1501,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "grafanaPathfinder", - "resourceVersion": "1760434668782", - "creationTimestamp": "2025-10-14T10:40:40Z", - "deletionTimestamp": "2025-10-22T08:06:21Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-14 09:37:48.782577 +0000 UTC" - } - }, - "spec": { - "description": "Enables Pathfinder app", - "stage": "preview", - "codeowner": "@grafana/pathfinder" - } - }, { "metadata": { "name": "grafanaconThemes", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-02-06T11:08:04Z" }, "spec": { @@ -1903,7 +1519,7 @@ { "metadata": { "name": "graphiteBackendMode", - "resourceVersion": "1755870507537", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -1917,7 +1533,7 @@ { "metadata": { "name": "groupAttributeSync", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-09T15:29:43Z" }, "spec": { @@ -1930,7 +1546,7 @@ { "metadata": { "name": "groupByVariable", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-02-14T17:18:04Z" }, "spec": { @@ -1940,28 +1556,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "groupToNestedTableTransformation", - "resourceVersion": "1762442825881", - "creationTimestamp": "2024-02-07T14:28:26Z", - "deletionTimestamp": "2025-11-17T11:57:22Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" - } - }, - "spec": { - "description": "Enables the group to nested table transformation", - "stage": "GA", - "codeowner": "@grafana/datapro", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "grpcServer", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-09-26T20:25:34Z" }, "spec": { @@ -1973,7 +1571,7 @@ { "metadata": { "name": "improvedExternalSessionHandling", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-17T10:54:39Z" }, "spec": { @@ -1986,7 +1584,7 @@ { "metadata": { "name": "improvedExternalSessionHandlingSAML", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-01-09T17:02:49Z" }, "spec": { @@ -1999,7 +1597,7 @@ { "metadata": { "name": "individualCookiePreferences", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-02-21T10:19:07Z" }, "spec": { @@ -2011,7 +1609,7 @@ { "metadata": { "name": "infinityRunQueriesInParallel", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-14T12:54:04Z" }, "spec": { @@ -2023,7 +1621,7 @@ { "metadata": { "name": "influxdbBackendMigration", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-02-09T18:26:16Z", "deletionTimestamp": "2023-01-17T14:11:26Z" }, @@ -2038,7 +1636,7 @@ { "metadata": { "name": "influxdbRunQueriesInParallel", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-02-01T10:58:24Z" }, "spec": { @@ -2050,7 +1648,7 @@ { "metadata": { "name": "influxqlStreamingParser", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-11-29T17:29:35Z" }, "spec": { @@ -2062,7 +1660,7 @@ { "metadata": { "name": "interactiveLearning", - "resourceVersion": "1761063016739", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-22T08:06:21Z" }, "spec": { @@ -2074,7 +1672,7 @@ { "metadata": { "name": "investigationsBackend", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-18T08:31:03Z" }, "spec": { @@ -2084,25 +1682,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "inviteUserExperimental", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-03-07T19:09:59Z", - "deletionTimestamp": "2025-11-14T15:33:26Z" - }, - "spec": { - "description": "Renders invite user button along the app", - "stage": "experimental", - "codeowner": "@grafana/sharing-squad", - "frontend": true, - "hideFromDocs": true - } - }, { "metadata": { "name": "jaegerEnableGrpcEndpoint", - "resourceVersion": "1760451551713", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-31T18:19:16Z" }, "spec": { @@ -2114,7 +1697,7 @@ { "metadata": { "name": "jitterAlertRulesWithinGroups", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-18T18:48:11Z" }, "spec": { @@ -2128,7 +1711,7 @@ { "metadata": { "name": "k8SFolderCounts", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { @@ -2141,7 +1724,7 @@ { "metadata": { "name": "k8SFolderMove", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { @@ -2154,7 +1737,7 @@ { "metadata": { "name": "kubernetesAggregator", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-02-12T20:59:35Z" }, "spec": { @@ -2167,7 +1750,7 @@ { "metadata": { "name": "kubernetesAggregatorCapTokenAuth", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-05-15T18:14:23Z" }, "spec": { @@ -2180,7 +1763,7 @@ { "metadata": { "name": "kubernetesAlertingRules", - "resourceVersion": "1754340669702", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -2194,7 +1777,7 @@ { "metadata": { "name": "kubernetesAnnotations", - "resourceVersion": "1761142826172", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-11-06T18:22:20Z" }, "spec": { @@ -2207,7 +1790,7 @@ { "metadata": { "name": "kubernetesAuthZHandlerRedirect", - "resourceVersion": "1758820248165", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -2221,11 +1804,8 @@ { "metadata": { "name": "kubernetesAuthnMutation", - "resourceVersion": "1753454405614", - "creationTimestamp": "2025-07-25T15:05:32Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-25 14:40:05.614358 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-07-25T15:05:32Z" }, "spec": { "description": "Enables create, delete, and update mutations for resources owned by IAM identity", @@ -2237,7 +1817,7 @@ { "metadata": { "name": "kubernetesAuthzApis", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-18T07:43:01Z" }, "spec": { @@ -2247,24 +1827,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "kubernetesAuthzEndpoints", - "resourceVersion": "1758779666607", - "creationTimestamp": "2025-09-25T05:54:26Z", - "deletionTimestamp": "2025-09-25T17:10:48Z" - }, - "spec": { - "description": "Enables K8s AuthZ endpoints", - "stage": "experimental", - "codeowner": "@grafana/identity-access-team", - "hideFromDocs": true - } - }, { "metadata": { "name": "kubernetesAuthzResourcePermissionApis", - "resourceVersion": "1754668670559", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -2278,11 +1844,8 @@ { "metadata": { "name": "kubernetesAuthzZanzanaSync", - "resourceVersion": "1758887751768", - "creationTimestamp": "2025-10-13T19:37:13Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-26 11:55:51.768754 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-10-13T19:37:13Z" }, "spec": { "description": "Enable sync of Zanzana authorization store on AuthZ CRD mutations", @@ -2294,7 +1857,7 @@ { "metadata": { "name": "kubernetesCorrelations", - "resourceVersion": "1757513374180", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -2308,11 +1871,8 @@ { "metadata": { "name": "kubernetesDashboards", - "resourceVersion": "1763533458962", - "creationTimestamp": "2024-06-05T14:34:23Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 06:24:18.962973 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2024-06-05T14:34:23Z" }, "spec": { "description": "Use the kubernetes API in the frontend for dashboards", @@ -2324,7 +1884,7 @@ { "metadata": { "name": "kubernetesFeatureToggles", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-18T05:32:44Z" }, "spec": { @@ -2337,7 +1897,7 @@ { "metadata": { "name": "kubernetesLibraryPanels", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-25T22:21:56Z" }, "spec": { @@ -2350,11 +1910,8 @@ { "metadata": { "name": "kubernetesLogsDrilldown", - "resourceVersion": "1760632282014", - "creationTimestamp": "2025-10-16T21:31:42Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-16 16:31:22.014483 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-10-16T21:31:42Z" }, "spec": { "description": "Adds support for Kubernetes logs drilldown", @@ -2366,7 +1923,7 @@ { "metadata": { "name": "kubernetesQueryCaching", - "resourceVersion": "1760972620939", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-20T16:11:25Z" }, "spec": { @@ -2379,12 +1936,9 @@ { "metadata": { "name": "kubernetesShortURLs", - "resourceVersion": "1756914263808", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-03 15:44:23.80856 +0000 UTC" - } + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables k8s short url api and uses it under the hood when handling legacy /api", @@ -2396,7 +1950,7 @@ { "metadata": { "name": "kubernetesSnapshots", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-12-05T22:31:49Z" }, "spec": { @@ -2409,7 +1963,7 @@ { "metadata": { "name": "kubernetesStars", - "resourceVersion": "1759149842036", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -2420,24 +1974,10 @@ "requiresRestart": true } }, - { - "metadata": { - "name": "kubernetesZanzanaPopulate", - "resourceVersion": "1758879077577", - "creationTimestamp": "2025-09-26T09:31:17Z", - "deletionTimestamp": "2025-09-26T09:35:02Z" - }, - "spec": { - "description": "Populate Zanzana on AuthZ CRDs creation or update", - "stage": "experimental", - "codeowner": "@grafana/identity-access-team", - "hideFromDocs": true - } - }, { "metadata": { "name": "localeFormatPreference", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-31T13:59:07Z" }, "spec": { @@ -2446,23 +1986,10 @@ "codeowner": "@grafana/grafana-frontend-platform" } }, - { - "metadata": { - "name": "localizationForPlugins", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-03-31T04:38:38Z", - "deletionTimestamp": "2025-08-29T14:46:39Z" - }, - "spec": { - "description": "Enables localization for plugins", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" - } - }, { "metadata": { "name": "logQLScope", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-11T11:53:24Z" }, "spec": { @@ -2476,7 +2003,7 @@ { "metadata": { "name": "logRequestsInstrumentedAsUnknown", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-06-10T08:56:55Z" }, "spec": { @@ -2485,25 +2012,10 @@ "codeowner": "@grafana/grafana-backend-group" } }, - { - "metadata": { - "name": "logRowsPopoverMenu", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-11-16T09:48:10Z", - "deletionTimestamp": "2025-11-07T10:57:27Z" - }, - "spec": { - "description": "Enable filtering menu displayed when text of a log line is selected", - "stage": "GA", - "codeowner": "@grafana/observability-logs", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "logsContextDatasourceUi", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-01-27T14:12:01Z" }, "spec": { @@ -2517,7 +2029,7 @@ { "metadata": { "name": "logsExploreTableDefaultVisualization", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-05-02T15:28:15Z" }, "spec": { @@ -2530,7 +2042,7 @@ { "metadata": { "name": "logsExploreTableVisualisation", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-07-12T13:52:42Z" }, "spec": { @@ -2541,25 +2053,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "logsInfiniteScrolling", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-11-09T10:54:03Z", - "deletionTimestamp": "2025-11-07T10:59:01Z" - }, - "spec": { - "description": "Enables infinite scrolling for the Logs panel in Explore and Dashboards", - "stage": "GA", - "codeowner": "@grafana/observability-logs", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "logsPanelControls", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-07T14:38:55Z" }, "spec": { @@ -2573,7 +2070,7 @@ { "metadata": { "name": "lokiExperimentalStreaming", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-06-19T10:03:51Z" }, "spec": { @@ -2585,7 +2082,7 @@ { "metadata": { "name": "lokiLabelNamesQueryApi", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-13T14:31:41Z" }, "spec": { @@ -2598,7 +2095,7 @@ { "metadata": { "name": "lokiLogsDataplane", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-07-13T07:58:00Z" }, "spec": { @@ -2607,10 +2104,23 @@ "codeowner": "@grafana/observability-logs" } }, + { + "metadata": { + "name": "lokiQueryLimitsContext", + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-21T12:11:03Z" + }, + "spec": { + "description": "Send X-Loki-Query-Limits-Context header to Loki on first split request", + "stage": "experimental", + "codeowner": "@grafana/observability-logs", + "frontend": true + } + }, { "metadata": { "name": "lokiQuerySplitting", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-02-09T17:27:02Z" }, "spec": { @@ -2624,7 +2134,7 @@ { "metadata": { "name": "lokiRunQueriesInParallel", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-09-19T09:34:01Z" }, "spec": { @@ -2636,7 +2146,7 @@ { "metadata": { "name": "lokiShardSplitting", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-23T11:21:03Z" }, "spec": { @@ -2649,7 +2159,7 @@ { "metadata": { "name": "managedDualWriter", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-02-19T14:50:39Z" }, "spec": { @@ -2662,7 +2172,7 @@ { "metadata": { "name": "metricsFromProfiles", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-09T10:55:28Z" }, "spec": { @@ -2675,7 +2185,7 @@ { "metadata": { "name": "mlExpressions", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-07-13T17:37:50Z" }, "spec": { @@ -2684,23 +2194,10 @@ "codeowner": "@grafana/alerting-squad" } }, - { - "metadata": { - "name": "multiTenantFrontend", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-04-25T09:24:25Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Register MT frontend", - "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform" - } - }, { "metadata": { "name": "multiTenantTempCredentials", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-02T20:25:50Z" }, "spec": { @@ -2713,7 +2210,7 @@ { "metadata": { "name": "mysqlAnsiQuotes", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-10-12T11:43:35Z" }, "spec": { @@ -2725,7 +2222,7 @@ { "metadata": { "name": "newClickhouseConfigPageDesign", - "resourceVersion": "1754075145003", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -2736,25 +2233,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "newDashboardSharingComponent", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-05-03T15:02:18Z", - "deletionTimestamp": "2025-08-29T14:46:39Z" - }, - "spec": { - "description": "Enables the new sharing drawer design", - "stage": "GA", - "codeowner": "@grafana/sharing-squad", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "newDashboardWithFiltersAndGroupBy", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-04T11:25:21Z" }, "spec": { @@ -2767,7 +2249,7 @@ { "metadata": { "name": "newFiltersUI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-08-30T12:48:13Z" }, "spec": { @@ -2780,7 +2262,7 @@ { "metadata": { "name": "newGauge", - "resourceVersion": "1760700645318", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-20T16:33:19Z" }, "spec": { @@ -2794,7 +2276,7 @@ { "metadata": { "name": "newInfluxDSConfigPageDesign", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-25T16:39:54Z" }, "spec": { @@ -2807,7 +2289,7 @@ { "metadata": { "name": "newLogContext", - "resourceVersion": "1754044501326", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z" }, "spec": { @@ -2820,11 +2302,8 @@ { "metadata": { "name": "newLogsPanel", - "resourceVersion": "1762166984808", - "creationTimestamp": "2025-02-04T17:40:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-03 10:49:44.808226 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-02-04T17:40:17Z" }, "spec": { "description": "Enables the new logs panel", @@ -2834,24 +2313,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "newPDFRendering", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-02-08T12:09:34Z", - "deletionTimestamp": "2025-11-14T14:39:41Z" - }, - "spec": { - "description": "New implementation for the dashboard-to-PDF rendering", - "stage": "GA", - "codeowner": "@grafana/grafana-operator-experience-squad", - "expression": "true" - } - }, { "metadata": { "name": "newPanelPadding", - "resourceVersion": "1760780310038", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-11-12T15:40:46Z" }, "spec": { @@ -2864,7 +2329,7 @@ { "metadata": { "name": "newShareReportDrawer", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-02-17T19:05:46Z" }, "spec": { @@ -2877,8 +2342,8 @@ { "metadata": { "name": "newTimeRangeZoomShortcuts", - "resourceVersion": "1763646782694", - "creationTimestamp": "2025-11-20T13:53:02Z" + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-21T12:11:03Z" }, "spec": { "description": "Enables new keyboard shortcuts for time range zoom operations", @@ -2890,7 +2355,7 @@ { "metadata": { "name": "newVizSuggestions", - "resourceVersion": "1762456851857", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-11-12T19:26:29Z" }, "spec": { @@ -2904,7 +2369,7 @@ { "metadata": { "name": "oauthRequireSubClaim", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-03-25T13:22:24Z" }, "spec": { @@ -2917,7 +2382,7 @@ { "metadata": { "name": "onPremToCloudMigrations", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-01-22T16:09:08Z" }, "spec": { @@ -2930,11 +2395,8 @@ { "metadata": { "name": "onlyStoreActionSets", - "resourceVersion": "1759844046154", - "creationTimestamp": "2025-10-20T15:02:56Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-07 13:34:06.15476 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-10-20T15:02:56Z" }, "spec": { "description": "When storing dashboard and folder resource permissions, only store action sets and not the full list of underlying permission", @@ -2947,7 +2409,7 @@ { "metadata": { "name": "otelLogsFormatting", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-16T15:42:14Z" }, "spec": { @@ -2960,7 +2422,7 @@ { "metadata": { "name": "panelFilterVariable", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-11-03T12:15:54Z" }, "spec": { @@ -2971,39 +2433,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "panelMonitoring", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-10-09T05:19:08Z", - "deletionTimestamp": "2025-11-07T19:04:42Z" - }, - "spec": { - "description": "Enables panel monitoring through logs and measurements", - "stage": "GA", - "codeowner": "@grafana/dataviz-squad", - "frontend": true, - "expression": "true" - } - }, - { - "metadata": { - "name": "panelPadding", - "resourceVersion": "1760779980125", - "creationTimestamp": "2025-10-18T09:33:00Z", - "deletionTimestamp": "2025-10-18T09:38:30Z" - }, - "spec": { - "description": "Increases panel padding globally", - "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "expression": "false" - } - }, { "metadata": { "name": "panelTimeSettings", - "resourceVersion": "1761555646368", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-29T08:06:23Z" }, "spec": { @@ -3015,7 +2448,7 @@ { "metadata": { "name": "panelTitleSearch", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-02-15T18:26:03Z" }, "spec": { @@ -3027,7 +2460,7 @@ { "metadata": { "name": "passwordlessMagicLinkAuthentication", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-14T13:50:55Z" }, "spec": { @@ -3040,7 +2473,7 @@ { "metadata": { "name": "pdfTables", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-11-06T13:39:22Z" }, "spec": { @@ -3052,7 +2485,7 @@ { "metadata": { "name": "permissionsFilterRemoveSubquery", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-08-02T07:39:25Z" }, "spec": { @@ -3061,27 +2494,10 @@ "codeowner": "@grafana/search-and-storage" } }, - { - "metadata": { - "name": "pinNavItems", - "resourceVersion": "1762958248290", - "creationTimestamp": "2024-06-10T11:40:03Z", - "deletionTimestamp": "2025-11-17T12:12:47Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" - } - }, - "spec": { - "description": "Enables pinning of nav items", - "stage": "GA", - "codeowner": "@grafana/grafana-search-navigate-organise", - "expression": "true" - } - }, { "metadata": { "name": "playlistsReconciler", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-20T03:09:31Z" }, "spec": { @@ -3091,26 +2507,10 @@ "requiresRestart": true } }, - { - "metadata": { - "name": "pluginAssetProvider", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-07-17T15:20:35Z", - "deletionTimestamp": "2025-10-10T09:35:22Z" - }, - "spec": { - "description": "Allows decoupled core plugins to load from the Grafana CDN", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "requiresRestart": true, - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "pluginContainers", - "resourceVersion": "1756911074581", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -3125,7 +2525,7 @@ { "metadata": { "name": "pluginInstallAPISync", - "resourceVersion": "1760543624249", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-24T12:09:26Z" }, "spec": { @@ -3138,7 +2538,7 @@ { "metadata": { "name": "pluginProxyPreserveTrailingSlash", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-05T11:36:14Z" }, "spec": { @@ -3151,11 +2551,8 @@ { "metadata": { "name": "pluginStoreServiceLoading", - "resourceVersion": "1761144346944", - "creationTimestamp": "2025-10-17T20:01:43Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-22 14:45:46.944669 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-10-17T20:01:43Z" }, "spec": { "description": "Load plugins on store service startup instead of wire provider, and call RegisterFixedRoles after all plugins are loaded", @@ -3167,7 +2564,7 @@ { "metadata": { "name": "pluginsAutoUpdate", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-16T11:44:39Z" }, "spec": { @@ -3176,36 +2573,10 @@ "codeowner": "@grafana/plugins-platform-backend" } }, - { - "metadata": { - "name": "pluginsFrontendSandbox", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-06-05T08:51:36Z", - "deletionTimestamp": "2025-11-06T10:06:53Z" - }, - "spec": { - "description": "Enables the plugins frontend sandbox", - "stage": "privatePreview", - "codeowner": "@grafana/plugins-platform-backend" - } - }, - { - "metadata": { - "name": "pluginsSkipHostEnvVars", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-11-15T17:09:14Z", - "deletionTimestamp": "2025-11-13T15:31:57Z" - }, - "spec": { - "description": "Disables passing host environment variable to plugin processes", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" - } - }, { "metadata": { "name": "pluginsSriChecks", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-04T12:55:09Z" }, "spec": { @@ -3218,7 +2589,7 @@ { "metadata": { "name": "postgresDSUsePGX", - "resourceVersion": "1753174666056", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-05-26T06:54:18Z", "deletionTimestamp": "2025-06-03T12:45:07Z" }, @@ -3231,7 +2602,7 @@ { "metadata": { "name": "preferLibraryPanelTitle", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-17T11:21:21Z" }, "spec": { @@ -3241,24 +2612,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "preinstallAutoUpdate", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-11-07T12:14:25Z", - "deletionTimestamp": "2025-11-10T14:06:30Z" - }, - "spec": { - "description": "Enables automatic updates for pre-installed plugins", - "stage": "GA", - "codeowner": "@grafana/plugins-platform-backend", - "expression": "true" - } - }, { "metadata": { "name": "preserveDashboardStateWhenNavigating", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-05-27T12:28:06Z" }, "spec": { @@ -3271,7 +2628,7 @@ { "metadata": { "name": "preventPanelChromeOverflow", - "resourceVersion": "1760704390127", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-17T14:40:08Z" }, "spec": { @@ -3282,25 +2639,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "promQLScope", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-01-29T20:22:17Z", - "deletionTimestamp": "2025-10-10T14:53:18Z" - }, - "spec": { - "description": "In-development feature that will allow injection of labels into prometheus queries.", - "stage": "GA", - "codeowner": "@grafana/oss-big-tent", - "hideFromDocs": true, - "expression": "true" - } - }, { "metadata": { "name": "prometheusAzureOverrideAudience", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-05-30T15:43:32Z", "deletionTimestamp": "2023-07-16T21:30:14Z" }, @@ -3311,24 +2653,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "prometheusCodeModeMetricNamesSearch", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-04-04T20:38:23Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names", - "stage": "experimental", - "codeowner": "@grafana/oss-big-tent", - "frontend": true - } - }, { "metadata": { "name": "prometheusSpecialCharsInLabelValues", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-18T21:31:08Z" }, "spec": { @@ -3341,12 +2669,9 @@ { "metadata": { "name": "prometheusTypeMigration", - "resourceVersion": "1757089774247", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-05 16:29:34.247055837 +0000 UTC" - } + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Checks for deprecated Prometheus authentication methods (SigV4 and Azure), installs the relevant data source, and migrates the Prometheus data sources", @@ -3359,7 +2684,7 @@ { "metadata": { "name": "provisioning", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-22T09:03:50Z" }, "spec": { @@ -3372,7 +2697,7 @@ { "metadata": { "name": "publicDashboardsEmailSharing", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-01-03T19:45:15Z" }, "spec": { @@ -3385,7 +2710,7 @@ { "metadata": { "name": "publicDashboardsScene", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-03-22T14:48:21Z" }, "spec": { @@ -3399,7 +2724,7 @@ { "metadata": { "name": "queryCacheRequestDeduplication", - "resourceVersion": "1757521912495", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -3413,12 +2738,9 @@ { "metadata": { "name": "queryLibrary", - "resourceVersion": "1758208636622", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-10-07T18:31:45Z", - "deletionTimestamp": "2023-03-20T16:00:14Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-18 15:17:16.622111 +0000 UTC" - } + "deletionTimestamp": "2023-03-20T16:00:14Z" }, "spec": { "description": "Enables Saved queries (query library) feature", @@ -3429,7 +2751,7 @@ { "metadata": { "name": "queryService", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -3442,7 +2764,7 @@ { "metadata": { "name": "queryServiceFromExplore", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-02T10:00:33Z" }, "spec": { @@ -3455,7 +2777,7 @@ { "metadata": { "name": "queryServiceFromUI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -3468,7 +2790,7 @@ { "metadata": { "name": "queryServiceRewrite", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -3481,7 +2803,7 @@ { "metadata": { "name": "queryServiceWithConnections", - "resourceVersion": "1756367172351", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-28T19:28:26Z", "deletionTimestamp": "2025-08-29T12:49:57Z" }, @@ -3492,24 +2814,10 @@ "requiresRestart": true } }, - { - "metadata": { - "name": "recordedQueriesMulti", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-06-14T12:34:22Z", - "deletionTimestamp": "2025-11-10T20:31:43Z" - }, - "spec": { - "description": "Enables writing multiple items from a single query within Recorded Queries", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "expression": "true" - } - }, { "metadata": { "name": "refactorVariablesTimeRange", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-06-06T13:12:09Z" }, "spec": { @@ -3521,8 +2829,8 @@ { "metadata": { "name": "refreshTokenRequired", - "resourceVersion": "1763561990273", - "creationTimestamp": "2025-11-19T14:19:50Z" + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-21T12:11:03Z" }, "spec": { "description": "Require that refresh tokens are present in oauth tokens.", @@ -3531,27 +2839,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "regressionTransformation", - "resourceVersion": "1762442825881", - "creationTimestamp": "2023-11-24T14:49:16Z", - "deletionTimestamp": "2025-07-01T13:59:22Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-06 15:27:05.88172 +0000 UTC" - } - }, - "spec": { - "description": "Enables regression analysis transformation", - "stage": "preview", - "codeowner": "@grafana/datapro", - "frontend": true - } - }, { "metadata": { "name": "reloadDashboardsOnParamsChange", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-25T12:56:54Z" }, "spec": { @@ -3564,7 +2855,7 @@ { "metadata": { "name": "renderAuthJWT", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-04-03T16:53:38Z" }, "spec": { @@ -3576,7 +2867,7 @@ { "metadata": { "name": "rendererDisableAppPluginsPreload", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-02-24T14:43:06Z" }, "spec": { @@ -3590,7 +2881,7 @@ { "metadata": { "name": "reportingRetries", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-08-31T07:47:47Z" }, "spec": { @@ -3603,11 +2894,8 @@ { "metadata": { "name": "restoreDashboards", - "resourceVersion": "1762958248290", - "creationTimestamp": "2025-05-23T14:35:54Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-05-23T14:35:54Z" }, "spec": { "description": "Enables restore deleted dashboards feature", @@ -3619,12 +2907,9 @@ { "metadata": { "name": "restrictedPluginApis", - "resourceVersion": "1753776783657", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-29 08:13:03.657209 +0000 UTC" - } + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Enables sharing a list of APIs with a list of plugins", @@ -3638,7 +2923,7 @@ { "metadata": { "name": "rolePickerDrawer", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-26T12:51:38Z" }, "spec": { @@ -3650,8 +2935,8 @@ { "metadata": { "name": "rudderstackUpgrade", - "resourceVersion": "1763481217872", - "creationTimestamp": "2025-11-18T15:53:37Z" + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-21T12:11:03Z" }, "spec": { "description": "Enables the new version of rudderstack", @@ -3661,26 +2946,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "savedQueries", - "resourceVersion": "1756920131554", - "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-03 17:22:11.554759 +0000 UTC" - } - }, - "spec": { - "description": "Enables Saved Queries feature", - "stage": "preview", - "codeowner": "@grafana/sharing-squad" - } - }, { "metadata": { "name": "scanRowInvalidDashboardParseFallbackEnabled", - "resourceVersion": "1753730899886", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-30T14:18:38Z" }, "spec": { @@ -3692,7 +2961,7 @@ { "metadata": { "name": "scopeApi", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-11-27T07:58:25Z" }, "spec": { @@ -3705,7 +2974,7 @@ { "metadata": { "name": "scopeFilters", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-03-05T15:41:19Z" }, "spec": { @@ -3718,11 +2987,8 @@ { "metadata": { "name": "scopeSearchAllLevels", - "resourceVersion": "1762958248290", - "creationTimestamp": "2025-04-14T07:42:16Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-04-14T07:42:16Z" }, "spec": { "description": "Enable scope search to include all levels of the scope node tree", @@ -3734,7 +3000,7 @@ { "metadata": { "name": "secretsManagementAppPlatform", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-19T09:25:14Z" }, "spec": { @@ -3746,7 +3012,7 @@ { "metadata": { "name": "secretsManagementAppPlatformUI", - "resourceVersion": "1756816818369", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -3759,11 +3025,8 @@ { "metadata": { "name": "sharingDashboardImage", - "resourceVersion": "1761782859498", - "creationTimestamp": "2025-07-15T21:07:39Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-30 00:07:39.498343 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-07-15T21:07:39Z" }, "spec": { "description": "Enables image sharing functionality for dashboards", @@ -3776,7 +3039,7 @@ { "metadata": { "name": "showDashboardValidationWarnings", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-10-14T13:51:05Z" }, "spec": { @@ -3785,43 +3048,11 @@ "codeowner": "@grafana/dashboards-squad" } }, - { - "metadata": { - "name": "skipTokenRotationIfRecent", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-06-03T06:59:40Z", - "deletionTimestamp": "2025-10-23T08:02:41Z" - }, - "spec": { - "description": "Skip token rotation if it was already rotated less than 5 seconds ago", - "stage": "GA", - "codeowner": "@grafana/identity-access-team", - "hideFromDocs": true, - "expression": "true" - } - }, - { - "metadata": { - "name": "sqlDatasourceDatabaseSelection", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-06-06T16:28:52Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Enables previous SQL data source dataset dropdown behavior", - "stage": "preview", - "codeowner": "@grafana/oss-big-tent", - "frontend": true - } - }, { "metadata": { "name": "sqlExpressions", - "resourceVersion": "1756828051955", - "creationTimestamp": "2024-02-27T21:16:00Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-02 15:47:31.955288671 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2024-02-27T21:16:00Z" }, "spec": { "description": "Enables SQL Expressions, which can execute SQL queries against data source results.", @@ -3832,7 +3063,7 @@ { "metadata": { "name": "sqlExpressionsColumnAutoComplete", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-23T21:49:58Z" }, "spec": { @@ -3845,11 +3076,8 @@ { "metadata": { "name": "sseGroupByDatasource", - "resourceVersion": "1762552416963", - "creationTimestamp": "2023-09-07T20:02:07Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-07 21:53:36.963146843 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2023-09-07T20:02:07Z" }, "spec": { "description": "Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch.", @@ -3860,7 +3088,7 @@ { "metadata": { "name": "ssoSettingsLDAP", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-18T11:31:27Z" }, "spec": { @@ -3874,12 +3102,9 @@ { "metadata": { "name": "starsFromAPIServer", - "resourceVersion": "1762958248290", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" - } + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "populate star status from apiserver", @@ -3892,7 +3117,7 @@ { "metadata": { "name": "storage", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2022-03-17T17:19:23Z" }, "spec": { @@ -3904,11 +3129,8 @@ { "metadata": { "name": "suggestedDashboards", - "resourceVersion": "1763643877862", - "creationTimestamp": "2025-11-07T13:38:59Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-20 13:04:37.862907 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-07T13:38:59Z" }, "spec": { "description": "Displays datasource provisioned and community dashboards in dashboard empty page, only when coming from datasource configuration page", @@ -3916,24 +3138,10 @@ "codeowner": "@grafana/sharing-squad" } }, - { - "metadata": { - "name": "tableNextGen", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-03-26T03:57:57Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Allows access to the new react-data-grid based table component.", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true - } - }, { "metadata": { "name": "tableSharedCrosshair", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-12-13T09:33:14Z" }, "spec": { @@ -3946,7 +3154,7 @@ { "metadata": { "name": "tabularNumbers", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-06-24T11:52:03Z" }, "spec": { @@ -3959,7 +3167,7 @@ { "metadata": { "name": "teamFolders", - "resourceVersion": "1755099058649", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -3970,24 +3178,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "teamHttpHeadersMimir", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-01-13T10:42:47Z", - "deletionTimestamp": "2025-07-31T22:56:50Z" - }, - "spec": { - "description": "Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams", - "stage": "GA", - "codeowner": "@grafana/identity-access-team", - "expression": "true" - } - }, { "metadata": { "name": "teamHttpHeadersTempo", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-05-22T19:13:31Z" }, "spec": { @@ -3996,42 +3190,11 @@ "codeowner": "@grafana/identity-access-team" } }, - { - "metadata": { - "name": "templateDashboards", - "resourceVersion": "1758897202631", - "creationTimestamp": "2025-09-26T14:33:22Z", - "deletionTimestamp": "2025-09-26T16:02:12Z" - }, - "spec": { - "description": "Enables template dashboards suggestions when creating new dashboards", - "stage": "experimental", - "codeowner": "@grafana/sharing-squad", - "frontend": true - } - }, - { - "metadata": { - "name": "templateVariablesUsesCombobox", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-01-31T09:53:13Z", - "deletionTimestamp": "2025-11-13T03:31:18Z" - }, - "spec": { - "description": "Use new **Combobox** component for template variables", - "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform", - "frontend": true - } - }, { "metadata": { "name": "tempoAlerting", - "resourceVersion": "1763532089512", - "creationTimestamp": "2025-07-15T13:36:36Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-19 06:01:29.512182 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-07-15T13:36:36Z" }, "spec": { "description": "Enables creating alerts from Tempo data source", @@ -4042,12 +3205,9 @@ { "metadata": { "name": "tempoSearchBackendMigration", - "resourceVersion": "1758029567165", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-16 13:32:47.165146 +0000 UTC" - } + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Run search queries through the tempo backend", @@ -4060,7 +3220,7 @@ { "metadata": { "name": "timeComparison", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-24T20:07:28Z" }, "spec": { @@ -4073,12 +3233,8 @@ { "metadata": { "name": "timeRangePan", - "resourceVersion": "1762290731154", - "creationTimestamp": "2025-11-05T01:39:46Z", - "deletionTimestamp": "2025-11-06T17:39:31Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-04 21:12:11.154822 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-05T01:39:46Z" }, "spec": { "description": "Enables time range panning functionality", @@ -4090,7 +3246,7 @@ { "metadata": { "name": "timeRangeProvider", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-22T10:52:33Z" }, "spec": { @@ -4099,28 +3255,11 @@ "codeowner": "@grafana/grafana-frontend-platform" } }, - { - "metadata": { - "name": "tlsMemcached", - "resourceVersion": "1753448760331", - "creationTimestamp": "2024-05-09T19:12:08Z", - "deletionTimestamp": "2025-11-12T15:49:28Z" - }, - "spec": { - "description": "Use TLS-enabled memcached in the enterprise caching feature", - "stage": "GA", - "codeowner": "@grafana/grafana-operator-experience-squad", - "expression": "true" - } - }, { "metadata": { "name": "transformationsEmptyPlaceholder", - "resourceVersion": "1763373021129", - "creationTimestamp": "2025-11-17T13:57:05Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-17 09:50:21.129721 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-11-17T13:57:05Z" }, "spec": { "description": "Show transformation quick-start cards in empty transformations state", @@ -4129,25 +3268,10 @@ "frontend": true } }, - { - "metadata": { - "name": "transformationsRedesign", - "resourceVersion": "1753448760331", - "creationTimestamp": "2023-07-12T16:35:49Z", - "deletionTimestamp": "2025-11-06T20:37:15Z" - }, - "spec": { - "description": "Enables the transformations redesign", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "ttlPluginInstanceManager", - "resourceVersion": "1763462850634", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-11-18T11:17:23Z" }, "spec": { @@ -4160,11 +3284,8 @@ { "metadata": { "name": "unifiedHistory", - "resourceVersion": "1762958248290", - "creationTimestamp": "2024-12-13T10:41:18Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2024-12-13T10:41:18Z" }, "spec": { "description": "Displays the navigation history so the user can navigate back to previous pages", @@ -4176,7 +3297,7 @@ { "metadata": { "name": "unifiedNavbars", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-04-09T12:51:22Z" }, "spec": { @@ -4190,7 +3311,7 @@ { "metadata": { "name": "unifiedRequestLog", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2023-03-31T13:38:09Z" }, "spec": { @@ -4203,7 +3324,7 @@ { "metadata": { "name": "unifiedStorageBigObjectsSupport", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-10-17T10:18:29Z" }, "spec": { @@ -4215,7 +3336,7 @@ { "metadata": { "name": "unifiedStorageGrpcConnectionPool", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-03-21T13:24:54Z" }, "spec": { @@ -4225,25 +3346,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "unifiedStorageHistoryPruner", - "resourceVersion": "1753448760331", - "creationTimestamp": "2025-03-17T10:36:38Z", - "deletionTimestamp": "2025-11-17T19:47:37Z" - }, - "spec": { - "description": "Enables the unified storage history pruner", - "stage": "GA", - "codeowner": "@grafana/search-and-storage", - "hideFromDocs": true, - "expression": "true" - } - }, { "metadata": { "name": "unifiedStorageSearch", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-30T19:46:14Z" }, "spec": { @@ -4253,29 +3359,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "unifiedStorageSearchAfterWriteExperimentalAPI", - "resourceVersion": "1755089543487", - "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-08-13 12:52:23.487521 +0000 UTC" - } - }, - "spec": { - "description": "Enable experimental search-after-write guarantees to unified-storage search endpoints", - "stage": "experimental", - "codeowner": "@grafana/search-and-storage", - "requiresRestart": true, - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "unifiedStorageSearchDualReaderEnabled", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-18T12:43:56Z" }, "spec": { @@ -4288,7 +3375,7 @@ { "metadata": { "name": "unifiedStorageSearchSprinkles", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-18T17:00:54Z" }, "spec": { @@ -4301,7 +3388,7 @@ { "metadata": { "name": "unifiedStorageSearchUI", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-12-19T18:21:48Z" }, "spec": { @@ -4311,24 +3398,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "unifiedStorageUseFullNgram", - "resourceVersion": "1758820248165", - "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z" - }, - "spec": { - "description": "Use full n-gram indexing instead of edge n-gram for unified storage search", - "stage": "experimental", - "codeowner": "@grafana/search-and-storage", - "hideFromDocs": true - } - }, { "metadata": { "name": "unlimitedLayoutsNesting", - "resourceVersion": "1760013838902", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-10-10T12:15:54Z" }, "spec": { @@ -4341,12 +3414,9 @@ { "metadata": { "name": "useKubernetesShortURLsAPI", - "resourceVersion": "1756914263808", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", - "deletionTimestamp": "2025-09-01T09:33:33Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-03 15:44:23.80856 +0000 UTC" - } + "deletionTimestamp": "2025-09-01T09:33:33Z" }, "spec": { "description": "Routes short url requests from /api to the /apis endpoint in the frontend. Depends on kubernetesShortURLs", @@ -4358,7 +3428,7 @@ { "metadata": { "name": "useMultipleScopeNodesEndpoint", - "resourceVersion": "1759237515008", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -4374,7 +3444,7 @@ { "metadata": { "name": "useScopeSingleNodeEndpoint", - "resourceVersion": "1753960766702", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T14:32:41Z" }, "spec": { @@ -4389,11 +3459,8 @@ { "metadata": { "name": "useScopesNavigationEndpoint", - "resourceVersion": "1762958248290", - "creationTimestamp": "2025-03-31T15:20:00Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-03-31T15:20:00Z" }, "spec": { "description": "Use the scopes navigation endpoint instead of the dashboardbindings endpoint", @@ -4406,7 +3473,7 @@ { "metadata": { "name": "useSessionStorageForRedirection", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-09-23T09:31:23Z" }, "spec": { @@ -4419,12 +3486,9 @@ { "metadata": { "name": "vizActionsAuth", - "resourceVersion": "1756904995830", + "resourceVersion": "1763727063618", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-09-03 13:09:55.830412 +0000 UTC" - } + "deletionTimestamp": "2025-08-01T11:30:17Z" }, "spec": { "description": "Allows authenticated API calls in actions", @@ -4437,7 +3501,7 @@ { "metadata": { "name": "zanzana", - "resourceVersion": "1753448760331", + "resourceVersion": "1763727063618", "creationTimestamp": "2024-06-19T13:59:47Z" }, "spec": { @@ -4450,11 +3514,8 @@ { "metadata": { "name": "zanzanaNoLegacyClient", - "resourceVersion": "1761063016739", - "creationTimestamp": "2025-10-21T14:03:17Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-10-21 16:10:16.739546 +0000 UTC" - } + "resourceVersion": "1763727063618", + "creationTimestamp": "2025-10-21T14:03:17Z" }, "spec": { "description": "Use openFGA as main authorization engine and disable legacy RBAC clietn.", diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index eb7f72e6570..8d5d4c6282f 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -96,6 +96,8 @@ func makeDataRequest(ctx context.Context, lokiDsUrl string, query lokiQuery) (*h return nil, backend.DownstreamError(fmt.Errorf("failed to create request: %w", err)) } + addQueryLimitsHeader(query, req) + if query.SupportingQueryType != SupportingQueryNone { value := getSupportingQueryHeaderValue(query.SupportingQueryType) if value != "" { @@ -108,6 +110,15 @@ func makeDataRequest(ctx context.Context, lokiDsUrl string, query lokiQuery) (*h return req, nil } +func addQueryLimitsHeader(query lokiQuery, req *http.Request) { + if len(query.LimitsContext.Expr) > 0 { + queryLimitStr, err := json.Marshal(query.LimitsContext) + if err == nil { + req.Header.Set("X-Loki-Query-Limits-Context", string(queryLimitStr)) + } + } +} + type lokiResponseError struct { Message string `json:"message"` TraceID string `json:"traceID,omitempty"` diff --git a/pkg/tsdb/loki/api_test.go b/pkg/tsdb/loki/api_test.go index 29b15e7fb18..415f807cf10 100644 --- a/pkg/tsdb/loki/api_test.go +++ b/pkg/tsdb/loki/api_test.go @@ -2,10 +2,12 @@ package loki import ( "context" + "encoding/json" "fmt" "net/http" "strings" "testing" + "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/tsdb/loki/kinds/dataquery" @@ -47,6 +49,56 @@ func TestApiLogVolume(t *testing.T) { require.True(t, called) }) + t.Run("X-Loki-Query-Limits-Context header should be set when LimitsContext is provided", func(t *testing.T) { + called := false + from := time.Now().Truncate(time.Millisecond).Add(-1 * time.Hour) + to := time.Now().Truncate(time.Millisecond) + limitsContext := LimitsContext{ + Expr: "{cluster=\"us-central1\"}", + From: from, + To: to, + } + + limitsContextJson, _ := json.Marshal(limitsContext) + api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, string(limitsContextJson), req.Header.Get("X-Loki-Query-Limits-Context")) + }) + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryLogsSample, QueryType: QueryTypeRange, LimitsContext: limitsContext}, ResponseOpts{}) + require.NoError(t, err) + require.True(t, called) + }) + + t.Run("X-Loki-Query-Limits-Context header should not get set when LimitsContext is missing expr", func(t *testing.T) { + called := false + from := time.Now().Truncate(time.Millisecond).Add(-1 * time.Hour) + to := time.Now().Truncate(time.Millisecond) + limitsContext := LimitsContext{ + Expr: "", + From: from, + To: to, + } + + api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, "", req.Header.Get("X-Loki-Query-Limits-Context")) + }) + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryLogsSample, QueryType: QueryTypeRange, LimitsContext: limitsContext}, ResponseOpts{}) + require.NoError(t, err) + require.True(t, called) + }) + + t.Run("X-Loki-Query-Limits-Context header should not get set when LimitsContext is not provided", func(t *testing.T) { + called := false + api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { + called = true + require.Equal(t, "", req.Header.Get("X-Loki-Query-Limits-Context")) + }) + _, err := api.DataQuery(context.Background(), lokiQuery{Expr: "", SupportingQueryType: SupportingQueryLogsSample, QueryType: QueryTypeRange}, ResponseOpts{}) + require.NoError(t, err) + require.True(t, called) + }) + t.Run("data sample queries should set data sample http header", func(t *testing.T) { called := false api := makeMockedAPI(200, "application/json", response, func(req *http.Request) { diff --git a/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go index c9764b174fd..9826c494599 100644 --- a/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go @@ -18,6 +18,17 @@ const ( QueryEditorModeBuilder QueryEditorMode = "builder" ) +type LimitsContext struct { + Expr string `json:"expr"` + From int64 `json:"from"` + To int64 `json:"to"` +} + +// NewLimitsContext creates a new LimitsContext object. +func NewLimitsContext() *LimitsContext { + return &LimitsContext{} +} + type LokiQueryType string const ( @@ -59,6 +70,8 @@ type LokiDataQuery struct { Instant *bool `json:"instant,omitempty"` // Used to set step value for range queries. Step *string `json:"step,omitempty"` + // The full query plan for split/shard queries. Encoded and sent to Loki via `X-Loki-Query-Limits-Context` header. Requires "lokiQueryLimitsContext" feature flag + LimitsContext *LimitsContext `json:"limitsContext,omitempty"` // A unique identifier for the query within the list of targets. // In server side expressions, the refId is used as a variable name to identify results. // By default, the UI will assign A->Z; however setting meaningful names may be useful. diff --git a/pkg/tsdb/loki/parse_query.go b/pkg/tsdb/loki/parse_query.go index 06cf90cb339..0bbf76c4f05 100644 --- a/pkg/tsdb/loki/parse_query.go +++ b/pkg/tsdb/loki/parse_query.go @@ -156,6 +156,8 @@ func parseQuery(queryContext *backend.QueryDataRequest, logqlScopesEnabled bool) expr := interpolateVariables(model.Expr, interval, timeRange, queryType, step) + limitsConfig := generateLimitsConfig(model, interval, timeRange, queryType, step) + direction, err := parseDirection(model.Direction) if err != nil { return nil, err @@ -192,8 +194,21 @@ func parseQuery(queryContext *backend.QueryDataRequest, logqlScopesEnabled bool) RefID: query.RefID, SupportingQueryType: supportingQueryType, Scopes: model.Scopes, + LimitsContext: limitsConfig, }) } return qs, nil } + +func generateLimitsConfig(model *QueryJSONModel, interval time.Duration, timeRange time.Duration, queryType QueryType, step time.Duration) LimitsContext { + var limitsConfig LimitsContext + // Only supply limits context config if we have expression, and from and to + if model.LimitsContext != nil && model.LimitsContext.Expr != "" && model.LimitsContext.From > 0 && model.LimitsContext.To > 0 { + // If a limits expression was provided, interpolate it and parse the time range + limitsConfig.Expr = interpolateVariables(model.LimitsContext.Expr, interval, timeRange, queryType, step) + limitsConfig.From = time.UnixMilli(model.LimitsContext.From) + limitsConfig.To = time.UnixMilli(model.LimitsContext.To) + } + return limitsConfig +} diff --git a/pkg/tsdb/loki/parse_query_test.go b/pkg/tsdb/loki/parse_query_test.go index 497f1c880c8..f8028e0dcfe 100644 --- a/pkg/tsdb/loki/parse_query_test.go +++ b/pkg/tsdb/loki/parse_query_test.go @@ -1,6 +1,7 @@ package loki import ( + "strconv" "testing" "time" @@ -145,6 +146,74 @@ func TestParseQuery(t *testing.T) { require.Equal(t, `{namespace="logish"} |= "problems"`, models[0].Expr) }) + t.Run("parsing query model with invalid query limits context expr", func(t *testing.T) { + from := time.Now().Add(-3000 * time.Second) + fullFrom := time.Now().Add(-1 * time.Hour) + to := time.Now() + + queryContext := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(` + { + "expr": "count_over_time({service_name=\"apache\", __stream_shard__=\"2\"}[$__auto])", + "format": "time_series", + "refId": "A", + "limitsContext": {"expr": "", "from": ` + strconv.FormatInt(fullFrom.UnixMilli(), 10) + `, "to": ` + strconv.FormatInt(to.UnixMilli(), 10) + `} + }`, + ), + TimeRange: backend.TimeRange{ + From: from, + To: to, + }, + Interval: time.Second * 15, + MaxDataPoints: 200, + }, + }, + } + models, err := parseQuery(queryContext, true) + require.NoError(t, err) + require.Equal(t, `count_over_time({service_name="apache", __stream_shard__="2"}[15s])`, models[0].Expr) + // If the limits context expression is missing, we don't set any limits context + require.Equal(t, ``, models[0].LimitsContext.Expr) + require.Equal(t, time.Time{}, models[0].LimitsContext.To) + require.Equal(t, time.Time{}, models[0].LimitsContext.From) + }) + + t.Run("parsing query model with query limits context", func(t *testing.T) { + from := time.Now().Add(-3000 * time.Second) + fullFrom := time.Now().Add(-1 * time.Hour) + to := time.Now() + + queryContext := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(` + { + "expr": "count_over_time({service_name=\"apache\", __stream_shard__=\"2\"}[$__auto])", + "format": "time_series", + "refId": "A", + "limitsContext": {"expr": "count_over_time({service_name=\"apache\"}[$__auto])", "from": ` + strconv.FormatInt(fullFrom.UnixMilli(), 10) + `, "to": ` + strconv.FormatInt(to.UnixMilli(), 10) + `} + }`, + ), + TimeRange: backend.TimeRange{ + From: from, + To: to, + }, + Interval: time.Second * 15, + MaxDataPoints: 200, + }, + }, + } + models, err := parseQuery(queryContext, true) + require.NoError(t, err) + require.Equal(t, time.Second*15, models[0].Step) + require.Equal(t, `count_over_time({service_name="apache", __stream_shard__="2"}[15s])`, models[0].Expr) + require.Equal(t, `count_over_time({service_name="apache"}[15s])`, models[0].LimitsContext.Expr) + require.Equal(t, to.Truncate(time.Millisecond), models[0].LimitsContext.To) + require.Equal(t, fullFrom.Truncate(time.Millisecond), models[0].LimitsContext.From) + }) + t.Run("interpolate variables, range between 1s and 0.5s", func(t *testing.T) { expr := "go_goroutines $__interval $__interval_ms $__range $__range_s $__range_ms" queryType := dataquery.LokiQueryTypeRange diff --git a/pkg/tsdb/loki/types.go b/pkg/tsdb/loki/types.go index 0d6156ed158..dd86595ada0 100644 --- a/pkg/tsdb/loki/types.go +++ b/pkg/tsdb/loki/types.go @@ -11,6 +11,11 @@ import ( type QueryType = dataquery.LokiQueryType type SupportingQueryType = dataquery.SupportingQueryType type Direction = dataquery.LokiQueryDirection +type LimitsContext struct { + Expr string + From time.Time + To time.Time +} const ( QueryTypeRange = dataquery.LokiQueryTypeRange @@ -42,4 +47,5 @@ type lokiQuery struct { RefID string SupportingQueryType SupportingQueryType Scopes []scope.ScopeFilter + LimitsContext LimitsContext } diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 055f4d454b7..d1e7bc6b461 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -774,6 +774,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { diff --git a/public/app/features/explore/Logs/LogsVolumePanelList.tsx b/public/app/features/explore/Logs/LogsVolumePanelList.tsx index 4d0556cfdb5..cfd35f7a73c 100644 --- a/public/app/features/explore/Logs/LogsVolumePanelList.tsx +++ b/public/app/features/explore/Logs/LogsVolumePanelList.tsx @@ -26,7 +26,7 @@ import { mergeLogsVolumeDataFrames, isLogsVolumeLimited, getLogsVolumeMaximumRan import { SupplementaryResultError } from '../SupplementaryResultError'; import { LogsVolumePanel } from './LogsVolumePanel'; -import { isTimeoutErrorResponse } from './utils/logsVolumeResponse'; +import { isMaxBytesErrorResponse, isTimeoutErrorResponse } from './utils/logsVolumeResponse'; type Props = { logsVolumeData: DataQueryResponse | undefined; @@ -93,7 +93,8 @@ export const LogsVolumePanelList = ({ const canShowPartialData = config.featureToggles.lokiShardSplitting && logsVolumeData && logsVolumeData.data.length > 0; const timeoutError = isTimeoutErrorResponse(logsVolumeData); - + const maxBytesError = isMaxBytesErrorResponse(logsVolumeData); + const queryTooLargeError = timeoutError || maxBytesError; const from = dateTime(Math.max(absoluteRange.from, allLogsVolumeMaximumRange.from)); const to = dateTime(Math.min(absoluteRange.to, allLogsVolumeMaximumRange.to)); const visibleRange: TimeRange = { from, to, raw: { from, to } }; @@ -123,7 +124,7 @@ export const LogsVolumePanelList = ({ Loading... ); - } else if (timeoutError && !canShowPartialData) { + } else if (queryTooLargeError && !canShowPartialData) { return (

- - The query is trying to access too much data. Try one or more of the following: - + {timeoutError && ( + + The query is trying to access too much data. Try one or more of the following: + + )} + {maxBytesError && ( + + The query would read too many bytes. Try one or more of the following: + + )}

  • @@ -184,10 +192,24 @@ export const LogsVolumePanelList = ({ return (
    - {timeoutError && canShowPartialData && ( + {queryTooLargeError && canShowPartialData && ( diff --git a/public/app/features/explore/Logs/utils/logsVolumeResponse.test.ts b/public/app/features/explore/Logs/utils/logsVolumeResponse.test.ts index b29f1798767..c383ec2e05b 100644 --- a/public/app/features/explore/Logs/utils/logsVolumeResponse.test.ts +++ b/public/app/features/explore/Logs/utils/logsVolumeResponse.test.ts @@ -1,6 +1,6 @@ import { DataQueryResponse } from '@grafana/data'; -import { isTimeoutErrorResponse } from './logsVolumeResponse'; +import { isMaxBytesErrorResponse, isTimeoutErrorResponse } from './logsVolumeResponse'; const errorA = 'Get "http://localhost:3100/loki/api/v1/query_range?direction=backward&end=1680001200000000000&limit=1000&query=sum+by+%28level%29+%28count_over_time%28%7Bcontainer_name%3D%22docker-compose-app-1%22%7D%5B1h%5D%29%29&start=1679914800000000000&step=3600000ms": net/http: request canceled (Client.Timeout exceeded while awaiting headers)'; @@ -17,6 +17,7 @@ describe('isTimeoutErrorResponse', () => { }, }; expect(isTimeoutErrorResponse(response)).toBe(true); + expect(isMaxBytesErrorResponse(response)).toBe(false); } ); test.each([errorA, errorB])( @@ -34,6 +35,7 @@ describe('isTimeoutErrorResponse', () => { ], }; expect(isTimeoutErrorResponse(response)).toBe(true); + expect(isMaxBytesErrorResponse(response)).toBe(false); } ); test.each([errorA, errorB])( @@ -55,6 +57,7 @@ describe('isTimeoutErrorResponse', () => { ], }; expect(isTimeoutErrorResponse(response)).toBe(true); + expect(isMaxBytesErrorResponse(response)).toBe(false); } ); test('does not report false positives', () => { @@ -62,5 +65,70 @@ describe('isTimeoutErrorResponse', () => { data: [], }; expect(isTimeoutErrorResponse(response)).toBe(false); + expect(isMaxBytesErrorResponse(response)).toBe(false); + }); +}); +describe('isMaxBytesErrorResponse', () => { + const error = `the query would read too many bytes (query: 49 MiB, limit: 1.0 MiB); consider adding more specific stream selectors or reduce the time range of the query`; + + it('identifies timeout errors in the error.message attribute when the message is `%s`', () => { + const response: DataQueryResponse = { + data: [], + error: { + message: error, + }, + }; + expect(isTimeoutErrorResponse(response)).toBe(false); + expect(isMaxBytesErrorResponse(response)).toBe(true); + }); + + it('identifies timeout errors in the errors.message attribute when the message is `%s`', () => { + const response: DataQueryResponse = { + data: [], + errors: [ + { + message: 'Something else', + }, + { + message: error, + }, + ], + }; + expect(isTimeoutErrorResponse(response)).toBe(false); + expect(isMaxBytesErrorResponse(response)).toBe(true); + }); + it('identifies timeout errors in the errors.data.message attribute when the message is `%s`', () => { + const response: DataQueryResponse = { + data: [], + errors: [ + { + data: { + message: 'Something else', + }, + }, + { + data: { + message: error, + }, + }, + ], + }; + expect(isTimeoutErrorResponse(response)).toBe(false); + expect(isMaxBytesErrorResponse(response)).toBe(true); + }); + test('does not report false positives', () => { + const response: DataQueryResponse = { + data: [], + errors: [ + { + status: 500, + data: { + message: 'Servr error', + }, + }, + ], + }; + expect(isTimeoutErrorResponse(response)).toBe(false); + expect(isMaxBytesErrorResponse(response)).toBe(false); }); }); diff --git a/public/app/features/explore/Logs/utils/logsVolumeResponse.ts b/public/app/features/explore/Logs/utils/logsVolumeResponse.ts index 2dcb5221331..f21ca9659bd 100644 --- a/public/app/features/explore/Logs/utils/logsVolumeResponse.ts +++ b/public/app/features/explore/Logs/utils/logsVolumeResponse.ts @@ -1,6 +1,8 @@ import { DataQueryError, DataQueryResponse } from '@grafana/data'; -// Currently we can only infer if an error response is a timeout or not. +export const LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX = 'the query would read too many bytes'; +export const LOKI_TIMEOUT_ERROR_REGEX = /timeout|timed out/; + export function isTimeoutErrorResponse(response: DataQueryResponse | undefined): boolean { if (!response) { return false; @@ -12,7 +14,27 @@ export function isTimeoutErrorResponse(response: DataQueryResponse | undefined): const errors = response.error ? [response.error] : response.errors || []; return errors.some((error: DataQueryError) => { - const message = `${error.message || error.data?.message}`?.toLowerCase(); - return message.includes('timeout'); + const message = normalizeMessage(error); + return message.match(LOKI_TIMEOUT_ERROR_REGEX); }); } + +export function isMaxBytesErrorResponse(response: DataQueryResponse | undefined): boolean { + if (!response) { + return false; + } + if (!response.error && !response.errors) { + return false; + } + + const errors = response.error ? [response.error] : response.errors || []; + + return errors.some((error: DataQueryError) => { + const message = normalizeMessage(error); + return message?.includes(LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX); + }); +} + +function normalizeMessage(error: DataQueryError) { + return ((error.message || error.data?.message) ?? '').toLowerCase(); +} diff --git a/public/app/plugins/datasource/loki/dataquery.cue b/public/app/plugins/datasource/loki/dataquery.cue index 63dd1c0265a..0b4d4968db9 100644 --- a/public/app/plugins/datasource/loki/dataquery.cue +++ b/public/app/plugins/datasource/loki/dataquery.cue @@ -42,6 +42,14 @@ composableKinds: DataQuery: { instant?: bool // Used to set step value for range queries. step?: string + // The full query plan for split/shard queries. Encoded and sent to Loki via `X-Loki-Query-Limits-Context` header. Requires "lokiQueryLimitsContext" feature flag + limitsContext?: #LimitsContext + + #LimitsContext: { + expr: string + from: int64 + to: int64 + } #QueryEditorMode: "code" | "builder" @cuetsy(kind="enum") diff --git a/public/app/plugins/datasource/loki/dataquery.gen.ts b/public/app/plugins/datasource/loki/dataquery.gen.ts index 99047e02221..c52cece76db 100644 --- a/public/app/plugins/datasource/loki/dataquery.gen.ts +++ b/public/app/plugins/datasource/loki/dataquery.gen.ts @@ -48,6 +48,14 @@ export interface LokiDataQuery extends common.DataQuery { * Used to override the name of the series. */ legendFormat?: string; + /** + * The full query plan for split/shard queries. Encoded and sent to Loki via `X-Loki-Query-Limits-Context` header. Requires "lokiQueryLimitsContext" feature flag + */ + limitsContext?: { + expr: string; + from: number; + to: number; + }; /** * Used to limit the number of log rows returned. */ diff --git a/public/app/plugins/datasource/loki/mergeResponses.ts b/public/app/plugins/datasource/loki/mergeResponses.ts index 57697a04d78..5ec5704796c 100644 --- a/public/app/plugins/datasource/loki/mergeResponses.ts +++ b/public/app/plugins/datasource/loki/mergeResponses.ts @@ -36,6 +36,11 @@ function getFrameKey(frame: DataFrame): string | undefined { return frame.refId ?? frame.name; } +/** + * @todo test new response is error, current response is not + * @param currentResponse + * @param newResponse + */ export function combineResponses(currentResponse: DataQueryResponse | null, newResponse: DataQueryResponse) { if (!currentResponse) { return cloneQueryResponse(newResponse); @@ -65,6 +70,7 @@ export function combineResponses(currentResponse: DataQueryResponse | null, newR const mergedErrors = [...(currentResponse.errors ?? []), ...(newResponse.errors ?? [])]; if (mergedErrors.length > 0) { currentResponse.errors = mergedErrors; + currentResponse.state = LoadingState.Error; } // the `.error` attribute is obsolete now, @@ -75,6 +81,7 @@ export function combineResponses(currentResponse: DataQueryResponse | null, newR const mergedError = currentResponse.error ?? newResponse.error; if (mergedError != null) { currentResponse.error = mergedError; + currentResponse.state = LoadingState.Error; } const mergedTraceIds = [...(currentResponse.traceIds ?? []), ...(newResponse.traceIds ?? [])]; diff --git a/public/app/plugins/datasource/loki/mocks/frames.ts b/public/app/plugins/datasource/loki/mocks/frames.ts index 0aeda947306..bcc432078de 100644 --- a/public/app/plugins/datasource/loki/mocks/frames.ts +++ b/public/app/plugins/datasource/loki/mocks/frames.ts @@ -222,6 +222,33 @@ export function getMockFrames() { length: 2, }; + const metricFrameAB: DataFrame = { + refId: 'A', + fields: [ + { + name: 'Time', + type: FieldType.time, + config: {}, + values: [1000000, 2000000, 3000000, 4000000], + }, + { + name: 'Value', + type: FieldType.number, + config: {}, + values: [6, 7, 5, 4], + labels: { + level: 'debug', + }, + }, + ], + meta: { + notices: [], + type: DataFrameType.TimeSeriesMulti, + stats: [{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 33 }], + }, + length: 4, + }; + const metricFrameC: DataFrame = { refId: 'A', name: 'some-time-series', @@ -305,6 +332,7 @@ export function getMockFrames() { metricFrameA, metricFrameB, metricFrameC, + metricFrameAB, emptyFrame, }; } diff --git a/public/app/plugins/datasource/loki/querySplitting.test.ts b/public/app/plugins/datasource/loki/querySplitting.test.ts index 26abb0423b8..2d04b447005 100644 --- a/public/app/plugins/datasource/loki/querySplitting.test.ts +++ b/public/app/plugins/datasource/loki/querySplitting.test.ts @@ -1,12 +1,13 @@ import { of } from 'rxjs'; -import { DataQueryRequest, dateTime, LoadingState } from '@grafana/data'; +import { DataQueryError, DataQueryRequest, DataQueryResponse, dateTime, LoadingState } from '@grafana/data'; import { config } from '@grafana/runtime'; import { LokiDatasource } from './datasource'; import { createLokiDatasource } from './mocks/datasource'; import { getMockFrames } from './mocks/frames'; import { runSplitQuery } from './querySplitting'; +import { LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX, LOKI_TIMEOUT_ERROR_MSG } from './responseUtils'; import { trackGroupedQueries } from './tracking'; import { LokiQuery, LokiQueryDirection, LokiQueryType } from './types'; @@ -16,6 +17,7 @@ jest.mock('uuid', () => ({ })); const originalShardingFlagState = config.featureToggles.lokiShardSplitting; +const originalLokiQueryLimitsContextState = config.featureToggles.lokiQueryLimitsContext; const originalErr = console.error; beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}); @@ -26,22 +28,23 @@ beforeAll(() => { callback(); }); config.featureToggles.lokiShardSplitting = false; + config.featureToggles.lokiQueryLimitsContext = true; }); afterAll(() => { jest.mocked(global.setTimeout).mockReset(); config.featureToggles.lokiShardSplitting = originalShardingFlagState; + config.featureToggles.lokiQueryLimitsContext = originalLokiQueryLimitsContextState; console.error = originalErr; }); describe('runSplitQuery()', () => { let datasource: LokiDatasource; + const from = dateTime('2023-02-08T05:00:00.000Z'); + const to = dateTime('2023-02-10T06:00:00.000Z'); const range = { - from: dateTime('2023-02-08T05:00:00.000Z'), - to: dateTime('2023-02-10T06:00:00.000Z'), - raw: { - from: dateTime('2023-02-08T05:00:00.000Z'), - to: dateTime('2023-02-10T06:00:00.000Z'), - }, + from, + to, + raw: { from, to }, }; const createRequest = (targets: LokiQuery[], overrides?: Partial>) => { @@ -94,20 +97,59 @@ describe('runSplitQuery()', () => { }); }); - test('Retries retriable failed requests', async () => { + test('Retries retriable failed requests (timeout)', async () => { jest .mocked(datasource.runQuery) - .mockReturnValueOnce(of({ state: LoadingState.Error, errors: [{ refId: 'A', message: 'timeout' }], data: [] })); - await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { - // 3 days, 3 chunks, 1 retry, 4 requests. + .mockReturnValueOnce( + of({ state: LoadingState.Error, errors: [{ refId: 'A', message: LOKI_TIMEOUT_ERROR_MSG }], data: [] }) + ); + await expect(runSplitQuery(datasource, request)).toEmitValuesWith((response) => { + expect(response[0].state).toBe(LoadingState.Done); + // 3 days, 3 chunks, 1 retry = 4 requests. expect(datasource.runQuery).toHaveBeenCalledTimes(4); }); }); + test('Does not retry max bytes errors (error on initial)', async () => { + jest.mocked(datasource.runQuery).mockReturnValueOnce( + of({ + state: LoadingState.Error, + errors: [{ refId: 'A', message: LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX }], + data: [], + }) + ); + await expect(runSplitQuery(datasource, request)).toEmitValuesWith((response) => { + expect(response[0].state).toBe(LoadingState.Error); + // Stop immediately + expect(datasource.runQuery).toHaveBeenCalledTimes(1); + }); + }); + + test('Does not retry max bytes errors (error on subsequent)', async () => { + const { logFrameA } = getMockFrames(); + jest + .mocked(datasource.runQuery) + .mockReturnValueOnce(of({ data: [logFrameA], refId: 'A' })) + .mockReturnValueOnce( + of({ + state: LoadingState.Error, + errors: [{ refId: 'A', message: LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX }], + data: [], + }) + ); + await expect(runSplitQuery(datasource, request)).toEmitValuesWith((response) => { + expect(response[0].state).toBe(LoadingState.Error); + // Stop immediately after error + expect(datasource.runQuery).toHaveBeenCalledTimes(2); + }); + }); + test('Does not retry failed queries as an option', async () => { jest .mocked(datasource.runQuery) - .mockReturnValueOnce(of({ state: LoadingState.Error, errors: [{ refId: 'A', message: 'timeout' }], data: [] })); + .mockReturnValueOnce( + of({ state: LoadingState.Error, errors: [{ refId: 'A', message: LOKI_TIMEOUT_ERROR_MSG }], data: [] }) + ); await expect(runSplitQuery(datasource, request, { disableRetry: true })).toEmitValuesWith(() => { // No retries expect(datasource.runQuery).toHaveBeenCalledTimes(1); @@ -165,6 +207,19 @@ describe('runSplitQuery()', () => { _i: 1676008800000, }), }), + targets: [ + { + expr: 'count_over_time({a="b"}[1m])', + legendFormat: undefined, + refId: 'A', + step: undefined, + limitsContext: { + expr: 'count_over_time({a="b"}[1m])', + from: from.valueOf(), + to: to.valueOf(), + }, + }, + ], }) ); @@ -183,6 +238,15 @@ describe('runSplitQuery()', () => { _i: 1676005140000, }), }), + targets: [ + { + expr: 'count_over_time({a="b"}[1m])', + legendFormat: undefined, + refId: 'A', + step: undefined, + limitsContext: undefined, + }, + ], }) ); @@ -201,6 +265,15 @@ describe('runSplitQuery()', () => { _i: 1675918740000, }), }), + targets: [ + { + expr: 'count_over_time({a="b"}[1m])', + legendFormat: undefined, + refId: 'A', + step: undefined, + limitsContext: undefined, + }, + ], }) ); }); @@ -225,6 +298,19 @@ describe('runSplitQuery()', () => { _i: 1676008800000, }), }), + targets: [ + { + expr: 'count_over_time({a="b"}[1m])', + legendFormat: undefined, + refId: 'A', + step: '10s', + limitsContext: { + expr: 'count_over_time({a="b"}[1m])', + from: from.valueOf(), + to: to.valueOf(), + }, + }, + ], }) ); @@ -243,6 +329,15 @@ describe('runSplitQuery()', () => { _i: 1676005190000, }), }), + targets: [ + { + expr: 'count_over_time({a="b"}[1m])', + legendFormat: undefined, + refId: 'A', + step: '10s', + limitsContext: undefined, + }, + ], }) ); @@ -261,21 +356,87 @@ describe('runSplitQuery()', () => { _i: 1675918790000, }), }), + targets: [ + { + expr: 'count_over_time({a="b"}[1m])', + legendFormat: undefined, + refId: 'A', + step: '10s', + limitsContext: undefined, + }, + ], }) ); }); }); - test('Handles and reports errors', async () => { - jest - .spyOn(datasource, 'runQuery') - .mockReturnValue(of({ state: LoadingState.Error, error: { refId: 'A', message: 'Error' }, data: [] })); + test('Handles and reports 5xx error too many bytes', async () => { + const error: DataQueryError = { + message: `${LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX} ...`, + status: 500, + }; + const response: DataQueryResponse = { + state: LoadingState.Error, + data: [], + errors: [error], + }; + jest.spyOn(datasource, 'runQuery').mockReturnValue(of(response)); await expect(runSplitQuery(datasource, request)).toEmitValuesWith((values) => { expect(values).toHaveLength(1); expect(values[0]).toEqual( - expect.objectContaining({ error: { refId: 'A', message: 'Error' }, state: LoadingState.Streaming }) + expect.objectContaining({ + errors: [error], + state: LoadingState.Error, + }) ); }); + // Errors are not retried + expect(datasource.runQuery).toHaveBeenCalledTimes(1); + }); + test('Handles and reports 4xx errors', async () => { + const error: DataQueryError = { + message: 'BAD REQUEST', + status: 418, + }; + const response: DataQueryResponse = { + state: LoadingState.Error, + data: [], + errors: [error], + }; + jest.spyOn(datasource, 'runQuery').mockReturnValue(of(response)); + await expect(runSplitQuery(datasource, request)).toEmitValuesWith((values) => { + expect(values).toHaveLength(1); + expect(values[0]).toEqual( + expect.objectContaining({ + errors: [error], + state: LoadingState.Error, + }) + ); + }); + // Errors are not retried + expect(datasource.runQuery).toHaveBeenCalledTimes(1); + }); + + test('Handles and reports errors (deprecated error)', async () => { + jest.spyOn(datasource, 'runQuery').mockReturnValue( + of({ + state: LoadingState.Error, + error: { refId: 'A', message: `${LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX} ...` }, + data: [], + key: 'uuid', + }) + ); + await expect(runSplitQuery(datasource, request)).toEmitValuesWith((values) => { + expect(values).toHaveLength(1); + expect(values[0]).toEqual( + expect.objectContaining({ + error: { refId: 'A', message: `${LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX} ...` }, + state: LoadingState.Error, + }) + ); + }); + // Errors are not retried + expect(datasource.runQuery).toHaveBeenCalledTimes(1); }); describe('Hidden and empty queries', () => { diff --git a/public/app/plugins/datasource/loki/querySplitting.ts b/public/app/plugins/datasource/loki/querySplitting.ts index d0fd1678455..89a466b21d0 100644 --- a/public/app/plugins/datasource/loki/querySplitting.ts +++ b/public/app/plugins/datasource/loki/querySplitting.ts @@ -8,16 +8,18 @@ import { DataQueryResponse, DataTopic, dateTime, - rangeUtil, - TimeRange, LoadingState, + rangeUtil, + store, + TimeRange, } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { LokiDatasource } from './datasource'; import { splitTimeRange as splitLogsTimeRange } from './logsTimeSplitting'; import { combineResponses } from './mergeResponses'; import { splitTimeRange as splitMetricTimeRange } from './metricTimeSplitting'; -import { isLogsQuery, isQueryWithRangeVariable } from './queryUtils'; +import { addQueryLimitsContext, isLogsQuery, isQueryWithRangeVariable } from './queryUtils'; import { isRetriableError } from './responseUtils'; import { trackGroupedQueries } from './tracking'; import { LokiGroupedRequest, LokiQuery, LokiQueryDirection, LokiQueryType } from './types'; @@ -55,6 +57,10 @@ interface QuerySplittingOptions { * Do not retry failed queries. */ disableRetry?: boolean; + /** + * The current index of all query attempts + */ + shardQueryIndex?: number; } /** @@ -85,6 +91,25 @@ export function adjustTargetsFromResponseState(targets: LokiQuery[], response: D }) .filter((target) => target.maxLines === undefined || target.maxLines > 0); } + +const addLimitsToSplitRequests = (splitQueryIndex: number, shardQueryIndex: number, requests: LokiGroupedRequest[]) => { + // requests has already been mutated + return requests.map((r) => ({ + ...r, + request: { + ...r.request, + targets: r.request.targets.map((t) => { + // @todo if we retry the first request, we will strip out the query limits context + if (splitQueryIndex === 0 && shardQueryIndex === 0) { + // Don't pull from request if it has already been added by `addLimitsToShardGroups` + return t.limitsContext === undefined ? addQueryLimitsContext(t, r.request) : t; + } + return { ...t, limitsContext: undefined }; + }), + }, + })); +}; + export function runSplitGroupedQueries( datasource: LokiDatasource, requests: LokiGroupedRequest[], @@ -99,8 +124,15 @@ export function runSplitGroupedQueries( let subquerySubscription: Subscription | null = null; let retriesMap = new Map(); let retryTimer: ReturnType | null = null; + let splitQueryIndex = 0; + const shardQueryIndex = options.shardQueryIndex ?? 0; const runNextRequest = (subscriber: Subscriber, requestN: number, requestGroup: number) => { + if (config.featureToggles.lokiQueryLimitsContext) { + requests = addLimitsToSplitRequests(splitQueryIndex, shardQueryIndex, requests); + } + + splitQueryIndex++; let retrying = false; if (subquerySubscription != null) { @@ -114,7 +146,10 @@ export function runSplitGroupedQueries( } const done = () => { - mergedResponse.state = LoadingState.Done; + if (mergedResponse.state !== LoadingState.Error) { + mergedResponse.state = LoadingState.Done; + } + subscriber.next(mergedResponse); subscriber.complete(); }; @@ -189,6 +224,10 @@ export function runSplitGroupedQueries( if (!options.skipPartialUpdates) { mergedResponse = updateLoadingFrame(mergedResponse, subRequest, longestPartition, requestN); } + + if (mergedResponse.state === LoadingState.Error) { + done(); + } }, complete: () => { if (retrying) { @@ -301,7 +340,9 @@ export function runSplitQuery( const [logQueries, metricQueries] = partition(normalQueries, (query) => isLogsQuery(query.expr)); request.queryGroupId = uuidv4(); - const oneDayMs = 24 * 60 * 60 * 1000; + // Allow custom split durations for debugging, e.g. `localStorage.setItem('grafana.loki.querySplitInterval', 24 * 60 * 1000) // 1 hour` + const debugSplitDuration = parseInt(store.get('grafana.loki.querySplitInterval'), 10); + const oneDayMs = debugSplitDuration || 24 * 60 * 60 * 1000; const directionPartitionedLogQueries = groupBy(logQueries, (query) => query.direction === LokiQueryDirection.Forward ? LokiQueryDirection.Forward : LokiQueryDirection.Backward ); diff --git a/public/app/plugins/datasource/loki/queryUtils.ts b/public/app/plugins/datasource/loki/queryUtils.ts index 64ad1a8a4ae..a4a895feae5 100644 --- a/public/app/plugins/datasource/loki/queryUtils.ts +++ b/public/app/plugins/datasource/loki/queryUtils.ts @@ -1,6 +1,7 @@ import { SyntaxNode } from '@lezer/common'; import { escapeRegExp } from 'lodash'; +import { DataQueryRequest } from '@grafana/data'; import { parser, LineFilter, @@ -310,6 +311,7 @@ export function getStreamSelectorsFromQuery(query: string): string[] { export function requestSupportsSplitting(allQueries: LokiQuery[]) { const queries = allQueries .filter((query) => !query.hide) + .filter((query) => query.queryType !== LokiQueryType.Instant) .filter((query) => !query.refId.includes('do-not-chunk')) .filter((query) => query.expr); @@ -425,3 +427,21 @@ export const getSelectorForShardValues = (query: string) => { } return ''; }; + +/** + * Adds query plan to shard/split queries + * Must be called after interpolation step! + * + * @param lokiQuery + * @param request + */ +export const addQueryLimitsContext = (lokiQuery: LokiQuery, request: DataQueryRequest) => { + return { + ...lokiQuery, + limitsContext: { + expr: lokiQuery.expr, + from: request.range.from.toDate().getTime(), + to: request.range.to.toDate().getTime(), + }, + }; +}; diff --git a/public/app/plugins/datasource/loki/responseUtils.ts b/public/app/plugins/datasource/loki/responseUtils.ts index 95c9112b91c..ca65e218665 100644 --- a/public/app/plugins/datasource/loki/responseUtils.ts +++ b/public/app/plugins/datasource/loki/responseUtils.ts @@ -4,6 +4,11 @@ import { isBytesString, processLabels } from './languageUtils'; import { isLogLineJSON, isLogLineLogfmt, isLogLinePacked } from './lineParser'; import { LabelType } from './types'; +export const LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX = 'the query would read too many bytes'; +export const LOKI_TIMEOUT_ERROR_MSG = + 'request timed out, decrease the duration of the request or add more label matchers (prefer exact match over regex match) to reduce the amount of data processed'; +export const LOKI_TIMEOUT_ERROR_REGEX = /timeout|timed out/; + export function dataFrameHasLokiError(frame: DataFrame): boolean { const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values ?? []; return labelSets.some((labels) => labels.__error__ !== undefined); @@ -134,11 +139,20 @@ export function extractLevelLikeLabelFromDataFrame(frame: DataFrame): string | n export function isRetriableError(errorResponse: DataQueryResponse) { const message = errorResponse.errors - ? (errorResponse.errors[0].message ?? '').toLowerCase() - : (errorResponse.error?.message ?? ''); - if (message.includes('timeout')) { + ? errorResponse.errors + .map((err) => err.message ?? '') + .join() + .toLowerCase() + : (errorResponse.error?.message ?? '').toLowerCase(); + + // max_query_bytes_read exceeded + if (message.includes(LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX)) { + throw new Error(message); + } + if (message.match(LOKI_TIMEOUT_ERROR_REGEX)) { return true; - } else if (errorResponse.data.length > 0 && errorResponse.data[0].fields.length > 0) { + } + if (errorResponse.data.length > 0 && errorResponse.data[0].fields.length > 0) { // Error response but we're receiving data, continue querying. return false; } diff --git a/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts b/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts index 12bb8b04de0..cebee5bee94 100644 --- a/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts +++ b/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts @@ -1,10 +1,12 @@ import { of } from 'rxjs'; import { DataQueryRequest, DataQueryResponse, dateTime, LoadingState } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { LokiDatasource } from './datasource'; import { createLokiDatasource } from './mocks/datasource'; import { getMockFrames } from './mocks/frames'; +import { LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX, LOKI_TIMEOUT_ERROR_MSG } from './responseUtils'; import { runShardSplitQuery } from './shardQuerySplitting'; import { LokiQuery, LokiQueryDirection, LokiQueryType } from './types'; @@ -12,6 +14,8 @@ jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('uuid'), })); +const originalLokiQueryLimitsContextState = config.featureToggles.lokiQueryLimitsContext; + const originalLog = console.log; const originalWarn = console.warn; const originalErr = console.error; @@ -20,20 +24,26 @@ beforeEach(() => { jest.spyOn(console, 'warn').mockImplementation(() => {}); jest.spyOn(console, 'error').mockImplementation(() => {}); }); +beforeAll(() => { + config.featureToggles.lokiQueryLimitsContext = true; +}); afterAll(() => { console.log = originalLog; console.warn = originalWarn; console.error = originalErr; + config.featureToggles.lokiQueryLimitsContext = originalLokiQueryLimitsContextState; }); describe('runShardSplitQuery()', () => { let datasource: LokiDatasource; + const from = dateTime('2023-02-08T04:00:00.000Z'); + const to = dateTime('2023-02-08T11:00:00.000Z'); const range = { - from: dateTime('2023-02-08T04:00:00.000Z'), - to: dateTime('2023-02-08T11:00:00.000Z'), + from, + to, raw: { - from: dateTime('2023-02-08T04:00:00.000Z'), - to: dateTime('2023-02-08T11:00:00.000Z'), + from, + to, }, }; @@ -139,6 +149,11 @@ describe('runShardSplitQuery()', () => { targets: [ { expr: '{a="b", __stream_shard__=~"20|10"}', + limitsContext: { + expr: `{a="b"}`, + from: from.valueOf(), + to: to.valueOf(), + }, refId: 'A', direction: LokiQueryDirection.Scan, }, @@ -209,6 +224,11 @@ describe('runShardSplitQuery()', () => { targets: [ { expr: '{service_name="test", filter="true", __stream_shard__=~"20|10"}', + limitsContext: { + expr: `{service_name="test", filter="true"}`, + from: from.valueOf(), + to: to.valueOf(), + }, refId: 'A', direction: LokiQueryDirection.Scan, }, @@ -227,9 +247,13 @@ describe('runShardSplitQuery()', () => { test('Retries failed retriable requests', async () => { jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue(['1']); - jest - .spyOn(datasource, 'runQuery') - .mockReturnValueOnce(of({ state: LoadingState.Error, errors: [{ refId: 'A', message: 'timeout' }], data: [] })); + jest.spyOn(datasource, 'runQuery').mockReturnValueOnce( + of({ + state: LoadingState.Error, + errors: [{ refId: 'A', message: LOKI_TIMEOUT_ERROR_MSG }], + data: [], + }) + ); // @ts-expect-error jest.spyOn(global, 'setTimeout').mockImplementationOnce((callback) => { callback(); @@ -241,28 +265,73 @@ describe('runShardSplitQuery()', () => { }); }); - test('Failed requests have loading state Error', async () => { - jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue(['1']); - jest - .spyOn(datasource, 'runQuery') - .mockReturnValue(of({ state: LoadingState.Error, error: { refId: 'A', message: 'parse error' }, data: [] })); - await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith((response: DataQueryResponse[]) => { - expect(response[0].state).toBe(LoadingState.Error); + describe('Errors', () => { + beforeEach(() => { + const querySplittingRange = { + from: dateTime('2023-02-08T05:00:00.000Z'), + to: dateTime('2023-02-10T06:00:00.000Z'), + raw: { + from: dateTime('2023-02-08T05:00:00.000Z'), + to: dateTime('2023-02-10T06:00:00.000Z'), + }, + }; + request = createRequest([{ expr: '$SELECTOR', refId: 'A', direction: LokiQueryDirection.Scan }], { + range: querySplittingRange, + }); + // @ts-expect-error + jest.spyOn(global, 'setTimeout').mockImplementationOnce((callback) => { + callback(); + }); }); - }); - test('Does not retry on other errors', async () => { - jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue(['1']); - jest - .spyOn(datasource, 'runQuery') - .mockReturnValueOnce(of({ state: LoadingState.Error, errors: [{ refId: 'A', message: 'nope nope' }], data: [] })); - // @ts-expect-error - jest.spyOn(global, 'setTimeout').mockImplementationOnce((callback) => { - callback(); - }); - await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith((response: DataQueryResponse[]) => { + test('Failed 4xx responses have loading state Error', async () => { + jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue(['1', '12', '5']); + jest + .spyOn(datasource, 'runQuery') + .mockReturnValue( + of({ state: LoadingState.Error, error: { refId: 'A', message: 'client error', status: 400 }, data: [] }) + ); + await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith((response: DataQueryResponse[]) => { + expect(response[0].state).toBe(LoadingState.Error); + }); expect(datasource.runQuery).toHaveBeenCalledTimes(1); }); + + test('Max query bytes errors are not retried', async () => { + const errResp: DataQueryResponse = { + state: LoadingState.Error, + errors: [{ refId: 'A', message: `${LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX} ...`, status: 500 }], + data: [], + }; + jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue(['1', '10', '4']); + jest + .spyOn(datasource, 'runQuery') + .mockReturnValueOnce(of(errResp)) + .mockReturnValueOnce(of({ state: LoadingState.Done, data: [], status: 200 })); + + await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith((response: DataQueryResponse[]) => { + expect(response[0].state).toBe(LoadingState.Error); + }); + + // 5 shards, 3 groups + empty shard group, 4 requests * 3 days, 3 chunks, 3 requests + 1 retriable error = 13 requests + expect(datasource.runQuery).toHaveBeenCalledTimes(1); + }); + + test('Does not retry on other errors', async () => { + jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue(['1']); + jest + .spyOn(datasource, 'runQuery') + .mockReturnValueOnce( + of({ state: LoadingState.Error, errors: [{ refId: 'A', message: 'nope nope' }], data: [] }) + ); + // @ts-expect-error + jest.spyOn(global, 'setTimeout').mockImplementationOnce((callback) => { + callback(); + }); + await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith((response: DataQueryResponse[]) => { + expect(datasource.runQuery).toHaveBeenCalledTimes(1); + }); + }); }); test('Adjusts the group size based on errors and execution time', async () => { @@ -302,9 +371,13 @@ describe('runShardSplitQuery()', () => { ); // sqrt(currentSize) - jest - .mocked(datasource.runQuery) - .mockReturnValueOnce(of({ state: LoadingState.Error, errors: [{ refId: 'A', message: 'timeout' }], data: [] })); + jest.mocked(datasource.runQuery).mockReturnValueOnce( + of({ + state: LoadingState.Error, + errors: [{ refId: 'A', message: LOKI_TIMEOUT_ERROR_MSG }], + data: [], + }) + ); // +10% jest.mocked(datasource.runQuery).mockReturnValueOnce( @@ -425,6 +498,11 @@ describe('runShardSplitQuery()', () => { targets: [ { expr: '{a="b", __stream_shard__=~"20|10|9"}', + limitsContext: { + expr: `{a="b"}`, + from: from.valueOf(), + to: to.valueOf(), + }, refId: 'A', direction: LokiQueryDirection.Scan, }, diff --git a/public/app/plugins/datasource/loki/shardQuerySplitting.ts b/public/app/plugins/datasource/loki/shardQuerySplitting.ts index 9cd5207da6d..605f5b9113b 100644 --- a/public/app/plugins/datasource/loki/shardQuerySplitting.ts +++ b/public/app/plugins/datasource/loki/shardQuerySplitting.ts @@ -2,15 +2,20 @@ import { groupBy, partition } from 'lodash'; import { Observable, Subscriber, Subscription } from 'rxjs'; import { v4 as uuidv4 } from 'uuid'; -import { DataQueryRequest, LoadingState, DataQueryResponse, QueryResultMetaStat } from '@grafana/data'; +import { DataQueryRequest, DataQueryResponse, LoadingState, QueryResultMetaStat } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { LokiDatasource } from './datasource'; import { combineResponses, replaceResponses } from './mergeResponses'; import { adjustTargetsFromResponseState, runSplitQuery } from './querySplitting'; -import { getSelectorForShardValues, interpolateShardingSelector, requestSupportsSharding } from './queryUtils'; +import { + addQueryLimitsContext, + getSelectorForShardValues, + interpolateShardingSelector, + requestSupportsSharding, +} from './queryUtils'; import { isRetriableError } from './responseUtils'; import { LokiQuery } from './types'; - /** * Query splitting by stream shards. * Query splitting was introduced in Loki to optimize querying for long intervals and high volume of data, @@ -54,6 +59,19 @@ export function runShardSplitQuery(datasource: LokiDatasource, request: DataQuer return splitQueriesByStreamShard(datasource, request, queries); } +const addLimitsToShardGroups = ( + queryIndex: number, + groups: ShardedQueryGroup[], + request: DataQueryRequest +) => { + return groups.map((g) => ({ + ...g, + targets: g.targets.map((t) => { + return queryIndex === 0 ? addQueryLimitsContext(t, request) : { ...t, limitsContext: undefined }; + }), + })); +}; + function splitQueriesByStreamShard( datasource: LokiDatasource, request: DataQueryRequest, @@ -64,8 +82,13 @@ function splitQueriesByStreamShard( let subquerySubscription: Subscription | null = null; let retriesMap = new Map(); let retryTimer: ReturnType | null = null; + let queryIndex = 0; const runNextRequest = (subscriber: Subscriber, group: number, groups: ShardedQueryGroup[]) => { + if (config.featureToggles.lokiQueryLimitsContext) { + groups = addLimitsToShardGroups(queryIndex, groups, request); + } + queryIndex++; let nextGroupSize = groups[group].groupSize; const { shards, groupSize, cycle } = groups[group]; let retrying = false; @@ -164,6 +187,7 @@ function splitQueriesByStreamShard( subquerySubscription = runSplitQuery(datasource, subRequest, { skipPartialUpdates: true, disableRetry: true, + shardQueryIndex: queryIndex - 1, }).subscribe({ next: (partialResponse: DataQueryResponse) => { if ((partialResponse.errors ?? []).length > 0 || partialResponse.error != null) { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index cc900022bb4..8793ca25bdf 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -7218,6 +7218,7 @@ "logs-volume": { "add-filters": "Add more labels to your query to narrow down your search.", "decrease-timerange": "Decrease the time range of your query.", + "max-bytes-error": "The query would read too many bytes. Try one or more of the following:", "much-data": "The query is trying to access too much data. Try one or more of the following:" }, "maximum-pinned-logs": "Maximum of {{PINNED_LOGS_LIMIT}} pinned logs reached. Unpin a log to add another.", @@ -7267,12 +7268,15 @@ "tooltip-select-query-visualize-table": "Select a query to visualize in the table" }, "logs-volume-panel": { - "content-streaming": "Streaming" + "content-streaming": "Streaming", + "partial-data-tips": "Try decreasing the time range or adding more labels to your query." }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Reload log volume", "label-reload-log-volume": "Reload log volume", "loading": "Loading...", + "message-showing-partial-data-max-bytes": "The query would read too many bytes and some sharded requests could not be completed. ", + "message-showing-partial-data-timeout": "The query is trying to access too much data and some sharded requests could not be completed. ", "title-failed-volume-query": "Failed to load log volume for this query", "title-no-logs-volume-available": "No logs volume available", "title-showing-partial-data": "Showing partial data", From def4c644ec4d5c9ad281d6ac07f3355e1b24e463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Fri, 21 Nov 2025 14:38:09 +0100 Subject: [PATCH 051/378] Use infra/log instead of slog in pkg/storage/unified (#114292) Use infra/log instead of slog. Use log.FromContext to add contextual info to log messages. --- pkg/storage/unified/resource/access.go | 11 +++++---- pkg/storage/unified/resource/client.go | 6 ++--- .../unified/resource/grpc/authenticator.go | 4 ++-- pkg/storage/unified/resource/search.go | 24 +++++++------------ pkg/storage/unified/resource/server.go | 13 +++++----- pkg/storage/unified/resource/server_test.go | 5 ++-- pkg/storage/unified/search/bleve.go | 21 ++++++++-------- pkg/storage/unified/search/bleve_test.go | 5 ++-- 8 files changed, 40 insertions(+), 49 deletions(-) diff --git a/pkg/storage/unified/resource/access.go b/pkg/storage/unified/resource/access.go index 94cd5b4a801..58a9d8b8b76 100644 --- a/pkg/storage/unified/resource/access.go +++ b/pkg/storage/unified/resource/access.go @@ -3,7 +3,6 @@ package resource import ( "context" "fmt" - "log/slog" "sync" "time" @@ -14,6 +13,8 @@ import ( "go.opentelemetry.io/otel/trace/noop" claims "github.com/grafana/authlib/types" + + "github.com/grafana/grafana/pkg/infra/log" ) type groupResource map[string]map[string]interface{} @@ -75,7 +76,7 @@ type authzLimitedClient struct { client claims.AccessClient // allowlist is a map of group to resources that are compatible with RBAC. allowlist groupResource - logger *slog.Logger + logger log.Logger tracer trace.Tracer metrics *accessMetrics } @@ -87,7 +88,7 @@ type AuthzOptions struct { // NewAuthzLimitedClient creates a new authzLimitedClient. func NewAuthzLimitedClient(client claims.AccessClient, opts AuthzOptions) claims.AccessClient { - logger := slog.Default().With("logger", "limited-authz-client") + logger := log.New("limited-authz-client") if opts.Tracer == nil { opts.Tracer = noop.NewTracerProvider().Tracer("limited-authz-client") } @@ -147,7 +148,7 @@ func (c authzLimitedClient) Check(ctx context.Context, id claims.AuthInfo, req c } resp, err := c.client.Check(ctx, id, req, folder) if err != nil { - c.logger.Error("Check", "group", req.Group, "resource", req.Resource, "error", err, "duration", time.Since(t), "traceid", trace.SpanContextFromContext(ctx).TraceID().String()) + c.logger.FromContext(ctx).Error("Check", "group", req.Group, "resource", req.Resource, "error", err, "duration", time.Since(t)) c.metrics.errorsTotal.WithLabelValues(req.Group, req.Resource, req.Verb).Inc() span.SetStatus(codes.Error, fmt.Sprintf("check failed: %v", err)) span.RecordError(err) @@ -197,7 +198,7 @@ func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req } checker, zookie, err := c.client.Compile(ctx, id, req) if err != nil { - c.logger.Error("Compile", "group", req.Group, "resource", req.Resource, "error", err, "traceid", trace.SpanContextFromContext(ctx).TraceID().String()) + c.logger.FromContext(ctx).Error("Compile", "group", req.Group, "resource", req.Resource, "error", err) c.metrics.errorsTotal.WithLabelValues(req.Group, req.Resource, req.Verb).Inc() span.SetStatus(codes.Error, fmt.Sprintf("compile failed: %v", err)) span.RecordError(err) diff --git a/pkg/storage/unified/resource/client.go b/pkg/storage/unified/resource/client.go index e7d43a7bbcc..b2742f71dc9 100644 --- a/pkg/storage/unified/resource/client.go +++ b/pkg/storage/unified/resource/client.go @@ -7,7 +7,6 @@ import ( "crypto/rand" "crypto/tls" "fmt" - "log/slog" "net/http" "github.com/fullstorydev/grpchan" @@ -24,6 +23,7 @@ import ( "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/log" authnGrpcUtils "github.com/grafana/grafana/pkg/services/authn/grpcutils" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" @@ -158,7 +158,7 @@ func NewRemoteResourceClient(tracer trace.Tracer, conn grpc.ClientConnInterface, return newResourceClient(cc, cci), nil } -var authLogger = slog.Default().With("logger", "resource-client-auth-interceptor") +var authLogger = log.New("resource-client-auth-interceptor") func idTokenExtractor(ctx context.Context) (string, error) { if identity.IsServiceIdentity(ctx) { @@ -175,7 +175,7 @@ func idTokenExtractor(ctx context.Context) (string, error) { } if !types.IsIdentityType(info.GetIdentityType(), types.TypeAccessPolicy) { - authLogger.Warn( + authLogger.FromContext(ctx).Warn( "calling resource store as the service without id token or marking it as the service identity", "subject", info.GetSubject(), "uid", info.GetUID(), diff --git a/pkg/storage/unified/resource/grpc/authenticator.go b/pkg/storage/unified/resource/grpc/authenticator.go index 270835a85ef..6cc601565b7 100644 --- a/pkg/storage/unified/resource/grpc/authenticator.go +++ b/pkg/storage/unified/resource/grpc/authenticator.go @@ -2,7 +2,6 @@ package grpc import ( "context" - "log/slog" "strconv" "google.golang.org/grpc" @@ -14,6 +13,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/log" ) const ( @@ -25,7 +25,7 @@ const ( mdOrgRole = "grafana-org-role" ) -var logger = slog.Default().With("logger", "legacy.grpc.Authenticator") +var logger = log.New("legacy.grpc.Authenticator") // This is in a package we can no import // var _ interceptors.Authenticator = (*Authenticator)(nil) diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index 2752c59650d..dce40f884cf 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -4,7 +4,6 @@ import ( "cmp" "context" "fmt" - "log/slog" "slices" "strings" "sync" @@ -23,6 +22,7 @@ import ( dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util/debouncer" @@ -129,7 +129,7 @@ type SearchBackend interface { // This supports indexing+search regardless of implementation type searchSupport struct { - log *slog.Logger + log log.Logger storage StorageBackend search SearchBackend indexMetrics *BleveIndexMetrics @@ -184,7 +184,7 @@ func newSearchSupport(opts SearchOptions, storage StorageBackend, access types.A access: access, storage: storage, search: opts.Backend, - log: slog.Default().With("logger", "resource-search"), + log: log.New("resource-search"), initWorkers: opts.InitWorkerThreads, rebuildWorkers: opts.IndexRebuildWorkers, initMinSize: opts.InitMinCount, @@ -295,14 +295,6 @@ func (s *searchSupport) ListManagedObjects(ctx context.Context, req *resourcepb. return rsp, nil } -func (s *searchSupport) logWithTraceID(ctx context.Context) *slog.Logger { - l := s.log - if traceID := tracing.TraceIDFromContext(ctx, false); traceID != "" { - l = l.With("traceID", traceID) - } - return l -} - func (s *searchSupport) logStats(ctx context.Context, stats *SearchStats, span trace.Span, params ...any) { elapsed := time.Since(stats.startTime) @@ -319,7 +311,7 @@ func (s *searchSupport) logStats(ctx context.Context, stats *SearchStats, span t } args = append(args, params...) - s.logWithTraceID(ctx).Debug("Search stats", args...) + s.log.FromContext(ctx).Debug("Search stats", args...) if span != nil { attrs := make([]attribute.KeyValue, 0, len(args)/2) @@ -696,7 +688,7 @@ func (s *searchSupport) rebuildIndex(ctx context.Context, req rebuildRequest) { ctx, span := tracer.Start(ctx, "resource.searchSupport.rebuildIndex") defer span.End() - l := s.log.With("namespace", req.Namespace, "group", req.Group, "resource", req.Resource) + l := s.log.New("namespace", req.Namespace, "group", req.Group, "resource", req.Resource) idx := s.search.GetIndex(req.NamespacedResource) if idx == nil { @@ -752,7 +744,7 @@ func (s *searchSupport) rebuildIndex(ctx context.Context, req rebuildRequest) { } } -func shouldRebuildIndex(buildInfo IndexBuildInfo, minBuildVersion *semver.Version, minBuildTime time.Time, lastImportTime time.Time, rebuildLogger *slog.Logger) bool { +func shouldRebuildIndex(buildInfo IndexBuildInfo, minBuildVersion *semver.Version, minBuildTime time.Time, lastImportTime time.Time, rebuildLogger log.Logger) bool { if !minBuildTime.IsZero() { if buildInfo.BuildTime.IsZero() || buildInfo.BuildTime.Before(minBuildTime) { if rebuildLogger != nil { @@ -873,7 +865,7 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, stats *SearchStats if s.indexMetrics != nil { s.indexMetrics.SearchUpdateWaitTime.WithLabelValues(reason).Observe(elapsed.Seconds()) } - s.logWithTraceID(ctx).Debug("Index updated before search", "namespace", key.Namespace, "group", key.Group, "resource", key.Resource, "reason", reason, "duration", elapsed, "rv", rv) + s.log.FromContext(ctx).Debug("Index updated before search", "namespace", key.Namespace, "group", key.Group, "resource", key.Resource, "reason", reason, "duration", elapsed, "rv", rv) span.AddEvent("Index updated") return idx, nil @@ -890,7 +882,7 @@ func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size attribute.Int64("size", size), ) - logger := s.logWithTraceID(ctx).With("namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource) + logger := s.log.New("namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource) builder, err := s.builders.get(ctx, nsr) if err != nil { diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index bf805cc0a6a..ca9f87896e1 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "iter" - "log/slog" "net/http" "sync" "sync/atomic" @@ -24,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apimachinery/validation" + "github.com/grafana/grafana/pkg/infra/log" secrets "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util/scheduler" @@ -320,7 +320,7 @@ func NewResourceServer(opts ResourceServerOptions) (*server, error) { } } - logger := slog.Default().With("logger", "resource-server") + logger := log.New("resource-server") // Make this cancelable ctx, cancel := context.WithCancel(context.Background()) @@ -366,7 +366,7 @@ func NewResourceServer(opts ResourceServerOptions) (*server, error) { var _ ResourceServer = &server{} type server struct { - log *slog.Logger + log log.Logger backend StorageBackend blob BlobSupport secure secrets.InlineSecureValueSupport @@ -470,14 +470,15 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *resour return nil, AsErrorResult(err) } + l := s.log.FromContext(ctx) if obj.GetUID() == "" { // TODO! once https://github.com/grafana/grafana/pull/96086 is deployed everywhere // return nil, NewBadRequestError("object is missing UID") - s.log.Error("object is missing UID", "key", key) + l.Error("object is missing UID", "key", key) } if obj.GetResourceVersion() != "" { - s.log.Error("object must not include a resource version", "key", key) + l.Error("object must not include a resource version", "key", key) } // Make sure the command labels are not saved @@ -694,7 +695,7 @@ func (s *server) create(ctx context.Context, user claims.AuthInfo, req *resource if err != nil { rsp.Error = AsErrorResult(err) } - s.log.Debug("server.WriteEvent", "type", event.Type, "rv", rsp.ResourceVersion, "previousRV", event.PreviousRV, "group", event.Key.Group, "namespace", event.Key.Namespace, "name", event.Key.Name, "resource", event.Key.Resource) + s.log.FromContext(ctx).Debug("server.WriteEvent", "type", event.Type, "rv", rsp.ResourceVersion, "previousRV", event.PreviousRV, "group", event.Key.Group, "namespace", event.Key.Namespace, "name", event.Key.Name, "resource", event.Key.Resource) return rsp, nil } diff --git a/pkg/storage/unified/resource/server_test.go b/pkg/storage/unified/resource/server_test.go index 38df20e8151..504c49616f9 100644 --- a/pkg/storage/unified/resource/server_test.go +++ b/pkg/storage/unified/resource/server_test.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "errors" - "log/slog" "net/http" "strings" "sync" @@ -581,7 +580,7 @@ func newTestServerWithQueue(t *testing.T, maxSizePerTenant int, numWorkers int) MaxRetries: 2, MinBackoff: 10 * time.Millisecond, }, - log: slog.Default(), + log: log.NewNopLogger(), } return s, q } @@ -589,7 +588,7 @@ func newTestServerWithQueue(t *testing.T, maxSizePerTenant int, numWorkers int) func TestArtificialDelayAfterSuccessfulOperation(t *testing.T) { s := &server{ artificialSuccessfulWriteDelay: 1 * time.Millisecond, - log: slog.Default(), + log: log.NewNopLogger(), } check := func(t *testing.T, expectedSleep bool, res responseWithErrorResult, err error) { diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 836c491b3c7..36c68183203 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "log/slog" "math" "os" "path/filepath" @@ -71,7 +70,7 @@ type BleveOptions struct { BuildVersion string - Logger *slog.Logger + Logger log.Logger // Minimum time between index updates. IndexMinUpdateInterval time.Duration @@ -84,7 +83,7 @@ type BleveOptions struct { type bleveBackend struct { tracer trace.Tracer - log *slog.Logger + log log.Logger opts BleveOptions // set from opts.OwnsIndex, always non-nil @@ -125,9 +124,9 @@ func NewBleveBackend(opts BleveOptions, tracer trace.Tracer, indexMetrics *resou } } - log := opts.Logger - if log == nil { - log = slog.Default().With("logger", "bleve-backend") + l := opts.Logger + if l == nil { + l = log.New("bleve-backend") } ownFn := opts.OwnsIndex @@ -137,7 +136,7 @@ func NewBleveBackend(opts BleveOptions, tracer trace.Tracer, indexMetrics *resou } be := &bleveBackend{ - log: log, + log: l, tracer: tracer, cache: map[resource.NamespacedResource]*bleveIndex{}, opts: opts, @@ -381,7 +380,7 @@ func (b *bleveBackend) BuildIndex( return nil, err } - logWithDetails := b.log.With("namespace", key.Namespace, "group", key.Group, "resource", key.Resource, "size", size, "reason", indexBuildReason) + logWithDetails := b.log.FromContext(ctx).New("namespace", key.Namespace, "group", key.Group, "resource", key.Resource, "size", size, "reason", indexBuildReason) // Close the newly created/opened index by default. closeIndex := true @@ -462,7 +461,7 @@ func (b *bleveBackend) BuildIndex( } // Batch all the changes - idx := b.newBleveIndex(key, index, newIndexType, fields, allFields, standardSearchFields, updater, b.log.With("namespace", key.Namespace, "group", key.Group, "resource", key.Resource)) + idx := b.newBleveIndex(key, index, newIndexType, fields, allFields, standardSearchFields, updater, b.log.New("namespace", key.Namespace, "group", key.Group, "resource", key.Resource)) if build { if b.indexMetrics != nil { @@ -714,7 +713,7 @@ type bleveIndex struct { // The values returned with all allFields []*resourcepb.ResourceTableColumnDefinition tracing trace.Tracer - logger *slog.Logger + logger log.Logger updaterFn resource.UpdateFn minUpdateInterval time.Duration @@ -741,7 +740,7 @@ func (b *bleveBackend) newBleveIndex( allFields []*resourcepb.ResourceTableColumnDefinition, standardSearchFields resource.SearchableDocumentFields, updaterFn resource.UpdateFn, - logger *slog.Logger, + logger log.Logger, ) *bleveIndex { bi := &bleveIndex{ key: key, diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index b303d11c6e8..4c66fc4c1b5 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "log/slog" "math" "os" "path/filepath" @@ -26,7 +25,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/infra/log/logtest" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/store/kind/dashboard" @@ -772,7 +771,7 @@ func setupBleveBackend(t *testing.T, options ...setupOption) (*bleveBackend, pro opts := BleveOptions{ FileThreshold: defaultFileThreshold, IndexCacheTTL: defaultIndexCacheTTL, - Logger: slog.New(logtest.NewNopHandler(t)), + Logger: log.NewNopLogger(), BuildVersion: buildVersion, } for _, opt := range options { From 198375599eb9d95a7a7094702ff86c42a8477c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Fri, 21 Nov 2025 14:41:45 +0100 Subject: [PATCH 052/378] Fix/ignore some common dashboard problems (#114296) * Fix some common problems found when parsing dashboards for indexing --- .../store/kind/dashboard/dashboard.go | 31 +++- .../store/kind/dashboard/dashboard_test.go | 1 + .../k8s-wrapper-with-parsing-errors-info.json | 74 ++++++++++ .../k8s-wrapper-with-parsing-errors.json | 133 ++++++++++++++++++ 4 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 pkg/services/store/kind/dashboard/testdata/k8s-wrapper-with-parsing-errors-info.json create mode 100644 pkg/services/store/kind/dashboard/testdata/k8s-wrapper-with-parsing-errors.json diff --git a/pkg/services/store/kind/dashboard/dashboard.go b/pkg/services/store/kind/dashboard/dashboard.go index 656eefc15f3..88cbb4d1f45 100644 --- a/pkg/services/store/kind/dashboard/dashboard.go +++ b/pkg/services/store/kind/dashboard/dashboard.go @@ -276,6 +276,12 @@ func readDashboardIter(jsonPath string, iter *jsoniter.Iterator, lookup Datasour } for sub := iter.ReadObject(); sub != ""; sub = iter.ReadObject() { + // Skip all null values silently. + if iter.WhatIsNext() == jsoniter.NilValue { + iter.Skip() + continue + } + if sub == "list" { templatingListPath := templatingPath + ".list" if !checkAndSkipUnexpectedElement(iter, templatingListPath, lc, jsoniter.ArrayValue) { @@ -283,6 +289,12 @@ func readDashboardIter(jsonPath string, iter *jsoniter.Iterator, lookup Datasour } for ix := 0; iter.ReadArray(); ix++ { + // Skip all null elements silently. + if iter.WhatIsNext() == jsoniter.NilValue { + iter.Skip() + continue + } + tv := templateVariable{} templatingListElementPath := fmt.Sprintf("%s[%d]", templatingListPath, ix) @@ -308,7 +320,12 @@ func readDashboardIter(jsonPath string, iter *jsoniter.Iterator, lookup Datasour case "query": tv.query = iter.Read() case "current": - if !checkAndSkipUnexpectedElement(iter, templatingListElementPath+".current", lc, jsoniter.ObjectValue) { + if !checkAndSkipUnexpectedElement(iter, templatingListElementPath+".current", lc, jsoniter.ObjectValue, jsoniter.ArrayValue) { + continue + } + + if iter.WhatIsNext() == jsoniter.ArrayValue { + iter.Skip() continue } @@ -527,10 +544,18 @@ func readpanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup, jsonPath st switch field { case "id": - if !checkAndSkipUnexpectedElement(iter, jsonPath+".id", lc, jsoniter.NumberValue) { + if !checkAndSkipUnexpectedElement(iter, jsonPath+".id", lc, jsoniter.NumberValue, jsoniter.StringValue) { continue } - panel.ID = iter.ReadInt64() + + if iter.WhatIsNext() == jsoniter.StringValue { + id, err := strconv.ParseInt(iter.ReadString(), 10, 64) + if err == nil { + panel.ID = id + } + } else { + panel.ID = iter.ReadInt64() + } case "type": if !checkAndSkipUnexpectedElement(iter, jsonPath+".type", lc, jsoniter.StringValue) { diff --git a/pkg/services/store/kind/dashboard/dashboard_test.go b/pkg/services/store/kind/dashboard/dashboard_test.go index 5e31a86f8bd..91e657df1a8 100644 --- a/pkg/services/store/kind/dashboard/dashboard_test.go +++ b/pkg/services/store/kind/dashboard/dashboard_test.go @@ -75,6 +75,7 @@ func TestReadDashboard(t *testing.T) { "k8s-wrapper", "k8s-wrapper-editable-string", "k8s-wrapper-tags-string", + "k8s-wrapper-with-parsing-errors", } devdash := "../../../../../devenv/dev-dashboards/" diff --git a/pkg/services/store/kind/dashboard/testdata/k8s-wrapper-with-parsing-errors-info.json b/pkg/services/store/kind/dashboard/testdata/k8s-wrapper-with-parsing-errors-info.json new file mode 100644 index 00000000000..6451c3d805b --- /dev/null +++ b/pkg/services/store/kind/dashboard/testdata/k8s-wrapper-with-parsing-errors-info.json @@ -0,0 +1,74 @@ +{ + "id": 141, + "title": "pppp", + "tags": null, + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ], + "panels": [ + { + "id": 1, + "title": "green pie", + "libraryPanel": "a7975b7a-fb53-4ab7-951d-15810953b54f", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + }, + { + "id": 2, + "title": "green pie", + "libraryPanel": "e1d5f519-dabd-47c6-9ad7-83d181ce1cee", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + }, + { + "id": 7, + "title": "", + "type": "barchart", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + }, + { + "id": 8, + "title": "", + "type": "graph", + "datasource": [ + { + "uid": "default.uid", + "type": "default.type" + } + ] + }, + { + "id": 3, + "title": "collapsed row", + "type": "row", + "collapsed": [ + { + "id": 42, + "title": "blue pie", + "libraryPanel": "l3d2s634-fdgf-75u4-3fg3-67j966ii7jur" + } + ] + } + ], + "schemaVersion": 38, + "linkCount": 0, + "timeFrom": "now-6h", + "timeTo": "now", + "timezone": "" +} \ No newline at end of file diff --git a/pkg/services/store/kind/dashboard/testdata/k8s-wrapper-with-parsing-errors.json b/pkg/services/store/kind/dashboard/testdata/k8s-wrapper-with-parsing-errors.json new file mode 100644 index 00000000000..73d2ce0ea22 --- /dev/null +++ b/pkg/services/store/kind/dashboard/testdata/k8s-wrapper-with-parsing-errors.json @@ -0,0 +1,133 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "adfbg6f", + "namespace": "default", + "uid": "b396894e-56bf-4a01-837b-64157912ca00", + "creationTimestamp": "2024-10-30T18:30:54Z", + "annotations": { + "grafana.app/createdBy": "user:be2g71ke8yoe8b", + "grafana.app/originHash": "Grafana v9.2.0 (NA)", + "grafana.app/originName": "UI", + "grafana.app/originPath": "/dashboard/new" + } + }, + "spec": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 141, + "links": [], + "liveNow": false, + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": "1", + "libraryPanel": { + "name": "green pie", + "uid": "a7975b7a-fb53-4ab7-951d-15810953b54f" + }, + "title": "green pie" + }, + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "libraryPanel": { + "name": "red pie", + "uid": "e1d5f519-dabd-47c6-9ad7-83d181ce1cee" + }, + "title": "green pie" + }, + { + "id": "7", + "type": "barchart" + }, + { + "id": 8, + "type": "graph" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 3, + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 42, + "libraryPanel": { + "name": "blue pie", + "uid": "l3d2s634-fdgf-75u4-3fg3-67j966ii7jur" + }, + "title": "blue pie" + } + ], + "title": "collapsed row", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 38, + "tags": [], + "templating": { + "list": null, + "list": [ + null, + { + "current": [] + }, + { + "current": { + "value": "default" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "pppp", + "uid": "adfbg6f", + "version": 3, + "weekStart": "" + } +} From 10e0c435ad89a0a8c1e2bebda1175d3070865e5f Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 21 Nov 2025 14:59:25 +0100 Subject: [PATCH 053/378] Alerting: Fix NotificationPreview permission checking (#114303) Add permission check to NotificationPreview component --- .../NotificationPreview.tsx | 59 ++++++++++++++----- .../alerting/unified/hooks/useAbilities.ts | 16 +++++ public/locales/en-US/grafana.json | 4 +- 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.tsx index 0e75531c986..71b05c172cb 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.tsx @@ -4,8 +4,10 @@ import { useEffectOnce } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { Button, LoadingPlaceholder, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui'; +import { Alert, Button, LoadingPlaceholder, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui'; +import { contextSrv } from 'app/core/services/context_srv'; import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi'; +import { AccessControlAction } from 'app/types/accessControl'; import { AlertQuery, Labels } from 'app/types/unified-alerting-dto'; import { Folder, KBObjectArray } from '../../../types/rule-form'; @@ -36,6 +38,7 @@ export const NotificationPreview = ({ alertUid, }: NotificationPreviewProps) => { const styles = useStyles2(getStyles); + const previewRoutingDisabled = !condition || !folder; const [trigger, { data = [], isLoading, isUninitialized: previewUninitialized }] = preview.useMutation(); @@ -67,9 +70,7 @@ export const NotificationPreview = ({ }; useEffectOnce(() => { - if (!previewRoutingDisabled) { - onPreview(); - } + onPreview(); }); // Get alert managers's data source information @@ -77,18 +78,18 @@ export const NotificationPreview = ({ const singleAlertManagerConfigured = alertManagerDataSources.length === 1; const getTooltipContent = () => { + if (!condition) { + return ( + + Select a query condition to preview routing + + ); + } if (!folder) { return ( Select a folder to preview routing ); } - if (previewRoutingDisabled) { - return ( - - You don't have sufficient permissions to preview - - ); - } return ''; }; @@ -146,10 +147,12 @@ export const NotificationPreview = ({ )} {alertManagerSource.name === 'grafana' ? ( - + + + ) : ( {children}; + } + + return ( + + + You don't have permission to view notification policies. Preview is not available. + + + ); +} + const getStyles = (theme: GrafanaTheme2) => ({ firstAlertManagerLine: css({ height: '1px', diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index 5dd43d6db63..5635a3d666a 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -125,6 +125,22 @@ const AlwaysSupported = true; const NotSupported = false; export type Action = AlertmanagerAction | AlertingAction | AlertRuleAction | FolderBulkAction; + +/** + * Represents the ability to perform an action, with two distinct checks: + * + * @param actionSupported - Whether the action is technically possible in the current context. + * This depends on system capabilities (e.g., API availability), feature availability + * (e.g., Grafana vs external alertmanager), and rule state (e.g., provisioned/immutable rules). + * Examples: Can't edit provisioned rules, can't export from external alertmanagers. + * + * @param actionAllowed - Whether the user has permission to perform the action. + * This is based on RBAC permissions, folder-specific permissions, and admin status. + * Examples: User lacks AlertingRuleUpdate permission, user can't edit in this folder. + * + * Both must be true for an action to be available. This separation allows showing appropriate + * messages: "Feature not available" vs "You don't have permission". + */ export type Ability = [actionSupported: boolean, actionAllowed: boolean]; export type Abilities = Record; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 8793ca25bdf..0c9b8b63682 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1982,9 +1982,11 @@ }, "notification-preview": { "alertmanager": "Alertmanager:", - "disabled-tooltip": "You don't have sufficient permissions to preview", "error": "Could not load routing preview for {{alertmanager}}", "initialized": "Based on the labels added, alert instances are routed to the following notification policies. Expand each notification policy below to view more details.", + "no-condition-tooltip": "Select a query condition to preview routing", + "permission-warning": "Preview not available", + "permission-warning-message": "You don't have permission to view notification policies. Preview is not available.", "preview-routing": "Preview routing", "select-folder-tooltip": "Select a folder to preview routing", "text-loading-preview": "Loading preview...", From 57d02283178efb85223c39e98f49a48066b892a1 Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 21 Nov 2025 23:01:52 +0900 Subject: [PATCH 054/378] Tempo: Bump gRPC limit to allow for larger streaming queries (#114286) --- pkg/tsdb/tempo/grpc.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/tsdb/tempo/grpc.go b/pkg/tsdb/tempo/grpc.go index 81a41020b6c..ac1ff74d424 100644 --- a/pkg/tsdb/tempo/grpc.go +++ b/pkg/tsdb/tempo/grpc.go @@ -88,6 +88,7 @@ func getDialOpts(ctx context.Context, settings backend.DataSourceInstanceSetting var dialOps []grpc.DialOption + dialOps = append(dialOps, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(100*1024*1024))) dialOps = append(dialOps, grpc.WithChainStreamInterceptor(CustomHeadersStreamInterceptor(opts))) if settings.BasicAuthEnabled { // If basic authentication is enabled, it uses TLS transport credentials and sets the basic authentication header for each RPC call. From 133677d1d643c66a256f794352158f841ba786e4 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Fri, 21 Nov 2025 08:05:19 -0600 Subject: [PATCH 055/378] Explore: fix logs viz toggle alignment (#114302) chore: fix alignment --- public/app/features/explore/Logs/Logs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index d1e7bc6b461..02b63224e76 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -1235,7 +1235,7 @@ const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: n visualisationType: css({ display: 'flex', flex: '1', - justifyContent: 'space-between', + justifyContent: 'flex-end', }), visualisationTypeRadio: css({ margin: `0 0 0 ${theme.spacing(1)}`, From 0d67442f1a0ce9cba61407cca874e69cd70fcd33 Mon Sep 17 00:00:00 2001 From: Bruno Date: Fri, 21 Nov 2025 11:20:16 -0300 Subject: [PATCH 056/378] Secrets manager: create secure value using the active keeper (#114039) * Secrets manager: create secure value using the active keeper * SecureValueService.Update: fetch secure value from db to get the keeper * fix keeper_store_test.go * SecureValueService: fix bug in update where the current version keeper wasn't being passed to the createNewVersion method * make gofmt * remove outdated test * update TestModel * undo enterprise_imports changes * use xkube.Namespace * migrator: set secret_secure_value.keeper to 'system' when the column is null * indent cue * fix tests * fix enterprise imports * properly fix enterprise imports * make update-workspace * go mod tidy --------- Co-authored-by: Matheus Macabu --- apps/secret/kinds/v1beta1/securevalue.cue | 10 +- .../pkg/apis/secret/v1beta1/register.go | 2 +- .../secret/v1beta1/securevalue_spec_gen.go | 6 - .../secret/v1beta1/securevalue_status_gen.go | 8 +- .../pkg/apis/secret/v1beta1/zz_openapi_gen.go | 22 +- pkg/registry/apis/secret/contracts/keeper.go | 7 +- .../apis/secret/contracts/secure_value.go | 3 +- .../secret/garbagecollectionworker/worker.go | 6 +- .../garbagecollectionworker/worker_test.go | 2 +- .../apis/secret/service/secure_value.go | 62 +++-- pkg/registry/apis/secret/validator/keeper.go | 4 + .../apis/secret/validator/keeper_test.go | 53 ++--- .../apis/secret/validator/secure_value.go | 5 - .../secret/validator/secure_value_test.go | 24 +- .../metadata/data/keeper_read_active.sql | 20 ++ .../metadata/data/keeper_set_as_active.sql | 5 + pkg/storage/secret/metadata/decrypt_store.go | 2 +- pkg/storage/secret/metadata/keeper_model.go | 15 ++ pkg/storage/secret/metadata/keeper_store.go | 119 +++++++++- .../secret/metadata/keeper_store_test.go | 47 +++- pkg/storage/secret/metadata/query.go | 35 ++- pkg/storage/secret/metadata/query_test.go | 18 ++ .../secret/metadata/secure_value_model.go | 11 +- .../secret/metadata/secure_value_store.go | 33 +-- .../metadata/secure_value_store_test.go | 16 +- .../secret/metadata/secure_value_test.go | 216 ++++++++++++++++-- .../mysql--keeper_read_active-read active.sql | 20 ++ ...per_set_as_active-keeper set as active.sql | 5 + ...stgres--keeper_read_active-read active.sql | 20 ++ ...per_set_as_active-keeper set as active.sql | 5 + ...sqlite--keeper_read_active-read active.sql | 20 ++ ...per_set_as_active-keeper set as active.sql | 5 + pkg/storage/secret/migrator/migrator.go | 17 +- 33 files changed, 647 insertions(+), 196 deletions(-) create mode 100644 pkg/storage/secret/metadata/data/keeper_read_active.sql create mode 100644 pkg/storage/secret/metadata/data/keeper_set_as_active.sql create mode 100755 pkg/storage/secret/metadata/testdata/mysql--keeper_read_active-read active.sql create mode 100755 pkg/storage/secret/metadata/testdata/mysql--keeper_set_as_active-keeper set as active.sql create mode 100755 pkg/storage/secret/metadata/testdata/postgres--keeper_read_active-read active.sql create mode 100755 pkg/storage/secret/metadata/testdata/postgres--keeper_set_as_active-keeper set as active.sql create mode 100755 pkg/storage/secret/metadata/testdata/sqlite--keeper_read_active-read active.sql create mode 100755 pkg/storage/secret/metadata/testdata/sqlite--keeper_set_as_active-keeper set as active.sql diff --git a/apps/secret/kinds/v1beta1/securevalue.cue b/apps/secret/kinds/v1beta1/securevalue.cue index 6305bb563ff..04d85c1757f 100644 --- a/apps/secret/kinds/v1beta1/securevalue.cue +++ b/apps/secret/kinds/v1beta1/securevalue.cue @@ -29,13 +29,6 @@ SecureValueSpec: { // +optional ref?: string & strings.MinRunes(1) & strings.MaxRunes(1024) - // Name of the keeper, being the actual storage of the secure value. - // If not specified, the default keeper for the namespace will be used. - // +k8s:validation:minLength=1 - // +k8s:validation:maxLength=253 - // +optional - keeper?: string & strings.MinRunes(1) & strings.MaxRunes(253) - // The Decrypters that are allowed to decrypt this secret. // An empty list means no service can decrypt it. // +k8s:validation:maxItems=64 @@ -53,4 +46,7 @@ SecureValueStatus: { // External ID where the secret is stored. Cannot be set. // +optional externalID: string + + // The name of the keeper used to create the secure value. Cannot be set. + keeper: string } diff --git a/apps/secret/pkg/apis/secret/v1beta1/register.go b/apps/secret/pkg/apis/secret/v1beta1/register.go index 32d89abf7d5..a2fac8dd59f 100644 --- a/apps/secret/pkg/apis/secret/v1beta1/register.go +++ b/apps/secret/pkg/apis/secret/v1beta1/register.go @@ -28,7 +28,7 @@ var SecureValuesResourceInfo = utils.NewResourceInfo( }, Reader: func(obj any) ([]any, error) { if r, ok := obj.(*SecureValue); ok { - return []any{r.Name, r.Spec.Description, r.Spec.Keeper, r.Spec.Ref}, nil + return []any{r.Name, r.Spec.Description, r.Status.Keeper, r.Spec.Ref}, nil } return nil, fmt.Errorf("expected SecureValue but got %T", obj) diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_spec_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_spec_gen.go index 1b5d0956893..8ac05275ca3 100644 --- a/apps/secret/pkg/apis/secret/v1beta1/securevalue_spec_gen.go +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_spec_gen.go @@ -25,12 +25,6 @@ type SecureValueSpec struct { // +k8s:validation:maxLength=1024 // +optional Ref *string `json:"ref,omitempty"` - // Name of the keeper, being the actual storage of the secure value. - // If not specified, the default keeper for the namespace will be used. - // +k8s:validation:minLength=1 - // +k8s:validation:maxLength=253 - // +optional - Keeper *string `json:"keeper,omitempty"` // The Decrypters that are allowed to decrypt this secret. // An empty list means no service can decrypt it. // +k8s:validation:maxItems=64 diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go index a2534ab464d..a952ef13ef0 100644 --- a/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go @@ -25,12 +25,14 @@ type SecureValueStatus struct { // Version of the secure value. Cannot be set. // +optional Version int64 `json:"version"` - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]SecureValuestatusOperatorState `json:"operatorStates,omitempty"` // External ID where the secret is stored. Cannot be set. // +optional ExternalID string `json:"externalID"` + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]SecureValuestatusOperatorState `json:"operatorStates,omitempty"` + // The name of the keeper used to create the secure value. Cannot be set. + Keeper string `json:"keeper"` // additionalFields is reserved for future use AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` } diff --git a/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go b/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go index 7a3b6d090c0..101e4bb55b3 100644 --- a/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go +++ b/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go @@ -587,15 +587,6 @@ func schema_pkg_apis_secret_v1beta1_SecureValueSpec(ref common.ReferenceCallback Format: "", }, }, - "keeper": { - SchemaProps: spec.SchemaProps{ - Description: "Name of the keeper, being the actual storage of the secure value. If not specified, the default keeper for the namespace will be used.", - MinLength: ptr.To[int64](1), - MaxLength: ptr.To[int64](253), - Type: []string{"string"}, - Format: "", - }, - }, "decrypters": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ @@ -639,6 +630,14 @@ func schema_pkg_apis_secret_v1beta1_SecureValueStatus(ref common.ReferenceCallba Format: "int64", }, }, + "externalID": { + SchemaProps: spec.SchemaProps{ + Description: "External ID where the secret is stored. Cannot be set.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "operatorStates": { SchemaProps: spec.SchemaProps{ Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", @@ -654,9 +653,9 @@ func schema_pkg_apis_secret_v1beta1_SecureValueStatus(ref common.ReferenceCallba }, }, }, - "externalID": { + "keeper": { SchemaProps: spec.SchemaProps{ - Description: "External ID where the secret is stored. Cannot be set.", + Description: "The name of the keeper used to create the secure value. Cannot be set.", Default: "", Type: []string{"string"}, Format: "", @@ -678,6 +677,7 @@ func schema_pkg_apis_secret_v1beta1_SecureValueStatus(ref common.ReferenceCallba }, }, }, + Required: []string{"keeper"}, }, }, Dependencies: []string{ diff --git a/pkg/registry/apis/secret/contracts/keeper.go b/pkg/registry/apis/secret/contracts/keeper.go index a204bb5b1a7..1e8f5e2acfb 100644 --- a/pkg/registry/apis/secret/contracts/keeper.go +++ b/pkg/registry/apis/secret/contracts/keeper.go @@ -10,6 +10,9 @@ import ( ) var ( + // The name used to refer to the system keeper + SystemKeeperName = "system" + ErrKeeperNotFound = errors.New("keeper not found") ErrKeeperAlreadyExists = errors.New("keeper already exists") ) @@ -21,7 +24,9 @@ type KeeperMetadataStorage interface { Update(ctx context.Context, keeper *secretv1beta1.Keeper, actorUID string) (*secretv1beta1.Keeper, error) Delete(ctx context.Context, namespace xkube.Namespace, name string) error List(ctx context.Context, namespace xkube.Namespace) ([]secretv1beta1.Keeper, error) - GetKeeperConfig(ctx context.Context, namespace string, name *string, opts ReadOpts) (secretv1beta1.KeeperConfig, error) + GetKeeperConfig(ctx context.Context, namespace string, name string, opts ReadOpts) (secretv1beta1.KeeperConfig, error) + SetAsActive(ctx context.Context, namespace xkube.Namespace, name string) error + GetActiveKeeperConfig(ctx context.Context, namespace string) (string, secretv1beta1.KeeperConfig, error) } // ErrKeeperInvalidSecureValues is returned when a Keeper references SecureValues that do not exist. diff --git a/pkg/registry/apis/secret/contracts/secure_value.go b/pkg/registry/apis/secret/contracts/secure_value.go index 533cb005f79..bfb763be3f6 100644 --- a/pkg/registry/apis/secret/contracts/secure_value.go +++ b/pkg/registry/apis/secret/contracts/secure_value.go @@ -31,7 +31,7 @@ type ReadOpts struct { // SecureValueMetadataStorage is the interface for wiring and dependency injection. type SecureValueMetadataStorage interface { - Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) + Create(ctx context.Context, keeper string, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) Read(ctx context.Context, namespace xkube.Namespace, name string, opts ReadOpts) (*secretv1beta1.SecureValue, error) List(ctx context.Context, namespace xkube.Namespace) ([]secretv1beta1.SecureValue, error) SetVersionToActive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error @@ -47,6 +47,7 @@ type SecureValueService interface { List(ctx context.Context, namespace xkube.Namespace) (*secretv1beta1.SecureValueList, error) Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, bool, error) Delete(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error) + SetKeeperAsActive(ctx context.Context, namespace xkube.Namespace, keeperName string) error } type SecureValueClient interface { diff --git a/pkg/registry/apis/secret/garbagecollectionworker/worker.go b/pkg/registry/apis/secret/garbagecollectionworker/worker.go index e10bbed600e..b967b1f21a1 100644 --- a/pkg/registry/apis/secret/garbagecollectionworker/worker.go +++ b/pkg/registry/apis/secret/garbagecollectionworker/worker.go @@ -93,14 +93,14 @@ func (w *Worker) CleanupInactiveSecureValues(ctx context.Context) ([]secretv1bet } func (w *Worker) Cleanup(ctx context.Context, sv *secretv1beta1.SecureValue) error { - keeperCfg, err := w.keeperMetadataStorage.GetKeeperConfig(ctx, sv.Namespace, sv.Spec.Keeper, contracts.ReadOpts{ForUpdate: false}) + keeperCfg, err := w.keeperMetadataStorage.GetKeeperConfig(ctx, sv.Namespace, sv.Status.Keeper, contracts.ReadOpts{ForUpdate: false}) if err != nil { - return fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", sv.Namespace, sv.Spec.Keeper, err) + return fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", sv.Namespace, sv.Status.Keeper, err) } keeper, err := w.keeperService.KeeperForConfig(keeperCfg) if err != nil { - return fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", sv.Namespace, sv.Spec.Keeper, err) + return fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", sv.Namespace, sv.Status.Keeper, err) } // Keeper deletion is idempotent diff --git a/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go b/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go index 23467d69b39..0b5a7614205 100644 --- a/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go +++ b/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go @@ -52,7 +52,7 @@ func TestBasic(t *testing.T) { sv, err := sut.CreateSv(t.Context()) require.NoError(t, err) - keeperCfg, err := sut.KeeperMetadataStorage.GetKeeperConfig(t.Context(), sv.Namespace, sv.Spec.Keeper, contracts.ReadOpts{ForUpdate: false}) + keeperCfg, err := sut.KeeperMetadataStorage.GetKeeperConfig(t.Context(), sv.Namespace, sv.Status.Keeper, contracts.ReadOpts{ForUpdate: false}) require.NoError(t, err) keeper, err := sut.KeeperService.KeeperForConfig(keeperCfg) diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go index cc4e6c80c72..91f6a2e73ff 100644 --- a/pkg/registry/apis/secret/service/secure_value.go +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -93,7 +93,13 @@ func (s *SecureValueService) Create(ctx context.Context, sv *secretv1beta1.Secur s.metrics.SecureValueCreateDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds()) }() - return s.createNewVersion(ctx, sv, actorUID) + // Secure value creation uses the active keeper + keeperName, keeperCfg, err := s.keeperMetadataStorage.GetActiveKeeperConfig(ctx, sv.Namespace) + if err != nil { + return nil, fmt.Errorf("fetching active keeper config: namespace=%+v %w", sv.Namespace, err) + } + + return s.createNewVersion(ctx, keeperName, keeperCfg, sv, actorUID) } func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (_ *secretv1beta1.SecureValue, sync bool, updateErr error) { @@ -128,23 +134,22 @@ func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv s.metrics.SecureValueUpdateDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds()) }() + currentVersion, err := s.secureValueMetadataStorage.Read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{}) + if err != nil { + return nil, false, fmt.Errorf("reading secure value secret: %+w", err) + } + + keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, currentVersion.Namespace, currentVersion.Status.Keeper, contracts.ReadOpts{}) + if err != nil { + return nil, false, fmt.Errorf("fetching keeper config: namespace=%+v keeper: %q %w", newSecureValue.Namespace, currentVersion.Status.Keeper, err) + } + if newSecureValue.Spec.Value == nil { - currentVersion, err := s.secureValueMetadataStorage.Read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{}) - if err != nil { - return nil, false, fmt.Errorf("reading secure value secret: %+w", err) - } - - // TODO: does this need to be for update? - keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, newSecureValue.Namespace, newSecureValue.Spec.Keeper, contracts.ReadOpts{ForUpdate: true}) - if err != nil { - return nil, false, fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Spec.Keeper, err) - } - keeper, err := s.keeperService.KeeperForConfig(keeperCfg) if err != nil { - return nil, false, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Spec.Keeper, err) + return nil, false, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Status.Keeper, err) } - logging.FromContext(ctx).Debug("retrieved keeper", "namespace", newSecureValue.Namespace, "keeperName", newSecureValue.Spec.Keeper, "type", keeperCfg.Type()) + logging.FromContext(ctx).Debug("retrieved keeper", "namespace", newSecureValue.Namespace, "type", keeperCfg.Type()) secret, err := keeper.Expose(ctx, keeperCfg, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, currentVersion.Status.Version) if err != nil { @@ -154,12 +159,16 @@ func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv newSecureValue.Spec.Value = &secret } + // Secure value updates use the keeper used to create the secure value const updateIsSync = true - createdSv, err := s.createNewVersion(ctx, newSecureValue, actorUID) + createdSv, err := s.createNewVersion(ctx, currentVersion.Status.Keeper, keeperCfg, newSecureValue, actorUID) return createdSv, updateIsSync, err } -func (s *SecureValueService) createNewVersion(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) { +func (s *SecureValueService) createNewVersion(ctx context.Context, keeperName string, keeperCfg secretv1beta1.KeeperConfig, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) { + if keeperName == "" { + return nil, fmt.Errorf("keeper name is required, got empty string") + } if err := s.secureValueMutator.Mutate(sv, admission.Create); err != nil { return nil, err } @@ -168,25 +177,21 @@ func (s *SecureValueService) createNewVersion(ctx context.Context, sv *secretv1b return nil, contracts.NewErrValidateSecureValue(errorList) } - createdSv, err := s.secureValueMetadataStorage.Create(ctx, sv, actorUID) + createdSv, err := s.secureValueMetadataStorage.Create(ctx, keeperName, sv, actorUID) if err != nil { return nil, fmt.Errorf("creating secure value: %w", err) } + createdSv.Status = secretv1beta1.SecureValueStatus{ Version: createdSv.Status.Version, - } - - // TODO: does this need to be for update? - keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, createdSv.Namespace, createdSv.Spec.Keeper, contracts.ReadOpts{ForUpdate: true}) - if err != nil { - return nil, fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", createdSv.Namespace, createdSv.Spec.Keeper, err) + Keeper: keeperName, } keeper, err := s.keeperService.KeeperForConfig(keeperCfg) if err != nil { - return nil, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", createdSv.Namespace, createdSv.Spec.Keeper, err) + return nil, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", createdSv.Namespace, keeperName, err) } - logging.FromContext(ctx).Debug("retrieved keeper", "namespace", createdSv.Namespace, "keeperName", createdSv.Spec.Keeper, "type", keeperCfg.Type()) + logging.FromContext(ctx).Debug("retrieved keeper", "namespace", createdSv.Namespace, "type", keeperCfg.Type()) // TODO: can we stop using external id? // TODO: store uses only the namespace and returns and id. It could be a kv instead. @@ -364,3 +369,10 @@ func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespa return sv, nil } + +func (s *SecureValueService) SetKeeperAsActive(ctx context.Context, namespace xkube.Namespace, name string) error { + if err := s.keeperMetadataStorage.SetAsActive(ctx, namespace, name); err != nil { + return fmt.Errorf("calling keeper metadata storage to set keeper as active: %w", err) + } + return nil +} diff --git a/pkg/registry/apis/secret/validator/keeper.go b/pkg/registry/apis/secret/validator/keeper.go index 737ef355794..b4220b45a65 100644 --- a/pkg/registry/apis/secret/validator/keeper.go +++ b/pkg/registry/apis/secret/validator/keeper.go @@ -41,6 +41,10 @@ func (v *keeperValidator) Validate(keeper *secretv1beta1.Keeper, oldKeeper *secr return errs } + if keeper.Name == contracts.SystemKeeperName { + errs = append(errs, field.Forbidden(field.NewPath("name"), "the keeper name `system` is reserved")) + } + if keeper.Spec.Description == "" { errs = append(errs, field.Required(field.NewPath("spec", "description"), "a `description` is required")) } diff --git a/pkg/registry/apis/secret/validator/keeper_test.go b/pkg/registry/apis/secret/validator/keeper_test.go index 1dcb51b3e31..99b108e480e 100644 --- a/pkg/registry/apis/secret/validator/keeper_test.go +++ b/pkg/registry/apis/secret/validator/keeper_test.go @@ -35,36 +35,6 @@ func TestValidateKeeper(t *testing.T) { }) }) - t.Run("only one `keeper` must be present", func(t *testing.T) { - keeper := &secretv1beta1.Keeper{ - ObjectMeta: objectMeta, - Spec: secretv1beta1.KeeperSpec{ - Description: "short description", - Aws: &secretv1beta1.KeeperAWSConfig{}, - Azure: &secretv1beta1.KeeperAzureConfig{}, - Gcp: &secretv1beta1.KeeperGCPConfig{}, - HashiCorpVault: &secretv1beta1.KeeperHashiCorpConfig{}, - }, - } - - errs := validator.Validate(keeper, nil, admission.Create) - require.Len(t, errs, 1) - require.Equal(t, "spec", errs[0].Field) - }) - - t.Run("at least one `keeper` must be present", func(t *testing.T) { - keeper := &secretv1beta1.Keeper{ - ObjectMeta: objectMeta, - Spec: secretv1beta1.KeeperSpec{ - Description: "description", - }, - } - - errs := validator.Validate(keeper, nil, admission.Create) - require.Len(t, errs, 1) - require.Equal(t, "spec", errs[0].Field) - }) - t.Run("aws keeper validation", func(t *testing.T) { validKeeperAWS := &secretv1beta1.Keeper{ ObjectMeta: objectMeta, @@ -341,4 +311,27 @@ func TestValidateKeeper(t *testing.T) { require.Len(t, errs, 1) require.Equal(t, "metadata.namespace", errs[0].Field) }) + + t.Run("keeper name `system` is reserved", func(t *testing.T) { + keeper := &secretv1beta1.Keeper{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system", + Namespace: "ns1", + }, + Spec: secretv1beta1.KeeperSpec{ + Description: "description", + HashiCorpVault: &secretv1beta1.KeeperHashiCorpConfig{ + Address: "http://address", + Token: secretv1beta1.KeeperCredentialValue{ + ValueFromConfig: "config.path.value", + }, + }, + }, + } + + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "name", errs[0].Field) + require.Equal(t, "the keeper name `system` is reserved", errs[0].Detail) + }) } diff --git a/pkg/registry/apis/secret/validator/secure_value.go b/pkg/registry/apis/secret/validator/secure_value.go index 0df885bff08..a29bb57d4ff 100644 --- a/pkg/registry/apis/secret/validator/secure_value.go +++ b/pkg/registry/apis/secret/validator/secure_value.go @@ -110,11 +110,6 @@ func validateSecureValueUpdate(sv, oldSv *secretv1beta1.SecureValue) field.Error } } - // Keeper cannot be changed. - if sv.Spec.Keeper != oldSv.Spec.Keeper { - errs = append(errs, field.Forbidden(field.NewPath("spec"), "the `keeper` cannot be changed")) - } - return errs } diff --git a/pkg/registry/apis/secret/validator/secure_value_test.go b/pkg/registry/apis/secret/validator/secure_value_test.go index 88a6220db54..b3aa25057fd 100644 --- a/pkg/registry/apis/secret/validator/secure_value_test.go +++ b/pkg/registry/apis/secret/validator/secure_value_test.go @@ -25,9 +25,9 @@ func TestValidateSecureValue(t *testing.T) { Spec: secretv1beta1.SecureValueSpec{ Description: "description", Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")), - Keeper: &keeper, Decrypters: []string{"app1", "app2"}, }, + Status: secretv1beta1.SecureValueStatus{Keeper: keeper}, } t.Run("the `description` must be present", func(t *testing.T) { @@ -182,28 +182,6 @@ func TestValidateSecureValue(t *testing.T) { require.Len(t, errs, 1) require.Equal(t, "spec", errs[0].Field) }) - - t.Run("when trying to change the `keeper`, it returns an error", func(t *testing.T) { - keeperA := "a-keeper" - keeperAnother := "another-keeper" - oldSv := &secretv1beta1.SecureValue{ - ObjectMeta: objectMeta, - Spec: secretv1beta1.SecureValueSpec{ - Keeper: &keeperA, - }, - } - - sv := &secretv1beta1.SecureValue{ - ObjectMeta: objectMeta, - Spec: secretv1beta1.SecureValueSpec{ - Keeper: &keeperAnother, - }, - } - - errs := validator.Validate(sv, oldSv, admission.Update) - require.Len(t, errs, 1) - require.Equal(t, "spec", errs[0].Field) - }) }) t.Run("`decrypters` must have unique items", func(t *testing.T) { diff --git a/pkg/storage/secret/metadata/data/keeper_read_active.sql b/pkg/storage/secret/metadata/data/keeper_read_active.sql new file mode 100644 index 00000000000..437e5de5c26 --- /dev/null +++ b/pkg/storage/secret/metadata/data/keeper_read_active.sql @@ -0,0 +1,20 @@ +SELECT + {{ .Ident "guid" }}, + {{ .Ident "name" }}, + {{ .Ident "namespace" }}, + {{ .Ident "annotations" }}, + {{ .Ident "labels" }}, + {{ .Ident "created" }}, + {{ .Ident "created_by" }}, + {{ .Ident "updated" }}, + {{ .Ident "updated_by" }}, + {{ .Ident "description" }}, + {{ .Ident "type" }}, + {{ .Ident "payload" }} +FROM + {{ .Ident "secret_keeper" }} +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "active" }} = true +LIMIT 1 +; diff --git a/pkg/storage/secret/metadata/data/keeper_set_as_active.sql b/pkg/storage/secret/metadata/data/keeper_set_as_active.sql new file mode 100644 index 00000000000..3c03610d304 --- /dev/null +++ b/pkg/storage/secret/metadata/data/keeper_set_as_active.sql @@ -0,0 +1,5 @@ +UPDATE {{ .Ident "secret_keeper" }} +SET {{ .Ident "active" }} = ({{ .Ident "name" }} = {{ .Arg .Name }}) +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} +; diff --git a/pkg/storage/secret/metadata/decrypt_store.go b/pkg/storage/secret/metadata/decrypt_store.go index ea9d851999d..4c0ebabb5e8 100644 --- a/pkg/storage/secret/metadata/decrypt_store.go +++ b/pkg/storage/secret/metadata/decrypt_store.go @@ -135,7 +135,7 @@ func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, return "", fmt.Errorf("failed to authorize decryption with reason %v (%w)", reason, contracts.ErrDecryptNotAuthorized) } - keeperConfig, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, namespace.String(), sv.Spec.Keeper, contracts.ReadOpts{}) + keeperConfig, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, namespace.String(), sv.Status.Keeper, contracts.ReadOpts{}) if err != nil { return "", fmt.Errorf("failed to read keeper config metadata storage: %v (%w)", err, contracts.ErrDecryptFailed) } diff --git a/pkg/storage/secret/metadata/keeper_model.go b/pkg/storage/secret/metadata/keeper_model.go index 8fd43dc35e0..4831ee25a2e 100644 --- a/pkg/storage/secret/metadata/keeper_model.go +++ b/pkg/storage/secret/metadata/keeper_model.go @@ -280,3 +280,18 @@ func extractSecureValues(kp *secretv1beta1.Keeper) map[string]struct{} { return nil } + +func getKeeperConfig(keeper *secretv1beta1.Keeper) secretv1beta1.KeeperConfig { + switch keeper.Spec.GetType() { + case secretv1beta1.AWSKeeperType: + return keeper.Spec.Aws + case secretv1beta1.AzureKeeperType: + return keeper.Spec.Azure + case secretv1beta1.GCPKeeperType: + return keeper.Spec.Gcp + case secretv1beta1.HashiCorpKeeperType: + return keeper.Spec.HashiCorpVault + default: + return nil + } +} diff --git a/pkg/storage/secret/metadata/keeper_store.go b/pkg/storage/secret/metadata/keeper_store.go index 38c27cd1873..6036bfe2502 100644 --- a/pkg/storage/secret/metadata/keeper_store.go +++ b/pkg/storage/secret/metadata/keeper_store.go @@ -2,6 +2,7 @@ package metadata import ( "context" + "errors" "fmt" "strconv" "time" @@ -186,7 +187,7 @@ func (s *keeperMetadataStorage) read(ctx context.Context, namespace, name string defer func() { _ = res.Close() }() if !res.Next() { - return nil, contracts.ErrKeeperNotFound + return nil, fmt.Errorf("keeper=%s: %w", name, contracts.ErrKeeperNotFound) } var keeper keeperDB @@ -568,9 +569,10 @@ func (s *keeperMetadataStorage) validateSecureValueReferences(ctx context.Contex return nil } -func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace string, name *string, opts contracts.ReadOpts) (_ secretv1beta1.KeeperConfig, getErr error) { +func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace string, name string, opts contracts.ReadOpts) (_ secretv1beta1.KeeperConfig, getErr error) { ctx, span := s.tracer.Start(ctx, "KeeperMetadataStorage.GetKeeperConfig", trace.WithAttributes( attribute.String("namespace", namespace), + attribute.String("name", name), attribute.Bool("isForUpdate", opts.ForUpdate), )) start := time.Now() @@ -581,6 +583,7 @@ func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace s args := []any{ "namespace", namespace, + "name", name, "isForUpdate", strconv.FormatBool(opts.ForUpdate), } @@ -597,14 +600,12 @@ func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace s }() // Check if keeper is the systemwide one. - if name == nil { + if name == contracts.SystemKeeperName { return &secretv1beta1.SystemKeeperConfig{}, nil } - span.SetAttributes(attribute.String("name", *name)) - // Load keeper config from metadata store, or TODO: keeper cache. - kp, err := s.read(ctx, namespace, *name, opts) + kp, err := s.read(ctx, namespace, name, opts) if err != nil { return nil, err } @@ -614,3 +615,109 @@ func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace s // TODO: this would be a good place to check if credentials are secure values and load them. return keeperConfig, nil } + +func (s *keeperMetadataStorage) SetAsActive(ctx context.Context, namespace xkube.Namespace, name string) error { + req := setKeeperAsActive{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace.String(), + Name: name, + } + + query, err := sqltemplate.Execute(sqlKeeperSetAsActive, req) + if err != nil { + return fmt.Errorf("template %q: %w", sqlKeeperSetAsActive.Name(), err) + } + + // Check keeper exists. No need to worry about time of check to time of use + // since trying to activate a just deleted keeper will result in all + // keepers being inactive and defaulting to the system keeper. + if _, err := s.read(ctx, namespace.String(), name, contracts.ReadOpts{}); err != nil { + return fmt.Errorf("reading keeper before setting as active: %w", err) + } + + _, err = s.db.ExecContext(ctx, query, req.GetArgs()...) + if err != nil { + return fmt.Errorf("setting keeper as active %q: %w", query, err) + } + + return nil +} + +func (s *keeperMetadataStorage) GetActiveKeeper(ctx context.Context, namespace string) (keeper *secretv1beta1.Keeper, readErr error) { + start := time.Now() + ctx, span := s.tracer.Start(ctx, "KeeperMetadataStorage.GetActiveKeeper", trace.WithAttributes( + attribute.String("namespace", namespace), + )) + defer span.End() + + defer func() { + success := readErr == nil + + args := []any{ + "namespace", namespace, + } + + args = append(args, "success", success) + if !success { + span.SetStatus(codes.Error, "KeeperMetadataStorage.GetActiveKeeper failed") + span.RecordError(readErr) + args = append(args, "error", readErr) + } + + logging.FromContext(ctx).Info("KeeperMetadataStorage.GetActiveKeeper", args...) + + s.metrics.KeeperMetadataGetDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds()) + }() + + req := &readActiveKeeper{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace, + } + + query, err := sqltemplate.Execute(sqlKeeperReadActive, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlKeeperReadActive.Name(), err) + } + + res, err := s.db.QueryContext(ctx, query, req.GetArgs()...) + if err != nil { + return nil, fmt.Errorf("executing query to fetch active keeper in namespace %s: %w", namespace, err) + } + defer func() { _ = res.Close() }() + + if !res.Next() { + return nil, contracts.ErrKeeperNotFound + } + + var keeperDB keeperDB + err = res.Scan( + &keeperDB.GUID, &keeperDB.Name, &keeperDB.Namespace, &keeperDB.Annotations, &keeperDB.Labels, &keeperDB.Created, + &keeperDB.CreatedBy, &keeperDB.Updated, &keeperDB.UpdatedBy, &keeperDB.Description, &keeperDB.Type, &keeperDB.Payload, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan keeper row: %w", err) + } + if err := res.Err(); err != nil { + return nil, fmt.Errorf("read rows error: %w", err) + } + + keeper, readErr = keeperDB.toKubernetes() + if readErr != nil { + return keeper, fmt.Errorf("converting from keeperDB to kubernetes struct: %w", err) + } + + return keeper, nil +} + +func (s *keeperMetadataStorage) GetActiveKeeperConfig(ctx context.Context, namespace string) (string, secretv1beta1.KeeperConfig, error) { + keeper, err := s.GetActiveKeeper(ctx, namespace) + if err != nil { + // When there are not active keepers, default to the system keeper + if errors.Is(err, contracts.ErrKeeperNotFound) { + return contracts.SystemKeeperName, &secretv1beta1.SystemKeeperConfig{}, nil + } + return "", nil, fmt.Errorf("fetching active keeper from db: %w", err) + } + + return keeper.Name, getKeeperConfig(keeper), nil +} diff --git a/pkg/storage/secret/metadata/keeper_store_test.go b/pkg/storage/secret/metadata/keeper_store_test.go index f21fbdaa575..a38e0ec85b3 100644 --- a/pkg/storage/secret/metadata/keeper_store_test.go +++ b/pkg/storage/secret/metadata/keeper_store_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" @@ -40,7 +41,7 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { keeperMetadataStorage := initStorage(t) // get system keeper config - keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, defaultKeeperNS, nil, contracts.ReadOpts{}) + keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, defaultKeeperNS, contracts.SystemKeeperName, contracts.ReadOpts{}) require.NoError(t, err) require.IsType(t, &secretv1beta1.SystemKeeperConfig{}, keeperConfig) }) @@ -55,7 +56,7 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { _, err := keeperMetadataStorage.Create(ctx, testKeeper, "testuser") require.NoError(t, err) - keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, defaultKeeperNS, &defaultKeeperName, contracts.ReadOpts{}) + keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, defaultKeeperNS, defaultKeeperName, contracts.ReadOpts{}) require.NoError(t, err) require.NotNil(t, keeperConfig) require.NotEmpty(t, keeperConfig.Type()) @@ -105,7 +106,7 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { require.NoError(t, err) // we are able to get it - keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, keeperNamespaceTest, &keeperTest, contracts.ReadOpts{}) + keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, keeperNamespaceTest, keeperTest, contracts.ReadOpts{}) require.NoError(t, err) require.NotNil(t, keeperConfig) require.NotEmpty(t, keeperConfig.Type()) @@ -115,7 +116,7 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { require.NoError(t, delErr) // and we shouldn't be able to get it again - _, getErr := keeperMetadataStorage.GetKeeperConfig(ctx, keeperNamespaceTest, &keeperTest, contracts.ReadOpts{}) + _, getErr := keeperMetadataStorage.GetKeeperConfig(ctx, keeperNamespaceTest, keeperTest, contracts.ReadOpts{}) require.Errorf(t, getErr, "keeper not found") }) @@ -162,7 +163,7 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { require.NoError(t, err) // Validate updated values - updatedConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, keeperNamespaceTest, &keeperTest, contracts.ReadOpts{}) + updatedConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, keeperNamespaceTest, keeperTest, contracts.ReadOpts{}) require.NoError(t, err) require.NotNil(t, updatedConfig) require.NotEmpty(t, updatedConfig.Type()) @@ -260,7 +261,7 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { _, err := keeperMetadataStorage.Read(ctx, "ns", "non-existent", contracts.ReadOpts{}) require.Error(t, err) - require.Equal(t, contracts.ErrKeeperNotFound, err) + require.ErrorIs(t, err, contracts.ErrKeeperNotFound) }) t.Run("update keeper with different namespace", func(t *testing.T) { @@ -329,6 +330,40 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { }) } +func Test_KeeperMetadataStorage_SetAsActive(t *testing.T) { + t.Parallel() + + keeperMetadataStorage := initStorage(t) + + k1, err := keeperMetadataStorage.Create(t.Context(), &secretv1beta1.Keeper{ + ObjectMeta: v1.ObjectMeta{Namespace: "ns1", Name: "k1"}, + Spec: secretv1beta1.KeeperSpec{ + Description: "description", + Aws: &secretv1beta1.KeeperAWSConfig{}, + }, + }, "actor-uid") + require.NoError(t, err) + + k2, err := keeperMetadataStorage.Create(t.Context(), &secretv1beta1.Keeper{ + ObjectMeta: v1.ObjectMeta{Namespace: "ns1", Name: "k2"}, + Spec: secretv1beta1.KeeperSpec{ + Description: "description", + Aws: &secretv1beta1.KeeperAWSConfig{}, + }, + }, "actor-uid") + require.NoError(t, err) + + require.NoError(t, keeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(k1.Namespace), k1.Name)) + keeperName, _, err := keeperMetadataStorage.GetActiveKeeperConfig(t.Context(), k1.Namespace) + require.NoError(t, err) + require.Equal(t, k1.Name, keeperName) + + require.NoError(t, keeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(k2.Namespace), k2.Name)) + keeperName, _, err = keeperMetadataStorage.GetActiveKeeperConfig(t.Context(), k2.Namespace) + require.NoError(t, err) + require.Equal(t, k2.Name, keeperName) +} + func initStorage(t *testing.T) contracts.KeeperMetadataStorage { testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) tracer := noop.NewTracerProvider().Tracer("test") diff --git a/pkg/storage/secret/metadata/query.go b/pkg/storage/secret/metadata/query.go index d71b2a508e2..8e35099a250 100644 --- a/pkg/storage/secret/metadata/query.go +++ b/pkg/storage/secret/metadata/query.go @@ -15,11 +15,13 @@ var ( sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `data/*.sql`)) // The SQL Commands - sqlKeeperCreate = mustTemplate("keeper_create.sql") - sqlKeeperRead = mustTemplate("keeper_read.sql") - sqlKeeperUpdate = mustTemplate("keeper_update.sql") - sqlKeeperList = mustTemplate("keeper_list.sql") - sqlKeeperDelete = mustTemplate("keeper_delete.sql") + sqlKeeperCreate = mustTemplate("keeper_create.sql") + sqlKeeperRead = mustTemplate("keeper_read.sql") + sqlKeeperReadActive = mustTemplate("keeper_read_active.sql") + sqlKeeperUpdate = mustTemplate("keeper_update.sql") + sqlKeeperList = mustTemplate("keeper_list.sql") + sqlKeeperDelete = mustTemplate("keeper_delete.sql") + sqlKeeperSetAsActive = mustTemplate("keeper_set_as_active.sql") sqlKeeperListByName = mustTemplate("keeper_listByName.sql") sqlSecureValueListByName = mustTemplate("secure_value_listByName.sql") @@ -48,6 +50,18 @@ func mustTemplate(filename string) *template.Template { /**-- Keeper Queries --**/ /************************/ +// Set as active +type setKeeperAsActive struct { + sqltemplate.SQLTemplate + Namespace string + Name string +} + +// Validate is only used if we use `dbutil` from `unifiedstorage` +func (r setKeeperAsActive) Validate() error { + return nil // TODO +} + // Create type createKeeper struct { sqltemplate.SQLTemplate @@ -72,6 +86,17 @@ func (r readKeeper) Validate() error { return nil // TODO } +// Read active keeper +type readActiveKeeper struct { + sqltemplate.SQLTemplate + Namespace string +} + +// Validate is only used if we use `dbutil` from `unifiedstorage` +func (r readActiveKeeper) Validate() error { + return nil // TODO +} + // Update type updateKeeper struct { sqltemplate.SQLTemplate diff --git a/pkg/storage/secret/metadata/query_test.go b/pkg/storage/secret/metadata/query_test.go index 23f12d1927c..75cd970d382 100644 --- a/pkg/storage/secret/metadata/query_test.go +++ b/pkg/storage/secret/metadata/query_test.go @@ -13,6 +13,15 @@ func TestKeeperQueries(t *testing.T) { mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ RootDir: "testdata", Templates: map[*template.Template][]mocks.TemplateTestCase{ + sqlKeeperSetAsActive: { + { + Name: "keeper set as active", + Data: &setKeeperAsActive{ + SQLTemplate: mocks.NewTestingSQLTemplate(), Name: "name", + Namespace: "ns", + }, + }, + }, sqlKeeperCreate: { { Name: "create", @@ -54,6 +63,15 @@ func TestKeeperQueries(t *testing.T) { }, }, }, + sqlKeeperReadActive: { + { + Name: "read active", + Data: &readActiveKeeper{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Namespace: "ns", + }, + }, + }, sqlKeeperRead: { { Name: "read", diff --git a/pkg/storage/secret/metadata/secure_value_model.go b/pkg/storage/secret/metadata/secure_value_model.go index bed3a9ae053..f514fa5f2d9 100644 --- a/pkg/storage/secret/metadata/secure_value_model.go +++ b/pkg/storage/secret/metadata/secure_value_model.go @@ -79,7 +79,7 @@ func (sv *secureValueDB) toKubernetes() (*secretv1beta1.SecureValue, error) { } if sv.Keeper.Valid { - resource.Spec.Keeper = &sv.Keeper.String + resource.Status.Keeper = sv.Keeper.String } if sv.Ref.Valid { resource.Spec.Ref = &sv.Ref.String @@ -122,14 +122,13 @@ func (sv *secureValueDB) toKubernetes() (*secretv1beta1.SecureValue, error) { } // toCreateRow maps a Kubernetes resource into a DB row for new resources being created/inserted. -func toCreateRow(now time.Time, sv *secretv1beta1.SecureValue, actorUID string) (*secureValueDB, error) { - row, err := toRow(sv, "") +func toCreateRow(now time.Time, keeper string, sv *secretv1beta1.SecureValue, actorUID string) (*secureValueDB, error) { + row, err := toRow(keeper, sv, "") if err != nil { return nil, fmt.Errorf("failed to convert SecureValue to secureValueDB: %w", err) } timestamp := now.UTC().Unix() - row.GUID = uuid.New().String() row.Created = timestamp row.CreatedBy = actorUID @@ -140,7 +139,7 @@ func toCreateRow(now time.Time, sv *secretv1beta1.SecureValue, actorUID string) } // toRow maps a Kubernetes resource into a DB row. -func toRow(sv *secretv1beta1.SecureValue, externalID string) (*secureValueDB, error) { +func toRow(keeper string, sv *secretv1beta1.SecureValue, externalID string) (*secureValueDB, error) { var annotations string if len(sv.Annotations) > 0 { cleanedAnnotations := xkube.CleanAnnotations(sv.Annotations) @@ -237,7 +236,7 @@ func toRow(sv *secretv1beta1.SecureValue, externalID string) (*secureValueDB, er Version: sv.Status.Version, Description: sv.Spec.Description, - Keeper: toNullString(sv.Spec.Keeper), + Keeper: toNullString(&keeper), Decrypters: toNullString(decrypters), Ref: toNullString(sv.Spec.Ref), ExternalID: externalID, diff --git a/pkg/storage/secret/metadata/secure_value_store.go b/pkg/storage/secret/metadata/secure_value_store.go index eec70a40991..4463de00c9a 100644 --- a/pkg/storage/secret/metadata/secure_value_store.go +++ b/pkg/storage/secret/metadata/secure_value_store.go @@ -47,13 +47,14 @@ type secureValueMetadataStorage struct { tracer trace.Tracer } -func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (_ *secretv1beta1.SecureValue, svmCreateErr error) { +func (s *secureValueMetadataStorage) Create(ctx context.Context, keeper string, sv *secretv1beta1.SecureValue, actorUID string) (_ *secretv1beta1.SecureValue, svmCreateErr error) { start := s.clock.Now() name := sv.GetName() namespace := sv.GetNamespace() ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Create", trace.WithAttributes( attribute.String("name", name), attribute.String("namespace", namespace), + attribute.String("keeper", keeper), attribute.String("actorUID", actorUID), )) defer span.End() @@ -64,6 +65,7 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv1bet args := []any{ "name", name, "namespace", namespace, + "keeper", keeper, "actorUID", actorUID, } @@ -83,33 +85,6 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv1bet var row *secureValueDB err := s.db.Transaction(ctx, func(ctx context.Context) error { - if sv.Spec.Keeper != nil { - // Validate before inserting that the chosen `keeper` exists. - - // -- This is a copy of KeeperMetadataStore.read, which is not public at the moment, and is not defined in contract.KeeperMetadataStorage - req := &readKeeper{ - SQLTemplate: sqltemplate.New(s.dialect), - Namespace: sv.Namespace, - Name: *sv.Spec.Keeper, - IsForUpdate: true, - } - - query, err := sqltemplate.Execute(sqlKeeperRead, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlKeeperRead.Name(), err) - } - - res, err := s.db.QueryContext(ctx, query, req.GetArgs()...) - if err != nil { - return fmt.Errorf("getting row: %w", err) - } - defer func() { _ = res.Close() }() - - if !res.Next() { - return contracts.ErrKeeperNotFound - } - } - latestVersion, err := s.getLatestVersion(ctx, xkube.Namespace(sv.Namespace), sv.Name) if err != nil { return fmt.Errorf("fetching latest secure value version: %w", err) @@ -127,7 +102,7 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv1bet for { sv.Status.Version = version - row, err = toCreateRow(s.clock.Now(), sv, actorUID) + row, err = toCreateRow(s.clock.Now(), keeper, sv, actorUID) if err != nil { return fmt.Errorf("to create row: %w", err) } diff --git a/pkg/storage/secret/metadata/secure_value_store_test.go b/pkg/storage/secret/metadata/secure_value_store_test.go index 8b7fbc837fc..f1aeeaf9028 100644 --- a/pkg/storage/secret/metadata/secure_value_store_test.go +++ b/pkg/storage/secret/metadata/secure_value_store_test.go @@ -63,20 +63,20 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { Spec: secretv1beta1.SecureValueSpec{ Description: "test description", Value: ptr.To(secretv1beta1.NewExposedSecureValue("test-value")), - Keeper: &keeperName, }, + Status: secretv1beta1.SecureValueStatus{Keeper: keeperName}, } testSecureValue.Name = "sv-test" testSecureValue.Namespace = "default" // Create the secure value - createdSecureValue, err := secureValueStorage.Create(ctx, testSecureValue, "testuser") + createdSecureValue, err := secureValueStorage.Create(ctx, keeperName, testSecureValue, "testuser") require.NoError(t, err) require.NotNil(t, createdSecureValue) require.Equal(t, "sv-test", createdSecureValue.Name) require.Equal(t, "default", createdSecureValue.Namespace) require.Equal(t, "test description", createdSecureValue.Spec.Description) - require.Equal(t, keeperName, *createdSecureValue.Spec.Keeper) + require.Equal(t, keeperName, createdSecureValue.Status.Keeper) require.NoError(t, secureValueStorage.SetVersionToActive(ctx, xkube.Namespace(createdSecureValue.Namespace), createdSecureValue.Name, createdSecureValue.Status.Version)) @@ -87,7 +87,7 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { require.Equal(t, "sv-test", readSecureValue.Name) require.Equal(t, "default", readSecureValue.Namespace) require.Equal(t, "test description", readSecureValue.Spec.Description) - require.Equal(t, keeperName, *readSecureValue.Spec.Keeper) + require.Equal(t, keeperName, readSecureValue.Status.Keeper) // List secure values and verify our value is in the list secureValues, err := secureValueStorage.List(ctx, xkube.Namespace("default")) @@ -101,7 +101,7 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { found = true require.Equal(t, "default", sv.Namespace) require.Equal(t, "test description", sv.Spec.Description) - require.Equal(t, keeperName, *sv.Spec.Keeper) + require.Equal(t, keeperName, sv.Status.Keeper) break } } @@ -117,14 +117,14 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { Spec: secretv1beta1.SecureValueSpec{ Description: "test description 2", Value: ptr.To(secretv1beta1.NewExposedSecureValue("test-value-2")), - Keeper: &keeperName, }, + Status: secretv1beta1.SecureValueStatus{Keeper: keeperName}, } testSecureValue.Name = "sv-test-2" testSecureValue.Namespace = "default" // Create the secure value - createdSecureValue, err := secureValueStorage.Create(ctx, testSecureValue, "testuser") + createdSecureValue, err := secureValueStorage.Create(ctx, keeperName, testSecureValue, "testuser") require.NoError(t, err) require.NotNil(t, createdSecureValue) @@ -211,7 +211,7 @@ func TestPropertySecureValueMetadataStorage(t *testing.T) { }, "delete": func(t *rapid.T) { ns := namespaceGen.Draw(t, "ns") - name := nameGen.Draw(t, "name") + name := secureValueNameGen.Draw(t, "name") modelSv, modelErr := model.delete(ns, name) sv, err := sut.DeleteSv(t.Context(), ns, name) if err != nil || modelErr != nil { diff --git a/pkg/storage/secret/metadata/secure_value_test.go b/pkg/storage/secret/metadata/secure_value_test.go index 3225117a3c7..8d7f318a9b5 100644 --- a/pkg/storage/secret/metadata/secure_value_test.go +++ b/pkg/storage/secret/metadata/secure_value_test.go @@ -26,9 +26,16 @@ type modelSecureValue struct { leaseCreated time.Time } +type modelKeeper struct { + namespace string + name string + active bool +} + // A simplified model of the grafana secrets manager type model struct { secureValues []*modelSecureValue + keepers []*modelKeeper } func newModel() *model { @@ -73,24 +80,91 @@ func (m *model) readActiveVersion(namespace, name string) *modelSecureValue { } func (m *model) create(now time.Time, sv *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, error) { + keeper := m.getActiveKeeper(sv.Namespace) + sv = deepCopy(sv) modelSv := &modelSecureValue{SecureValue: sv, active: false, created: now} modelSv.Status.Version = m.getNewVersionNumber(modelSv.Namespace, modelSv.Name) modelSv.Status.ExternalID = fmt.Sprintf("%d", modelSv.Status.Version) + modelSv.Status.Keeper = keeper.name m.secureValues = append(m.secureValues, modelSv) m.setVersionToActive(modelSv.Namespace, modelSv.Name, modelSv.Status.Version) return modelSv.SecureValue, nil } +func (m *model) getActiveKeeper(namespace string) *modelKeeper { + for _, k := range m.keepers { + if k.namespace == namespace && k.active { + return k + } + } + + // Default to the system keeper when there are no active keepers in the namespace + return &modelKeeper{namespace: namespace, name: contracts.SystemKeeperName, active: true} +} + +func (m *model) keeperExists(namespace, name string) bool { + return m.findKeeper(namespace, name) != nil +} + +func (m *model) findKeeper(namespace, name string) *modelKeeper { + // The system keeper is not in the list of keepers + if name == contracts.SystemKeeperName { + return &modelKeeper{namespace: namespace, name: contracts.SystemKeeperName, active: true} + } + for _, k := range m.keepers { + if k.namespace == namespace && k.name == name { + return k + } + } + return nil +} + +func (m *model) createKeeper(keeper *secretv1beta1.Keeper) (*secretv1beta1.Keeper, error) { + if m.keeperExists(keeper.Namespace, keeper.Name) { + return nil, contracts.ErrKeeperAlreadyExists + } + + m.keepers = append(m.keepers, &modelKeeper{namespace: keeper.Namespace, name: keeper.Name}) + + return deepCopy(keeper), nil +} + +func (m *model) setKeeperAsActive(namespace, keeperName string) error { + keeper := m.findKeeper(namespace, keeperName) + if keeper == nil { + return contracts.ErrKeeperNotFound + } + // Set the keeper as active + keeper.active = true + + // Set every other keeper in the namespace as inactive + for _, k := range m.keepers { + if k.namespace == namespace && k.name != keeperName { + k.active = false + } + } + + return nil +} + func (m *model) update(now time.Time, newSecureValue *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, bool, error) { + sv := m.readActiveVersion(newSecureValue.Namespace, newSecureValue.Name) + if sv == nil { + return nil, false, contracts.ErrSecureValueNotFound + } + + // If the keeper doesn't exist, return an error + if !m.keeperExists(sv.Namespace, sv.Status.Keeper) { + return nil, false, contracts.ErrKeeperNotFound + } + // If the payload doesn't contain a value, get the value from current version if newSecureValue.Spec.Value == nil { - sv := m.readActiveVersion(newSecureValue.Namespace, newSecureValue.Name) - if sv == nil { - return nil, false, contracts.ErrSecureValueNotFound - } newSecureValue.Spec.Value = sv.Spec.Value } + createdSv, err := m.create(now, newSecureValue) + return createdSv, true, err } @@ -161,13 +235,14 @@ func (m *model) leaseInactiveSecureValues(now time.Time, minAge, leaseTTL time.D } var ( - decryptersGen = rapid.SampledFrom([]string{"svc1", "svc2", "svc3", "svc4", "svc5"}) - nameGen = rapid.SampledFrom([]string{"n1", "n2", "n3", "n4", "n5"}) - namespaceGen = rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"}) - anySecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue { + decryptersGen = rapid.SampledFrom([]string{"svc1", "svc2", "svc3", "svc4", "svc5"}) + secureValueNameGen = rapid.SampledFrom([]string{"n1", "n2", "n3", "n4", "n5"}) + keeperNameGen = rapid.SampledFrom([]string{"k1", "k2", "k3", "k4", "k5"}) + namespaceGen = rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"}) + anySecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue { return &secretv1beta1.SecureValue{ ObjectMeta: metav1.ObjectMeta{ - Name: nameGen.Draw(t, "name"), + Name: secureValueNameGen.Draw(t, "name"), Namespace: namespaceGen.Draw(t, "ns"), }, Spec: secretv1beta1.SecureValueSpec{ @@ -191,10 +266,37 @@ var ( decryptGen = rapid.Custom(func(t *rapid.T) decryptInput { return decryptInput{ namespace: namespaceGen.Draw(t, "ns"), - name: nameGen.Draw(t, "name"), + name: secureValueNameGen.Draw(t, "name"), decrypter: decryptersGen.Draw(t, "decrypter"), } }) + anyKeeperGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.Keeper { + spec := secretv1beta1.KeeperSpec{ + Description: rapid.String().Draw(t, "description"), + } + + keeperType := rapid.SampledFrom([]string{"isAwsKeeper", "isAzureKeeper", "isGcpKeeper", "isVaultKeeper"}).Draw(t, "keeperType") + switch keeperType { + case "isAwsKeeper": + spec.Aws = &secretv1beta1.KeeperAWSConfig{} + case "isAzureKeeper": + spec.Azure = &secretv1beta1.KeeperAzureConfig{} + case "isGcpKeeper": + spec.Gcp = &secretv1beta1.KeeperGCPConfig{} + case "isVaultKeeper": + spec.HashiCorpVault = &secretv1beta1.KeeperHashiCorpConfig{} + default: + panic(fmt.Sprintf("unhandled keeper type '%+v', did you forget a switch case?", keeperType)) + } + + return &secretv1beta1.Keeper{ + ObjectMeta: metav1.ObjectMeta{ + Name: keeperNameGen.Draw(t, "name"), + Namespace: namespaceGen.Draw(t, "ns"), + }, + Spec: spec, + } + }) ) type decryptInput struct { @@ -268,9 +370,8 @@ func TestModel(t *testing.T) { sv4 := deepCopy(sv3) sv4.Name = "i_dont_exist" sv4.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue("sv4")) - sv4, _, err = m.update(now, sv4) - require.NoError(t, err) - require.EqualValues(t, 1, sv4.Status.Version) + _, _, err = m.update(now, sv4) + require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) }) t.Run("deleting a secure value", func(t *testing.T) { @@ -359,7 +460,6 @@ func TestStateMachine(t *testing.T) { sv := anySecureValueGen.Draw(t, "sv") modelCreatedSv, modelErr := model.create(sut.Clock.Now(), deepCopy(sv)) - createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(deepCopy(sv))) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) @@ -412,7 +512,8 @@ func TestStateMachine(t *testing.T) { if !slices.ContainsFunc(list.Items, func(v2 secretv1beta1.SecureValue) bool { return v2.Namespace == v1.Namespace && v2.Name == v1.Name && v2.Status.Version == v1.Status.Version }) { - t.Fatalf("expected sut to return secure value ns=%+v name=%+v version=%+v in the result", v1.Namespace, v1.Name, v1.Status.Version) + t.Fatalf("expected sut to return secure value ns=%+v name=%+v version=%+v in the result", + v1.Namespace, v1.Name, v1.Status.Version) } } }, @@ -439,8 +540,28 @@ func TestStateMachine(t *testing.T) { require.Equal(t, len(modelResult), len(result)) for name := range modelResult { - require.Equal(t, modelResult[name].Value(), result[name].Value()) require.Equal(t, modelResult[name].Error(), result[name].Error()) + require.Equal(t, modelResult[name].Value(), result[name].Value()) + } + }, + "createKeeper": func(t *rapid.T) { + input := anyKeeperGen.Draw(t, "keeper") + modelKeeper, modelErr := model.createKeeper(input) + keeper, err := sut.KeeperMetadataStorage.Create(t.Context(), input, "actor-uid") + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelKeeper.Name, keeper.Name) + }, + "setKeeperAsActive": func(t *rapid.T) { + namespace := namespaceGen.Draw(t, "namespace") + keeper := keeperNameGen.Draw(t, "keeper") + modelErr := model.setKeeperAsActive(namespace, keeper) + err := sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(namespace), keeper) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return } }, }) @@ -471,6 +592,69 @@ func TestSecureValueServiceExampleBased(t *testing.T) { require.Equal(t, 1, len(result)) require.ErrorIs(t, result[sv.Name].Error(), contracts.ErrDecryptNotFound) }) + + t.Run("should be able to use secrets that were created with a keeper that's inactive", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + + // - Create a secret with k1 + k1, err := sut.KeeperMetadataStorage.Create(t.Context(), &secretv1beta1.Keeper{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "n1", + Name: "k1", + }, + Spec: secretv1beta1.KeeperSpec{ + Description: "description", + Aws: &secretv1beta1.KeeperAWSConfig{}, + }, + }, "actor-uid") + require.NoError(t, err) + + require.NoError(t, sut.SecureValueService.SetKeeperAsActive(t.Context(), xkube.Namespace(k1.Namespace), k1.Name)) + + value := secretv1beta1.NewExposedSecureValue("v1") + sv1, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(&secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{Namespace: k1.Namespace, Name: "s1"}, + Spec: secretv1beta1.SecureValueSpec{ + Description: "desc", + Value: &value, + }, + })) + require.NoError(t, err) + require.Equal(t, k1.Name, sv1.Status.Keeper) + + // - Set a new keeper as active + k2, err := sut.KeeperMetadataStorage.Create(t.Context(), &secretv1beta1.Keeper{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "n1", + Name: "k2", + }, + Spec: secretv1beta1.KeeperSpec{ + Description: "description", + Aws: &secretv1beta1.KeeperAWSConfig{}, + }, + }, "actor-uid") + require.NoError(t, err) + require.NoError(t, sut.SecureValueService.SetKeeperAsActive(t.Context(), xkube.Namespace(k2.Namespace), k2.Name)) + + // - Read secure value created with inactive keeper + readSv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv1.Namespace), sv1.Name) + require.NoError(t, err) + require.Equal(t, sv1.Namespace, readSv.Namespace) + require.Equal(t, sv1.Name, readSv.Name) + require.Equal(t, k1.Name, readSv.Status.Keeper) + + // - Update secure value created with inactive keeper + newSv1 := sv1.DeepCopy() + newSv1.Spec.Description = "updated desc" + updatedSv, _, err := sut.SecureValueService.Update(t.Context(), newSv1, "actor-uid") + require.NoError(t, err) + require.Equal(t, sv1.Namespace, updatedSv.Namespace) + require.Equal(t, sv1.Name, updatedSv.Name) + require.Equal(t, k1.Name, updatedSv.Status.Keeper) + require.Equal(t, newSv1.Spec.Description, updatedSv.Spec.Description) + }) } func deepCopy[T any](sv T) T { diff --git a/pkg/storage/secret/metadata/testdata/mysql--keeper_read_active-read active.sql b/pkg/storage/secret/metadata/testdata/mysql--keeper_read_active-read active.sql new file mode 100755 index 00000000000..3225cb1a9fe --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--keeper_read_active-read active.sql @@ -0,0 +1,20 @@ +SELECT + `guid`, + `name`, + `namespace`, + `annotations`, + `labels`, + `created`, + `created_by`, + `updated`, + `updated_by`, + `description`, + `type`, + `payload` +FROM + `secret_keeper` +WHERE + `namespace` = 'ns' AND + `active` = true +LIMIT 1 +; diff --git a/pkg/storage/secret/metadata/testdata/mysql--keeper_set_as_active-keeper set as active.sql b/pkg/storage/secret/metadata/testdata/mysql--keeper_set_as_active-keeper set as active.sql new file mode 100755 index 00000000000..48d9b82f3fc --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--keeper_set_as_active-keeper set as active.sql @@ -0,0 +1,5 @@ +UPDATE `secret_keeper` +SET `active` = (`name` = 'name') +WHERE + `namespace` = 'ns' +; diff --git a/pkg/storage/secret/metadata/testdata/postgres--keeper_read_active-read active.sql b/pkg/storage/secret/metadata/testdata/postgres--keeper_read_active-read active.sql new file mode 100755 index 00000000000..cd17093b053 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--keeper_read_active-read active.sql @@ -0,0 +1,20 @@ +SELECT + "guid", + "name", + "namespace", + "annotations", + "labels", + "created", + "created_by", + "updated", + "updated_by", + "description", + "type", + "payload" +FROM + "secret_keeper" +WHERE + "namespace" = 'ns' AND + "active" = true +LIMIT 1 +; diff --git a/pkg/storage/secret/metadata/testdata/postgres--keeper_set_as_active-keeper set as active.sql b/pkg/storage/secret/metadata/testdata/postgres--keeper_set_as_active-keeper set as active.sql new file mode 100755 index 00000000000..f88a830d557 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--keeper_set_as_active-keeper set as active.sql @@ -0,0 +1,5 @@ +UPDATE "secret_keeper" +SET "active" = ("name" = 'name') +WHERE + "namespace" = 'ns' +; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--keeper_read_active-read active.sql b/pkg/storage/secret/metadata/testdata/sqlite--keeper_read_active-read active.sql new file mode 100755 index 00000000000..cd17093b053 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--keeper_read_active-read active.sql @@ -0,0 +1,20 @@ +SELECT + "guid", + "name", + "namespace", + "annotations", + "labels", + "created", + "created_by", + "updated", + "updated_by", + "description", + "type", + "payload" +FROM + "secret_keeper" +WHERE + "namespace" = 'ns' AND + "active" = true +LIMIT 1 +; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--keeper_set_as_active-keeper set as active.sql b/pkg/storage/secret/metadata/testdata/sqlite--keeper_set_as_active-keeper set as active.sql new file mode 100755 index 00000000000..f88a830d557 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--keeper_set_as_active-keeper set as active.sql @@ -0,0 +1,5 @@ +UPDATE "secret_keeper" +SET "active" = ("name" = 'name') +WHERE + "namespace" = 'ns' +; diff --git a/pkg/storage/secret/migrator/migrator.go b/pkg/storage/secret/migrator/migrator.go index b08fb766e96..215020561a2 100644 --- a/pkg/storage/secret/migrator/migrator.go +++ b/pkg/storage/secret/migrator/migrator.go @@ -77,7 +77,7 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) { } tables = append(tables, secureValueTable) - tables = append(tables, migrator.Table{ + keeperTable := migrator.Table{ Name: TableNameKeeper, Columns: []*migrator.Column{ // Kubernetes Metadata @@ -100,7 +100,8 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) { Indices: []*migrator.Index{ {Cols: []string{"namespace", "name"}, Type: migrator.UniqueIndex}, }, - }) + } + tables = append(tables, keeperTable) dataKeyTable := migrator.Table{ Name: TableNameDataKey, @@ -211,4 +212,16 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) { mg.AddMigration("add data_key_id index to "+TableNameEncryptedValue, migrator.NewAddIndexMigration(encryptedValueTable, &migrator.Index{ Cols: []string{"data_key_id"}, })) + mg.AddMigration("add active column to "+TableNameKeeper, migrator.NewAddColumnMigration(keeperTable, &migrator.Column{ + Name: "active", + Type: migrator.DB_Bool, + Nullable: false, + Default: "false", + })) + mg.AddMigration("add active column index to "+TableNameKeeper, migrator.NewAddIndexMigration(keeperTable, &migrator.Index{ + Cols: []string{"namespace", "name", "active"}, + })) + mg.AddMigration("set secret_secure_value.keeper to 'system' where keeper is null in "+TableNameSecureValue, migrator.NewRawSQLMigration( + fmt.Sprintf("UPDATE %s SET keeper = '%s' WHERE keeper IS NULL", TableNameSecureValue, contracts.SystemKeeperName), + )) } From 5e949fc95526a86155f8d94465a22dd111670565 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Fri, 21 Nov 2025 16:31:17 +0100 Subject: [PATCH 057/378] Secrets: Fix secure value creation timestamp changing when updating it (#114290) --- go.mod | 2 +- .../garbagecollectionworker/worker_test.go | 26 +++++---- ...lue_get_latest_version_and_created_at.sql} | 4 +- pkg/storage/secret/metadata/query.go | 10 ++-- pkg/storage/secret/metadata/query_test.go | 4 +- .../secret/metadata/secure_value_model.go | 7 +-- .../secret/metadata/secure_value_store.go | 56 +++++++++++++------ .../metadata/secure_value_store_test.go | 4 +- .../secret/metadata/secure_value_test.go | 51 +++++++++-------- ...ed_at-get latest secure value version.sql} | 4 +- ...ed_at-get latest secure value version.sql} | 4 +- ...ed_at-get latest secure value version.sql} | 4 +- 12 files changed, 104 insertions(+), 72 deletions(-) rename pkg/storage/secret/metadata/data/{secure_value_get_latest_version.sql => secure_value_get_latest_version_and_created_at.sql} (72%) rename pkg/storage/secret/metadata/testdata/{mysql--secure_value_get_latest_version-get latest secure value version.sql => mysql--secure_value_get_latest_version_and_created_at-get latest secure value version.sql} (76%) rename pkg/storage/secret/metadata/testdata/{postgres--secure_value_get_latest_version-get latest secure value version.sql => postgres--secure_value_get_latest_version_and_created_at-get latest secure value version.sql} (76%) rename pkg/storage/secret/metadata/testdata/{sqlite--secure_value_get_latest_version-get latest secure value version.sql => sqlite--secure_value_get_latest_version_and_created_at-get latest secure value version.sql} (76%) diff --git a/go.mod b/go.mod index eb9780e649f..daab4218c91 100644 --- a/go.mod +++ b/go.mod @@ -139,7 +139,6 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.4 // @grafana/alerting-backend github.com/microsoft/go-mssqldb v1.9.2 // @grafana/partner-datasources github.com/migueleliasweb/go-github-mock v1.1.0 // @grafana/grafana-git-ui-sync-team - github.com/mitchellh/copystructure v1.2.0 // @grafana/grafana-operator-experience-squad github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c //@grafana/identity-access-team github.com/mocktools/go-smtp-mock/v2 v2.5.1 // @grafana/grafana-backend-group github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // @grafana/alerting-backend @@ -515,6 +514,7 @@ require ( github.com/miekg/dns v1.1.63 // indirect github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go b/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go index 0b5a7614205..9b27aceefc1 100644 --- a/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go +++ b/pkg/registry/apis/secret/garbagecollectionworker/worker_test.go @@ -1,7 +1,6 @@ package garbagecollectionworker_test import ( - "fmt" "slices" "testing" "time" @@ -11,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/secret/encryption" - "github.com/mitchellh/copystructure" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -133,7 +131,7 @@ func TestProperty(t *testing.T) { t.Repeat(map[string]func(*rapid.T){ "create": func(t *rapid.T) { sv := anySecureValueGen.Draw(t, "sv") - svCopy := deepCopy(sv) + svCopy := sv.DeepCopy() createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv)) svCopy.UID = createdSv.UID @@ -194,13 +192,15 @@ func newModel() *model { } func (m *model) create(now time.Time, sv *secretv1beta1.SecureValue) error { + created := now for _, item := range m.items { if item.active && item.Namespace == sv.Namespace && item.Name == sv.Name { item.active = false + created = item.created break } } - m.items = append(m.items, &modelSecureValue{SecureValue: sv, active: true, created: now}) + m.items = append(m.items, &modelSecureValue{SecureValue: sv, active: true, created: created}) return nil } @@ -219,6 +219,16 @@ func (m *model) cleanupInactiveSecureValues(now time.Time, minAge time.Duration, // Using a slice to allow duplicates toDelete := make([]*modelSecureValue, 0) + // The implementation query sorts by created time ascending + slices.SortFunc(m.items, func(a, b *modelSecureValue) int { + if a.created.Before(b.created) { + return -1 + } else if a.created.After(b.created) { + return 1 + } + return 0 + }) + for _, sv := range m.items { if len(toDelete) >= int(maxBatchSize) { break @@ -238,11 +248,3 @@ func (m *model) cleanupInactiveSecureValues(now time.Time, minAge time.Duration, return toDelete, nil } - -func deepCopy[T any](sv T) T { - copied, err := copystructure.Copy(sv) - if err != nil { - panic(fmt.Sprintf("failed to copy secure value: %v", err)) - } - return copied.(T) -} diff --git a/pkg/storage/secret/metadata/data/secure_value_get_latest_version.sql b/pkg/storage/secret/metadata/data/secure_value_get_latest_version_and_created_at.sql similarity index 72% rename from pkg/storage/secret/metadata/data/secure_value_get_latest_version.sql rename to pkg/storage/secret/metadata/data/secure_value_get_latest_version_and_created_at.sql index 90319d469eb..1c804c2d031 100644 --- a/pkg/storage/secret/metadata/data/secure_value_get_latest_version.sql +++ b/pkg/storage/secret/metadata/data/secure_value_get_latest_version_and_created_at.sql @@ -1,5 +1,7 @@ SELECT - {{ .Ident "version" }} + {{ .Ident "created" }}, + {{ .Ident "version" }}, + {{ .Ident "active" }} FROM {{ .Ident "secret_secure_value" }} WHERE diff --git a/pkg/storage/secret/metadata/query.go b/pkg/storage/secret/metadata/query.go index 8e35099a250..e8f84855e7f 100644 --- a/pkg/storage/secret/metadata/query.go +++ b/pkg/storage/secret/metadata/query.go @@ -34,9 +34,9 @@ var ( sqlSecureValueLeaseInactive = mustTemplate("secure_value_lease_inactive.sql") sqlSecureValueListByLeaseToken = mustTemplate("secure_value_list_by_lease_token.sql") - sqlGetLatestSecureValueVersion = mustTemplate("secure_value_get_latest_version.sql") - sqlSecureValueSetVersionToActive = mustTemplate("secure_value_set_version_to_active.sql") - sqlSecureValueSetVersionToInactive = mustTemplate("secure_value_set_version_to_inactive.sql") + sqlGetLatestSecureValueVersionAndCreatedAt = mustTemplate("secure_value_get_latest_version_and_created_at.sql") + sqlSecureValueSetVersionToActive = mustTemplate("secure_value_set_version_to_active.sql") + sqlSecureValueSetVersionToInactive = mustTemplate("secure_value_set_version_to_inactive.sql") ) func mustTemplate(filename string) *template.Template { @@ -171,13 +171,13 @@ func (r readSecureValue) Validate() error { return nil // TODO } -type getLatestSecureValueVersion struct { +type getLatestSecureValueVersionAndCreatedAt struct { sqltemplate.SQLTemplate Namespace string Name string } -func (r getLatestSecureValueVersion) Validate() error { +func (r getLatestSecureValueVersionAndCreatedAt) Validate() error { return nil } diff --git a/pkg/storage/secret/metadata/query_test.go b/pkg/storage/secret/metadata/query_test.go index 75cd970d382..108b9a6a1cd 100644 --- a/pkg/storage/secret/metadata/query_test.go +++ b/pkg/storage/secret/metadata/query_test.go @@ -141,10 +141,10 @@ func TestSecureValueQueries(t *testing.T) { mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ RootDir: "testdata", Templates: map[*template.Template][]mocks.TemplateTestCase{ - sqlGetLatestSecureValueVersion: { + sqlGetLatestSecureValueVersionAndCreatedAt: { { Name: "get latest secure value version", - Data: &getLatestSecureValueVersion{ + Data: &getLatestSecureValueVersionAndCreatedAt{ SQLTemplate: mocks.NewTestingSQLTemplate(), Name: "name", Namespace: "ns", diff --git a/pkg/storage/secret/metadata/secure_value_model.go b/pkg/storage/secret/metadata/secure_value_model.go index f514fa5f2d9..81ed2eb2849 100644 --- a/pkg/storage/secret/metadata/secure_value_model.go +++ b/pkg/storage/secret/metadata/secure_value_model.go @@ -122,17 +122,16 @@ func (sv *secureValueDB) toKubernetes() (*secretv1beta1.SecureValue, error) { } // toCreateRow maps a Kubernetes resource into a DB row for new resources being created/inserted. -func toCreateRow(now time.Time, keeper string, sv *secretv1beta1.SecureValue, actorUID string) (*secureValueDB, error) { +func toCreateRow(createdAt, updatedAt int64, keeper string, sv *secretv1beta1.SecureValue, actorUID string) (*secureValueDB, error) { row, err := toRow(keeper, sv, "") if err != nil { return nil, fmt.Errorf("failed to convert SecureValue to secureValueDB: %w", err) } - timestamp := now.UTC().Unix() row.GUID = uuid.New().String() - row.Created = timestamp + row.Created = createdAt row.CreatedBy = actorUID - row.Updated = timestamp + row.Updated = updatedAt row.UpdatedBy = actorUID return row, nil diff --git a/pkg/storage/secret/metadata/secure_value_store.go b/pkg/storage/secret/metadata/secure_value_store.go index 4463de00c9a..d3a137ea611 100644 --- a/pkg/storage/secret/metadata/secure_value_store.go +++ b/pkg/storage/secret/metadata/secure_value_store.go @@ -85,14 +85,14 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, keeper string, var row *secureValueDB err := s.db.Transaction(ctx, func(ctx context.Context) error { - latestVersion, err := s.getLatestVersion(ctx, xkube.Namespace(sv.Namespace), sv.Name) + latest, err := s.getLatestVersionAndCreatedAt(ctx, xkube.Namespace(sv.Namespace), sv.Name) if err != nil { return fmt.Errorf("fetching latest secure value version: %w", err) } version := int64(1) - if latestVersion != nil { - version = *latestVersion + 1 + if latest.version > 0 { + version = latest.version + 1 } // Some other concurrent request may have created the version we're trying to create, @@ -102,7 +102,15 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, keeper string, for { sv.Status.Version = version - row, err = toCreateRow(s.clock.Now(), keeper, sv, actorUID) + now := s.clock.Now().UTC().Unix() + + createdAt := now + if latest.createdAt > 0 { + createdAt = latest.createdAt + } + updatedAt := now + + row, err = toCreateRow(createdAt, updatedAt, keeper, sv, actorUID) if err != nil { return fmt.Errorf("to create row: %w", err) } @@ -153,44 +161,60 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, keeper string, return createdSecureValue, nil } -func (s *secureValueMetadataStorage) getLatestVersion(ctx context.Context, namespace xkube.Namespace, name string) (*int64, error) { - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.getLatestVersion", trace.WithAttributes( +type versionAndCreatedAt struct { + createdAt int64 + version int64 +} + +func (s *secureValueMetadataStorage) getLatestVersionAndCreatedAt(ctx context.Context, namespace xkube.Namespace, name string) (versionAndCreatedAt, error) { + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.getLatestVersionAndCreatedAt", trace.WithAttributes( attribute.String("name", name), attribute.String("namespace", namespace.String()), )) defer span.End() - req := getLatestSecureValueVersion{ + req := getLatestSecureValueVersionAndCreatedAt{ SQLTemplate: sqltemplate.New(s.dialect), Namespace: namespace.String(), Name: name, } - q, err := sqltemplate.Execute(sqlGetLatestSecureValueVersion, req) + q, err := sqltemplate.Execute(sqlGetLatestSecureValueVersionAndCreatedAt, req) if err != nil { - return nil, fmt.Errorf("execute template %q: %w", sqlGetLatestSecureValueVersion.Name(), err) + return versionAndCreatedAt{}, fmt.Errorf("execute template %q: %w", sqlGetLatestSecureValueVersionAndCreatedAt.Name(), err) } rows, err := s.db.QueryContext(ctx, q, req.GetArgs()...) if err != nil { - return nil, fmt.Errorf("fetching latest version for secure value: namespace=%+v name=%+v %w", namespace, name, err) + return versionAndCreatedAt{}, fmt.Errorf("fetching latest version for secure value: namespace=%+v name=%+v %w", namespace, name, err) } defer func() { _ = rows.Close() }() if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error executing query: %w", err) + return versionAndCreatedAt{}, fmt.Errorf("error executing query: %w", err) } if !rows.Next() { - return nil, nil + return versionAndCreatedAt{}, nil } - var version int64 - if err := rows.Scan(&version); err != nil { - return nil, fmt.Errorf("scanning version from returned rows: %w", err) + var ( + createdAt int64 + version int64 + active bool + ) + if err := rows.Scan(&createdAt, &version, &active); err != nil { + return versionAndCreatedAt{}, fmt.Errorf("scanning version from returned rows: %w", err) } - return &version, nil + if !active { + createdAt = 0 + } + + return versionAndCreatedAt{ + createdAt: createdAt, + version: version, + }, nil } func (s *secureValueMetadataStorage) readActiveVersion(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (secureValueDB, error) { diff --git a/pkg/storage/secret/metadata/secure_value_store_test.go b/pkg/storage/secret/metadata/secure_value_store_test.go index f1aeeaf9028..f7551e9e484 100644 --- a/pkg/storage/secret/metadata/secure_value_store_test.go +++ b/pkg/storage/secret/metadata/secure_value_store_test.go @@ -199,8 +199,8 @@ func TestPropertySecureValueMetadataStorage(t *testing.T) { t.Repeat(map[string]func(*rapid.T){ "create": func(t *rapid.T) { sv := anySecureValueGen.Draw(t, "sv") - modelCreatedSv, modelErr := model.create(sut.Clock.Now(), deepCopy(sv)) - createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(deepCopy(sv))) + modelCreatedSv, modelErr := model.create(sut.Clock.Now(), sv.DeepCopy()) + createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv.DeepCopy())) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) return diff --git a/pkg/storage/secret/metadata/secure_value_test.go b/pkg/storage/secret/metadata/secure_value_test.go index 8d7f318a9b5..0eba9bbf9ce 100644 --- a/pkg/storage/secret/metadata/secure_value_test.go +++ b/pkg/storage/secret/metadata/secure_value_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - "github.com/mitchellh/copystructure" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -81,8 +80,15 @@ func (m *model) readActiveVersion(namespace, name string) *modelSecureValue { func (m *model) create(now time.Time, sv *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, error) { keeper := m.getActiveKeeper(sv.Namespace) - sv = deepCopy(sv) - modelSv := &modelSecureValue{SecureValue: sv, active: false, created: now} + sv = sv.DeepCopy() + + // Preserve the original creation time if this secure value already exists + created := now + if sv := m.readActiveVersion(sv.Namespace, sv.Name); sv != nil { + created = sv.created + } + + modelSv := &modelSecureValue{SecureValue: sv, active: false, created: created} modelSv.Status.Version = m.getNewVersionNumber(modelSv.Namespace, modelSv.Name) modelSv.Status.ExternalID = fmt.Sprintf("%d", modelSv.Status.Version) modelSv.Status.Keeper = keeper.name @@ -126,7 +132,7 @@ func (m *model) createKeeper(keeper *secretv1beta1.Keeper) (*secretv1beta1.Keepe m.keepers = append(m.keepers, &modelKeeper{namespace: keeper.Namespace, name: keeper.Name}) - return deepCopy(keeper), nil + return keeper.DeepCopy(), nil } func (m *model) setKeeperAsActive(namespace, keeperName string) error { @@ -196,7 +202,7 @@ func (m *model) decrypt(decrypter, namespace, name string) (map[string]decrypt.D v.active { if slices.ContainsFunc(v.Spec.Decrypters, func(d string) bool { return d == decrypter }) { return map[string]decrypt.DecryptResult{ - name: decrypt.NewDecryptResultValue(deepCopy(v).Spec.Value), + name: decrypt.NewDecryptResultValue(v.DeepCopy().Spec.Value), }, nil } @@ -328,14 +334,14 @@ func TestModel(t *testing.T) { now := time.Now() // Create a secure value - sv1, err := m.create(now, deepCopy(sv)) + sv1, err := m.create(now, sv.DeepCopy()) require.NoError(t, err) require.Equal(t, sv.Namespace, sv1.Namespace) require.Equal(t, sv.Name, sv1.Name) require.EqualValues(t, 1, sv1.Status.Version) // Create a new version of a secure value - sv2, err := m.create(now, deepCopy(sv)) + sv2, err := m.create(now, sv.DeepCopy()) require.NoError(t, err) require.Equal(t, sv.Namespace, sv2.Namespace) require.Equal(t, sv.Name, sv2.Name) @@ -349,25 +355,25 @@ func TestModel(t *testing.T) { now := time.Now() - sv1, err := m.create(now, deepCopy(sv)) + sv1, err := m.create(now, sv.DeepCopy()) require.NoError(t, err) // Create a new version of a secure value by updating it - sv2, _, err := m.update(now, deepCopy(sv1)) + sv2, _, err := m.update(now, sv1.DeepCopy()) require.NoError(t, err) require.Equal(t, sv.Namespace, sv2.Namespace) require.Equal(t, sv.Name, sv2.Name) require.EqualValues(t, 2, sv2.Status.Version) // Try updating a secure value that doesn't exist without specifying a value for it - sv3 := deepCopy(sv2) + sv3 := sv2.DeepCopy() sv3.Name = "i_dont_exist" sv3.Spec.Value = nil _, _, err = m.update(now, sv3) require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) // Updating a value that doesn't exist creates a new version - sv4 := deepCopy(sv3) + sv4 := sv3.DeepCopy() sv4.Name = "i_dont_exist" sv4.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue("sv4")) _, _, err = m.update(now, sv4) @@ -380,7 +386,7 @@ func TestModel(t *testing.T) { m := newModel() now := time.Now() - sv1, err := m.create(now, deepCopy(sv)) + sv1, err := m.create(now, sv.DeepCopy()) require.NoError(t, err) // Deleting a secure value @@ -407,7 +413,7 @@ func TestModel(t *testing.T) { require.Equal(t, 0, len(list.Items)) // Create a secure value - sv1, err := m.create(now, deepCopy(sv)) + sv1, err := m.create(now, sv.DeepCopy()) require.NoError(t, err) // 1 secure value exists and it should be returned @@ -434,7 +440,7 @@ func TestModel(t *testing.T) { // Create a secure value secret := "v1" - sv1, err := m.create(now, deepCopy(sv)) + sv1, err := m.create(now, sv.DeepCopy()) require.NoError(t, err) // Decrypt the just created secure value @@ -459,8 +465,9 @@ func TestStateMachine(t *testing.T) { "create": func(t *rapid.T) { sv := anySecureValueGen.Draw(t, "sv") - modelCreatedSv, modelErr := model.create(sut.Clock.Now(), deepCopy(sv)) - createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(deepCopy(sv))) + modelCreatedSv, modelErr := model.create(sut.Clock.Now(), sv.DeepCopy()) + + createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv.DeepCopy())) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) return @@ -471,8 +478,8 @@ func TestStateMachine(t *testing.T) { }, "update": func(t *rapid.T) { sv := updateSecureValueGen.Draw(t, "sv") - modelCreatedSv, _, modelErr := model.update(sut.Clock.Now(), deepCopy(sv)) - createdSv, err := sut.UpdateSv(t.Context(), deepCopy(sv)) + modelCreatedSv, _, modelErr := model.update(sut.Clock.Now(), sv.DeepCopy()) + createdSv, err := sut.UpdateSv(t.Context(), sv.DeepCopy()) if err != nil || modelErr != nil { require.ErrorIs(t, err, modelErr) return @@ -656,11 +663,3 @@ func TestSecureValueServiceExampleBased(t *testing.T) { require.Equal(t, newSv1.Spec.Description, updatedSv.Spec.Description) }) } - -func deepCopy[T any](sv T) T { - copied, err := copystructure.Copy(sv) - if err != nil { - panic(fmt.Sprintf("failed to copy secure value: %v", err)) - } - return copied.(T) -} diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version_and_created_at-get latest secure value version.sql similarity index 76% rename from pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version-get latest secure value version.sql rename to pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version_and_created_at-get latest secure value version.sql index 8118fc81489..661c6474221 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version-get latest secure value version.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version_and_created_at-get latest secure value version.sql @@ -1,5 +1,7 @@ SELECT - `version` + `created`, + `version`, + `active` FROM `secret_secure_value` WHERE diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version_and_created_at-get latest secure value version.sql similarity index 76% rename from pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version-get latest secure value version.sql rename to pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version_and_created_at-get latest secure value version.sql index 1549114582d..e1c3834ecbb 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version-get latest secure value version.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version_and_created_at-get latest secure value version.sql @@ -1,5 +1,7 @@ SELECT - "version" + "created", + "version", + "active" FROM "secret_secure_value" WHERE diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version_and_created_at-get latest secure value version.sql similarity index 76% rename from pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version-get latest secure value version.sql rename to pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version_and_created_at-get latest secure value version.sql index 1549114582d..e1c3834ecbb 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version-get latest secure value version.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version_and_created_at-get latest secure value version.sql @@ -1,5 +1,7 @@ SELECT - "version" + "created", + "version", + "active" FROM "secret_secure_value" WHERE From 336aa06966a618e94475f4098b5f50bca81ffb9a Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Fri, 21 Nov 2025 18:03:27 +0200 Subject: [PATCH 058/378] Dashboards: Add non-applicable filter pills per panel (#112507) * wip non-applicable pills * refactor * fix * tests * locales * canary version * mods * fix query fetching * fix * fix * Add VizPanelSubHeader component * lint * extra check * refactor * wip * refactor wipp * refactor * refactor * use canary scenes * prettier * canary scenes * interpolate ds uid for ds var case * refactor * refactor * memoize values + tests + rename component * refactor * refactor * refactor * refactor * canary scenes + fixes * Fix infinite loop casused by unstable useEffect dependency * lint * helper functions * bump scenes * reduce applicability calls to only when filters or groupBy keys change, not any var state change * refactor * fix * fix * fixes --------- Co-authored-by: Dominik Prokop --- package.json | 4 +- packages/grafana-data/src/types/datasource.ts | 2 +- .../src/types/featureToggles.gen.ts | 4 + .../src/selectors/components.ts | 3 + .../components/PanelChrome/PanelChrome.mdx | 64 ++ .../PanelChrome/PanelChrome.test.tsx | 27 + .../components/PanelChrome/PanelChrome.tsx | 84 ++- pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.json | 553 +++++++++--------- .../PanelNonApplicableDrilldownsSubHeader.tsx | 189 ++++++ .../scene/VizPanelSubHeader.test.tsx | 184 ++++++ .../scene/VizPanelSubHeader.tsx | 193 ++++++ .../serialization/layoutSerializers/utils.ts | 7 + .../transformSaveModelToScene.ts | 4 + .../utils/drilldownUtils.test.ts | 236 ++++++++ .../dashboard-scene/utils/drilldownUtils.ts | 75 +++ .../features/dashboard-scene/utils/utils.ts | 4 + yarn.lock | 232 +++++--- 19 files changed, 1486 insertions(+), 387 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/PanelNonApplicableDrilldownsSubHeader.tsx create mode 100644 public/app/features/dashboard-scene/scene/VizPanelSubHeader.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx create mode 100644 public/app/features/dashboard-scene/utils/drilldownUtils.test.ts create mode 100644 public/app/features/dashboard-scene/utils/drilldownUtils.ts diff --git a/package.json b/package.json index a41dee5cd15..1ab0dc7a811 100644 --- a/package.json +++ b/package.json @@ -296,8 +296,8 @@ "@grafana/plugin-ui": "^0.11.1", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^6.46.0", - "@grafana/scenes-react": "^6.46.0", + "@grafana/scenes": "6.47.1", + "@grafana/scenes-react": "6.47.1", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 25e39084acc..e5bf0e10a21 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -646,7 +646,7 @@ export interface MetricFindValue { } export interface DataSourceGetDrilldownsApplicabilityOptions { - filters: AdHocVariableFilter[]; + filters?: AdHocVariableFilter[]; groupByKeys?: string[]; timeRange?: TimeRange; queries?: TQuery[]; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 09c123d527a..b215e95220f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -365,6 +365,10 @@ export interface FeatureToggles { */ unlimitedLayoutsNesting?: boolean; /** + * Enables viewing non-applicable drilldowns on a panel level + */ + perPanelNonApplicableDrilldowns?: boolean; + /** * Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard */ panelFilterVariable?: boolean; diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index c8cfb1d47cb..e56318477c0 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -426,6 +426,9 @@ export const versionedComponents = { loadingBar: { '10.0.0': () => `Panel loading bar`, }, + PanelNonApplicableDrilldownsSubHeader: { + '12.4.0': 'Panel non-applicable drilldowns subheader', + }, HoverWidget: { container: { '10.1.0': 'data-testid hover-header-container', diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.mdx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.mdx index c00acacda20..a1fbb1dbbd4 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.mdx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.mdx @@ -421,6 +421,70 @@ Component used for rendering content wrapped in the same style as grafana panels +### Sub-header content + +The panel supports displaying additional content below the main header using the `subHeaderContent` prop. This can be used to show context-specific information. + +```tsx + + Additional info +
    + } + width={400} + height={200} +> + {(innerwidth, innerheight) => { + return ( +
    + Content +
    + ); + }} + +``` + + + + Additional info +
+ } + width={400} + height={200} + > + {(innerwidth, innerheight) => { + return ( +
+ Content +
+ ); + }} + + + ### Collapsible The panel can be collapsed/expanded by clicking on the chevron or the title. diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx index 3032099a069..7576138f533 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx @@ -196,3 +196,30 @@ it('collapses the uncontrolled panel when user clicks on the chevron or the titl expect(button).not.toHaveAttribute('aria-controlls'); expect(screen.queryByTestId(selectors.components.Panels.Panel.content)?.id).toBe(undefined); }); + +it('renders panel with a header if prop subHeaderContent', () => { + setup({ + subHeaderContent:
This should be a sub-header node
, + }); + + expect(screen.getByTestId(selectors.components.Panels.Panel.headerContainer)).toBeInTheDocument(); +}); + +it('renders panel with sub-header content in place if prop subHeaderContent', () => { + setup({ + subHeaderContent:
This should be a sub-header node
, + }); + + expect(screen.getByText('This should be a sub-header node')).toBeInTheDocument(); +}); + +it('does not render sub-header content when panel is collapsed', () => { + setup({ + title: 'Test Panel', + collapsible: true, + collapsed: true, + subHeaderContent:
This should be a sub-header node
, + }); + + expect(screen.queryByText('This should be a sub-header node')).not.toBeInTheDocument(); +}); diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index a10057a5cc3..a632f6ddca4 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -76,6 +76,11 @@ interface BaseProps { * If true, the VizPanelMenu will always be visible in the panel header. Defaults to false. */ showMenuAlways?: boolean; + /** + * Content to display in the sub-header area below the main header. + * Can contain text, pills, links, buttons, or any other React elements. + */ + subHeaderContent?: ReactNode; } interface FixedDimensions extends BaseProps { @@ -157,6 +162,7 @@ export function PanelChrome({ onMouseEnter, onDragStart, showMenuAlways = false, + subHeaderContent, }: PanelChromeProps) { const theme = useTheme2(); const styles = useStyles2(getStyles); @@ -164,6 +170,7 @@ export function PanelChrome({ const panelTitleId = useId().replace(/:/g, '_'); const { isSelected, onSelect, isSelectable } = useElementSelection(selectionId); const pointerDistance = usePointerDistance(); + const [subHeaderRef, { height: measuredSubHeaderHeight }] = useMeasure(); const hasHeader = !hoverHeader; @@ -184,11 +191,13 @@ export function PanelChrome({ const isPanelTransparent = displayMode === 'transparent'; const headerHeight = getHeaderHeight(theme, hasHeader); + const subHeaderHeight = Math.min(measuredSubHeaderHeight, headerHeight); const { contentStyle, innerWidth, innerHeight } = getContentStyle( padding, theme, headerHeight, collapsed, + subHeaderHeight, height, width ); @@ -395,37 +404,44 @@ export function PanelChrome({ )} {hasHeader && ( -
- {statusMessage && ( -
- +
+ {statusMessage && ( +
+ +
+ )} + + {headerContent} + + {menu && ( + + )} +
+ {!collapsed && subHeaderContent && ( +
+ {subHeaderContent}
)} - - {headerContent} - - {menu && ( - - )} -
+ )} {!collapsed && ( @@ -466,6 +482,7 @@ const getContentStyle = ( theme: GrafanaTheme2, headerHeight: number, collapsed: boolean, + subHeaderHeight: number, height?: number, width?: number ) => { @@ -481,7 +498,7 @@ const getContentStyle = ( let innerHeight = 0; if (height) { - innerHeight = height - headerHeight - panelPadding - panelBorder; + innerHeight = height - headerHeight - panelPadding - panelBorder - subHeaderHeight; } if (collapsed) { @@ -579,6 +596,15 @@ const getStyles = (theme: GrafanaTheme2) => { padding: newPanelPadding ? theme.spacing(0, 1, 0, 1.5) : theme.spacing(0, 0.5, 0, 1), gap: theme.spacing(1), }), + subHeader: css({ + label: 'panel-sub-header', + display: 'flex', + alignItems: 'center', + maxHeight: theme.spacing.gridSize * theme.components.panel.headerHeight, + padding: newPanelPadding ? theme.spacing(0, 1, 0, 1.5) : theme.spacing(0, 0.5, 0, 1), + overflow: 'hidden', + gap: theme.spacing(1), + }), pointer: css({ cursor: 'pointer', }), diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 26690f92481..e4afd8e5c11 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -586,6 +586,13 @@ var ( FrontendOnly: true, Owner: grafanaDashboardsSquad, }, + { + Name: "perPanelNonApplicableDrilldowns", + Description: "Enables viewing non-applicable drilldowns on a panel level", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDashboardsSquad, + }, { Name: "panelFilterVariable", Description: "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index d0a2674aa55..25e97404857 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -81,6 +81,7 @@ dashboardScene,GA,@grafana/dashboards-squad,false,false,true dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,false dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true +perPanelNonApplicableDrilldowns,experimental,@grafana/dashboards-squad,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 1f9315705ce..e791d3bd89b 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -6,7 +6,7 @@ { "metadata": { "name": "aiGeneratedDashboardChanges", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-03-05T12:01:31Z" }, "spec": { @@ -19,7 +19,7 @@ { "metadata": { "name": "alertEnrichment", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-06-06T12:16:07Z" }, "spec": { @@ -33,7 +33,7 @@ { "metadata": { "name": "alertEnrichmentConditional", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -48,7 +48,7 @@ { "metadata": { "name": "alertEnrichmentMultiStep", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -63,7 +63,7 @@ { "metadata": { "name": "alertRuleRestore", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-03-05T14:15:26Z" }, "spec": { @@ -76,7 +76,7 @@ { "metadata": { "name": "alertRuleUseFiredAtForStartsAt", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-22T11:16:38Z" }, "spec": { @@ -89,7 +89,7 @@ { "metadata": { "name": "alertingAIAnalyzeCentralStateHistory", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-16T16:42:42Z" }, "spec": { @@ -103,7 +103,7 @@ { "metadata": { "name": "alertingAIFeedback", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-23T12:38:09Z" }, "spec": { @@ -117,7 +117,7 @@ { "metadata": { "name": "alertingAIGenAlertRules", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-16T16:42:42Z" }, "spec": { @@ -131,7 +131,7 @@ { "metadata": { "name": "alertingAIGenTemplates", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-16T16:42:42Z" }, "spec": { @@ -145,7 +145,7 @@ { "metadata": { "name": "alertingAIImproveAlertRules", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-16T16:42:42Z" }, "spec": { @@ -159,7 +159,7 @@ { "metadata": { "name": "alertingBacktesting", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-12-14T14:44:14Z" }, "spec": { @@ -171,7 +171,7 @@ { "metadata": { "name": "alertingBulkActionsInUI", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-24T14:49:59Z" }, "spec": { @@ -186,7 +186,7 @@ { "metadata": { "name": "alertingCentralAlertHistory", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-05-29T15:01:38Z" }, "spec": { @@ -198,7 +198,7 @@ { "metadata": { "name": "alertingDisableSendAlertsExternal", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-05-23T12:29:19Z" }, "spec": { @@ -211,7 +211,7 @@ { "metadata": { "name": "alertingEnrichmentAssistantInvestigations", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -226,7 +226,7 @@ { "metadata": { "name": "alertingEnrichmentPerRule", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -241,7 +241,7 @@ { "metadata": { "name": "alertingFilterV2", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-09-11T11:29:26Z" }, "spec": { @@ -254,7 +254,7 @@ { "metadata": { "name": "alertingImportAlertmanagerAPI", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-06-10T08:32:50Z" }, "spec": { @@ -268,7 +268,7 @@ { "metadata": { "name": "alertingImportAlertmanagerUI", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -283,7 +283,7 @@ { "metadata": { "name": "alertingImportYAMLUI", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-05-21T15:59:41Z" }, "spec": { @@ -297,7 +297,7 @@ { "metadata": { "name": "alertingJiraIntegration", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-02-14T12:22:04Z" }, "spec": { @@ -311,7 +311,7 @@ { "metadata": { "name": "alertingListViewV2", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-05-24T14:40:49Z" }, "spec": { @@ -324,7 +324,7 @@ { "metadata": { "name": "alertingListViewV2PreviewToggle", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-22T08:50:34Z" }, "spec": { @@ -337,7 +337,7 @@ { "metadata": { "name": "alertingMigrationUI", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-03-14T16:40:05Z" }, "spec": { @@ -351,7 +351,7 @@ { "metadata": { "name": "alertingNotificationHistory", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-17T13:26:26Z" }, "spec": { @@ -365,7 +365,7 @@ { "metadata": { "name": "alertingNotificationsStepMode", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-11-22T11:07:45Z" }, "spec": { @@ -379,7 +379,7 @@ { "metadata": { "name": "alertingPrometheusRulesPrimary", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-09-27T12:27:16Z" }, "spec": { @@ -392,7 +392,7 @@ { "metadata": { "name": "alertingProvenanceLockWrites", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-23T18:16:06Z" }, "spec": { @@ -405,7 +405,7 @@ { "metadata": { "name": "alertingQueryAndExpressionsStepMode", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-09-26T06:33:14Z" }, "spec": { @@ -419,7 +419,7 @@ { "metadata": { "name": "alertingQueryOptimization", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-01-10T20:52:58Z" }, "spec": { @@ -432,7 +432,7 @@ { "metadata": { "name": "alertingRulePermanentlyDelete", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-03T11:18:25Z" }, "spec": { @@ -447,7 +447,7 @@ { "metadata": { "name": "alertingRuleRecoverDeleted", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-03-27T14:39:26Z" }, "spec": { @@ -461,7 +461,7 @@ { "metadata": { "name": "alertingRuleVersionHistoryRestore", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-02-17T12:25:32Z" }, "spec": { @@ -476,7 +476,7 @@ { "metadata": { "name": "alertingSaveStateCompressed", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-01-27T17:47:33Z" }, "spec": { @@ -489,7 +489,7 @@ { "metadata": { "name": "alertingSaveStatePeriodic", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-01-23T16:03:30Z" }, "spec": { @@ -501,7 +501,7 @@ { "metadata": { "name": "alertingTriage", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -516,7 +516,7 @@ { "metadata": { "name": "alertingUIOptimizeReducer", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-11-18T10:59:00Z" }, "spec": { @@ -530,7 +530,7 @@ { "metadata": { "name": "alertingUIUseBackendFilters", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-11-13T14:52:14Z" }, "spec": { @@ -543,7 +543,7 @@ { "metadata": { "name": "alertingUseNewSimplifiedRoutingHashAlgorithm", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -559,7 +559,7 @@ { "metadata": { "name": "alertmanagerRemotePrimary", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -571,7 +571,7 @@ { "metadata": { "name": "alertmanagerRemoteSecondary", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -583,7 +583,7 @@ { "metadata": { "name": "alertmanagerRemoteSecondaryWithRemoteState", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-25T15:06:59Z" }, "spec": { @@ -596,7 +596,7 @@ { "metadata": { "name": "annotationPermissionUpdate", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-10-31T13:30:13Z" }, "spec": { @@ -609,7 +609,7 @@ { "metadata": { "name": "appPlatformGrpcClientAuth", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-10-14T10:47:18Z" }, "spec": { @@ -622,7 +622,7 @@ { "metadata": { "name": "assetSriChecks", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-03-04T10:56:35Z" }, "spec": { @@ -635,7 +635,7 @@ { "metadata": { "name": "authZGRPCServer", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-06-13T09:41:35Z" }, "spec": { @@ -648,7 +648,7 @@ { "metadata": { "name": "awsAsyncQueryCaching", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-07-21T15:34:07Z" }, "spec": { @@ -661,7 +661,7 @@ { "metadata": { "name": "awsDatasourcesHttpProxy", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-11-12T18:51:23Z" }, "spec": { @@ -674,7 +674,7 @@ { "metadata": { "name": "awsDatasourcesTempCredentials", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-07-06T15:06:11Z" }, "spec": { @@ -687,7 +687,7 @@ { "metadata": { "name": "azureMonitorDisableLogLimit", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-10-24T13:32:09Z" }, "spec": { @@ -700,7 +700,7 @@ { "metadata": { "name": "azureMonitorEnableUserAuth", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-11-27T14:01:54Z" }, "spec": { @@ -713,7 +713,7 @@ { "metadata": { "name": "azureMonitorLogsBuilderEditor", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-02T14:15:25Z" }, "spec": { @@ -726,7 +726,7 @@ { "metadata": { "name": "azureMonitorPrometheusExemplars", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-06-06T16:53:17Z" }, "spec": { @@ -739,7 +739,7 @@ { "metadata": { "name": "azureResourcePickerUpdates", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -754,7 +754,7 @@ { "metadata": { "name": "cachingOptimizeSerializationMemoryUsage", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-10-12T16:56:49Z" }, "spec": { @@ -766,7 +766,7 @@ { "metadata": { "name": "canvasPanelNesting", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-05-31T19:03:34Z" }, "spec": { @@ -779,7 +779,7 @@ { "metadata": { "name": "canvasPanelPanZoom", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-01-02T19:52:21Z" }, "spec": { @@ -792,7 +792,7 @@ { "metadata": { "name": "cdnPluginsLoadFirst", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -806,7 +806,7 @@ { "metadata": { "name": "cdnPluginsUrls", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -820,7 +820,7 @@ { "metadata": { "name": "cloudRBACRoles", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-01-10T13:19:01Z" }, "spec": { @@ -834,7 +834,7 @@ { "metadata": { "name": "cloudWatchBatchQueries", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-10-20T19:09:41Z" }, "spec": { @@ -846,7 +846,7 @@ { "metadata": { "name": "cloudWatchCrossAccountQuerying", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-11-28T11:39:12Z" }, "spec": { @@ -859,7 +859,7 @@ { "metadata": { "name": "cloudWatchNewLabelParsing", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-04-05T15:57:56Z" }, "spec": { @@ -872,7 +872,7 @@ { "metadata": { "name": "cloudWatchRoundUpEndTime", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-06-27T15:10:28Z" }, "spec": { @@ -885,7 +885,7 @@ { "metadata": { "name": "configurableSchedulerTick", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-07-26T16:44:12Z" }, "spec": { @@ -899,7 +899,7 @@ { "metadata": { "name": "crashDetection", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-11-12T15:07:27Z" }, "spec": { @@ -912,7 +912,7 @@ { "metadata": { "name": "dashboardDisableSchemaValidationV1", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-11T16:52:46Z" }, "spec": { @@ -924,7 +924,7 @@ { "metadata": { "name": "dashboardDisableSchemaValidationV2", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-11T16:52:46Z" }, "spec": { @@ -936,7 +936,7 @@ { "metadata": { "name": "dashboardLevelTimeMacros", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T09:49:07Z" }, "spec": { @@ -949,7 +949,7 @@ { "metadata": { "name": "dashboardLibrary", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -962,7 +962,7 @@ { "metadata": { "name": "dashboardNewLayouts", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-10-23T08:55:45Z" }, "spec": { @@ -974,7 +974,7 @@ { "metadata": { "name": "dashboardScene", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-11-13T08:51:21Z" }, "spec": { @@ -988,7 +988,7 @@ { "metadata": { "name": "dashboardSceneForViewers", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-11-02T19:02:25Z" }, "spec": { @@ -1002,7 +1002,7 @@ { "metadata": { "name": "dashboardSceneSolo", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-02-11T08:08:47Z" }, "spec": { @@ -1016,7 +1016,7 @@ { "metadata": { "name": "dashboardSchemaValidationLogging", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-11T16:52:46Z" }, "spec": { @@ -1028,7 +1028,7 @@ { "metadata": { "name": "dashboardTemplates", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-28T20:05:32Z" }, "spec": { @@ -1040,7 +1040,7 @@ { "metadata": { "name": "dashboardUndoRedo", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -1054,7 +1054,7 @@ { "metadata": { "name": "dashgpt", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-08-30T20:22:05Z" }, "spec": { @@ -1068,7 +1068,7 @@ { "metadata": { "name": "dataplaneAggregator", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-08-09T08:41:07Z" }, "spec": { @@ -1081,7 +1081,7 @@ { "metadata": { "name": "datasourceAPIServers", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-09-19T08:28:27Z" }, "spec": { @@ -1094,7 +1094,7 @@ { "metadata": { "name": "datasourceConnectionsTab", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-01-21T17:39:48Z" }, "spec": { @@ -1107,7 +1107,7 @@ { "metadata": { "name": "datasourceQueryTypes", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-05-23T16:46:28Z" }, "spec": { @@ -1120,7 +1120,7 @@ { "metadata": { "name": "disableClassicHTTPHistogram", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-06-18T19:37:44Z" }, "spec": { @@ -1133,7 +1133,7 @@ { "metadata": { "name": "disableEnvelopeEncryption", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-05-24T08:34:47Z" }, "spec": { @@ -1146,7 +1146,7 @@ { "metadata": { "name": "disableNumericMetricsSortingInExpressions", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-04-16T14:52:47Z" }, "spec": { @@ -1159,7 +1159,7 @@ { "metadata": { "name": "disableSSEDataplane", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-04-12T16:24:34Z" }, "spec": { @@ -1171,7 +1171,7 @@ { "metadata": { "name": "elasticsearchCrossClusterSearch", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-12-12T22:20:04Z" }, "spec": { @@ -1184,7 +1184,7 @@ { "metadata": { "name": "elasticsearchImprovedParsing", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-01-15T17:05:54Z" }, "spec": { @@ -1196,7 +1196,7 @@ { "metadata": { "name": "enableAppChromeExtensions", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-06-30T04:32:08Z" }, "spec": { @@ -1211,7 +1211,7 @@ { "metadata": { "name": "enableDashboardEmptyExtensions", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-13T07:03:13Z" }, "spec": { @@ -1226,7 +1226,7 @@ { "metadata": { "name": "enableDatagridEditing", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-04-24T14:46:31Z" }, "spec": { @@ -1238,7 +1238,7 @@ { "metadata": { "name": "enableExtensionsAdminPage", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-11-05T15:55:10Z" }, "spec": { @@ -1251,7 +1251,7 @@ { "metadata": { "name": "enableNativeHTTPHistogram", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-10-03T18:23:55Z" }, "spec": { @@ -1264,7 +1264,7 @@ { "metadata": { "name": "enableSCIM", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-11-07T14:38:46Z" }, "spec": { @@ -1276,7 +1276,7 @@ { "metadata": { "name": "enableScopesInMetricsExplore", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-11-06T13:11:33Z" }, "spec": { @@ -1289,7 +1289,7 @@ { "metadata": { "name": "exploreLogsAggregatedMetrics", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1302,7 +1302,7 @@ { "metadata": { "name": "exploreLogsLimitedTimeRange", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1315,7 +1315,7 @@ { "metadata": { "name": "exploreLogsShardSplitting", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1328,7 +1328,7 @@ { "metadata": { "name": "exploreMetricsRelatedLogs", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-11-05T16:28:43Z" }, "spec": { @@ -1341,7 +1341,7 @@ { "metadata": { "name": "externalServiceAccounts", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-09-28T07:26:37Z" }, "spec": { @@ -1353,7 +1353,7 @@ { "metadata": { "name": "extraThemes", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-05-10T13:37:04Z", "deletionTimestamp": "2025-05-20T08:18:08Z" }, @@ -1367,7 +1367,7 @@ { "metadata": { "name": "faroDatasourceSelector", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-05-05T00:35:10Z" }, "spec": { @@ -1380,7 +1380,7 @@ { "metadata": { "name": "favoriteDatasources", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -1394,7 +1394,7 @@ { "metadata": { "name": "featureHighlights", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-02-03T11:53:23Z" }, "spec": { @@ -1407,7 +1407,7 @@ { "metadata": { "name": "fetchRulesUsingPost", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-01-29T12:17:44Z" }, "spec": { @@ -1420,7 +1420,7 @@ { "metadata": { "name": "foldersAppPlatformAPI", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-03T14:15:23Z" }, "spec": { @@ -1435,7 +1435,7 @@ { "metadata": { "name": "grafanaAPIServerEnsureKubectlAccess", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-12-06T20:21:21Z" }, "spec": { @@ -1449,7 +1449,7 @@ { "metadata": { "name": "grafanaAPIServerWithExperimentalAPIs", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-10-06T18:55:22Z" }, "spec": { @@ -1463,7 +1463,7 @@ { "metadata": { "name": "grafanaAdvisor", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-01-20T10:08:00Z" }, "spec": { @@ -1475,7 +1475,7 @@ { "metadata": { "name": "grafanaAssistantInProfilesDrilldown", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -1490,7 +1490,7 @@ { "metadata": { "name": "grafanaManagedRecordingRules", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-04-22T17:53:16Z", "deletionTimestamp": "2025-05-19T10:15:49Z" }, @@ -1504,7 +1504,7 @@ { "metadata": { "name": "grafanaconThemes", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-02-06T11:08:04Z" }, "spec": { @@ -1519,7 +1519,7 @@ { "metadata": { "name": "graphiteBackendMode", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -1533,7 +1533,7 @@ { "metadata": { "name": "groupAttributeSync", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-09-09T15:29:43Z" }, "spec": { @@ -1546,7 +1546,7 @@ { "metadata": { "name": "groupByVariable", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-02-14T17:18:04Z" }, "spec": { @@ -1559,7 +1559,7 @@ { "metadata": { "name": "grpcServer", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-09-26T20:25:34Z" }, "spec": { @@ -1571,7 +1571,7 @@ { "metadata": { "name": "improvedExternalSessionHandling", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-09-17T10:54:39Z" }, "spec": { @@ -1584,7 +1584,7 @@ { "metadata": { "name": "improvedExternalSessionHandlingSAML", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-01-09T17:02:49Z" }, "spec": { @@ -1597,7 +1597,7 @@ { "metadata": { "name": "individualCookiePreferences", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-02-21T10:19:07Z" }, "spec": { @@ -1609,7 +1609,7 @@ { "metadata": { "name": "infinityRunQueriesInParallel", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-03-14T12:54:04Z" }, "spec": { @@ -1621,7 +1621,7 @@ { "metadata": { "name": "influxdbBackendMigration", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-02-09T18:26:16Z", "deletionTimestamp": "2023-01-17T14:11:26Z" }, @@ -1636,7 +1636,7 @@ { "metadata": { "name": "influxdbRunQueriesInParallel", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-02-01T10:58:24Z" }, "spec": { @@ -1648,7 +1648,7 @@ { "metadata": { "name": "influxqlStreamingParser", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-11-29T17:29:35Z" }, "spec": { @@ -1660,7 +1660,7 @@ { "metadata": { "name": "interactiveLearning", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-22T08:06:21Z" }, "spec": { @@ -1672,7 +1672,7 @@ { "metadata": { "name": "investigationsBackend", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-12-18T08:31:03Z" }, "spec": { @@ -1685,7 +1685,7 @@ { "metadata": { "name": "jaegerEnableGrpcEndpoint", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-31T18:19:16Z" }, "spec": { @@ -1697,7 +1697,7 @@ { "metadata": { "name": "jitterAlertRulesWithinGroups", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-01-18T18:48:11Z" }, "spec": { @@ -1711,7 +1711,7 @@ { "metadata": { "name": "k8SFolderCounts", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { @@ -1724,7 +1724,7 @@ { "metadata": { "name": "k8SFolderMove", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { @@ -1737,7 +1737,7 @@ { "metadata": { "name": "kubernetesAggregator", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-02-12T20:59:35Z" }, "spec": { @@ -1750,7 +1750,7 @@ { "metadata": { "name": "kubernetesAggregatorCapTokenAuth", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-05-15T18:14:23Z" }, "spec": { @@ -1763,7 +1763,7 @@ { "metadata": { "name": "kubernetesAlertingRules", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -1777,7 +1777,7 @@ { "metadata": { "name": "kubernetesAnnotations", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-11-06T18:22:20Z" }, "spec": { @@ -1790,7 +1790,7 @@ { "metadata": { "name": "kubernetesAuthZHandlerRedirect", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -1804,7 +1804,7 @@ { "metadata": { "name": "kubernetesAuthnMutation", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-25T15:05:32Z" }, "spec": { @@ -1817,7 +1817,7 @@ { "metadata": { "name": "kubernetesAuthzApis", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-06-18T07:43:01Z" }, "spec": { @@ -1830,7 +1830,7 @@ { "metadata": { "name": "kubernetesAuthzResourcePermissionApis", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -1844,7 +1844,7 @@ { "metadata": { "name": "kubernetesAuthzZanzanaSync", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-13T19:37:13Z" }, "spec": { @@ -1857,7 +1857,7 @@ { "metadata": { "name": "kubernetesCorrelations", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -1871,7 +1871,7 @@ { "metadata": { "name": "kubernetesDashboards", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-06-05T14:34:23Z" }, "spec": { @@ -1884,7 +1884,7 @@ { "metadata": { "name": "kubernetesFeatureToggles", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-01-18T05:32:44Z" }, "spec": { @@ -1897,7 +1897,7 @@ { "metadata": { "name": "kubernetesLibraryPanels", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-06-25T22:21:56Z" }, "spec": { @@ -1910,7 +1910,7 @@ { "metadata": { "name": "kubernetesLogsDrilldown", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-16T21:31:42Z" }, "spec": { @@ -1923,7 +1923,7 @@ { "metadata": { "name": "kubernetesQueryCaching", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-20T16:11:25Z" }, "spec": { @@ -1936,7 +1936,7 @@ { "metadata": { "name": "kubernetesShortURLs", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -1950,7 +1950,7 @@ { "metadata": { "name": "kubernetesSnapshots", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-12-05T22:31:49Z" }, "spec": { @@ -1963,7 +1963,7 @@ { "metadata": { "name": "kubernetesStars", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -1977,7 +1977,7 @@ { "metadata": { "name": "localeFormatPreference", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-03-31T13:59:07Z" }, "spec": { @@ -1989,7 +1989,7 @@ { "metadata": { "name": "logQLScope", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-11-11T11:53:24Z" }, "spec": { @@ -2003,7 +2003,7 @@ { "metadata": { "name": "logRequestsInstrumentedAsUnknown", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-06-10T08:56:55Z" }, "spec": { @@ -2015,7 +2015,7 @@ { "metadata": { "name": "logsContextDatasourceUi", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-01-27T14:12:01Z" }, "spec": { @@ -2029,7 +2029,7 @@ { "metadata": { "name": "logsExploreTableDefaultVisualization", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-05-02T15:28:15Z" }, "spec": { @@ -2042,7 +2042,7 @@ { "metadata": { "name": "logsExploreTableVisualisation", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-07-12T13:52:42Z" }, "spec": { @@ -2056,7 +2056,7 @@ { "metadata": { "name": "logsPanelControls", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-07T14:38:55Z" }, "spec": { @@ -2070,7 +2070,7 @@ { "metadata": { "name": "lokiExperimentalStreaming", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-06-19T10:03:51Z" }, "spec": { @@ -2082,7 +2082,7 @@ { "metadata": { "name": "lokiLabelNamesQueryApi", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-12-13T14:31:41Z" }, "spec": { @@ -2095,7 +2095,7 @@ { "metadata": { "name": "lokiLogsDataplane", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-07-13T07:58:00Z" }, "spec": { @@ -2107,8 +2107,8 @@ { "metadata": { "name": "lokiQueryLimitsContext", - "resourceVersion": "1763727063618", - "creationTimestamp": "2025-11-21T12:11:03Z" + "resourceVersion": "1763734583253", + "creationTimestamp": "2025-11-21T14:16:23Z" }, "spec": { "description": "Send X-Loki-Query-Limits-Context header to Loki on first split request", @@ -2120,7 +2120,7 @@ { "metadata": { "name": "lokiQuerySplitting", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-02-09T17:27:02Z" }, "spec": { @@ -2134,7 +2134,7 @@ { "metadata": { "name": "lokiRunQueriesInParallel", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-09-19T09:34:01Z" }, "spec": { @@ -2146,7 +2146,7 @@ { "metadata": { "name": "lokiShardSplitting", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-10-23T11:21:03Z" }, "spec": { @@ -2159,7 +2159,7 @@ { "metadata": { "name": "managedDualWriter", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-02-19T14:50:39Z" }, "spec": { @@ -2172,7 +2172,7 @@ { "metadata": { "name": "metricsFromProfiles", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-09T10:55:28Z" }, "spec": { @@ -2185,7 +2185,7 @@ { "metadata": { "name": "mlExpressions", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-07-13T17:37:50Z" }, "spec": { @@ -2197,7 +2197,7 @@ { "metadata": { "name": "multiTenantTempCredentials", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-02T20:25:50Z" }, "spec": { @@ -2210,7 +2210,7 @@ { "metadata": { "name": "mysqlAnsiQuotes", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-10-12T11:43:35Z" }, "spec": { @@ -2222,7 +2222,7 @@ { "metadata": { "name": "newClickhouseConfigPageDesign", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -2236,7 +2236,7 @@ { "metadata": { "name": "newDashboardWithFiltersAndGroupBy", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-04-04T11:25:21Z" }, "spec": { @@ -2249,7 +2249,7 @@ { "metadata": { "name": "newFiltersUI", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-08-30T12:48:13Z" }, "spec": { @@ -2262,7 +2262,7 @@ { "metadata": { "name": "newGauge", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-20T16:33:19Z" }, "spec": { @@ -2276,7 +2276,7 @@ { "metadata": { "name": "newInfluxDSConfigPageDesign", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-06-25T16:39:54Z" }, "spec": { @@ -2289,7 +2289,7 @@ { "metadata": { "name": "newLogContext", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z" }, "spec": { @@ -2302,7 +2302,7 @@ { "metadata": { "name": "newLogsPanel", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-02-04T17:40:17Z" }, "spec": { @@ -2316,7 +2316,7 @@ { "metadata": { "name": "newPanelPadding", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-11-12T15:40:46Z" }, "spec": { @@ -2329,7 +2329,7 @@ { "metadata": { "name": "newShareReportDrawer", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-02-17T19:05:46Z" }, "spec": { @@ -2342,8 +2342,8 @@ { "metadata": { "name": "newTimeRangeZoomShortcuts", - "resourceVersion": "1763727063618", - "creationTimestamp": "2025-11-21T12:11:03Z" + "resourceVersion": "1763734583253", + "creationTimestamp": "2025-11-21T14:16:23Z" }, "spec": { "description": "Enables new keyboard shortcuts for time range zoom operations", @@ -2355,7 +2355,7 @@ { "metadata": { "name": "newVizSuggestions", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-11-12T19:26:29Z" }, "spec": { @@ -2369,7 +2369,7 @@ { "metadata": { "name": "oauthRequireSubClaim", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-03-25T13:22:24Z" }, "spec": { @@ -2382,7 +2382,7 @@ { "metadata": { "name": "onPremToCloudMigrations", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-01-22T16:09:08Z" }, "spec": { @@ -2395,7 +2395,7 @@ { "metadata": { "name": "onlyStoreActionSets", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-20T15:02:56Z" }, "spec": { @@ -2409,7 +2409,7 @@ { "metadata": { "name": "otelLogsFormatting", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-16T15:42:14Z" }, "spec": { @@ -2422,7 +2422,7 @@ { "metadata": { "name": "panelFilterVariable", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-11-03T12:15:54Z" }, "spec": { @@ -2436,7 +2436,7 @@ { "metadata": { "name": "panelTimeSettings", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-29T08:06:23Z" }, "spec": { @@ -2448,7 +2448,7 @@ { "metadata": { "name": "panelTitleSearch", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-02-15T18:26:03Z" }, "spec": { @@ -2460,7 +2460,7 @@ { "metadata": { "name": "passwordlessMagicLinkAuthentication", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-11-14T13:50:55Z" }, "spec": { @@ -2473,7 +2473,7 @@ { "metadata": { "name": "pdfTables", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-11-06T13:39:22Z" }, "spec": { @@ -2482,10 +2482,23 @@ "codeowner": "@grafana/grafana-operator-experience-squad" } }, + { + "metadata": { + "name": "perPanelNonApplicableDrilldowns", + "resourceVersion": "1763734583253", + "creationTimestamp": "2025-11-21T14:16:23Z" + }, + "spec": { + "description": "Enables viewing non-applicable drilldowns on a panel level", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, { "metadata": { "name": "permissionsFilterRemoveSubquery", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-08-02T07:39:25Z" }, "spec": { @@ -2497,7 +2510,7 @@ { "metadata": { "name": "playlistsReconciler", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-12-20T03:09:31Z" }, "spec": { @@ -2510,7 +2523,7 @@ { "metadata": { "name": "pluginContainers", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -2525,7 +2538,7 @@ { "metadata": { "name": "pluginInstallAPISync", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-24T12:09:26Z" }, "spec": { @@ -2538,7 +2551,7 @@ { "metadata": { "name": "pluginProxyPreserveTrailingSlash", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-06-05T11:36:14Z" }, "spec": { @@ -2551,7 +2564,7 @@ { "metadata": { "name": "pluginStoreServiceLoading", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-17T20:01:43Z" }, "spec": { @@ -2564,7 +2577,7 @@ { "metadata": { "name": "pluginsAutoUpdate", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-16T11:44:39Z" }, "spec": { @@ -2576,7 +2589,7 @@ { "metadata": { "name": "pluginsSriChecks", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-10-04T12:55:09Z" }, "spec": { @@ -2589,7 +2602,7 @@ { "metadata": { "name": "postgresDSUsePGX", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-05-26T06:54:18Z", "deletionTimestamp": "2025-06-03T12:45:07Z" }, @@ -2602,7 +2615,7 @@ { "metadata": { "name": "preferLibraryPanelTitle", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-06-17T11:21:21Z" }, "spec": { @@ -2615,7 +2628,7 @@ { "metadata": { "name": "preserveDashboardStateWhenNavigating", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-05-27T12:28:06Z" }, "spec": { @@ -2628,7 +2641,7 @@ { "metadata": { "name": "preventPanelChromeOverflow", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-17T14:40:08Z" }, "spec": { @@ -2642,7 +2655,7 @@ { "metadata": { "name": "prometheusAzureOverrideAudience", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-05-30T15:43:32Z", "deletionTimestamp": "2023-07-16T21:30:14Z" }, @@ -2656,7 +2669,7 @@ { "metadata": { "name": "prometheusSpecialCharsInLabelValues", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-12-18T21:31:08Z" }, "spec": { @@ -2669,7 +2682,7 @@ { "metadata": { "name": "prometheusTypeMigration", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -2684,7 +2697,7 @@ { "metadata": { "name": "provisioning", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-11-22T09:03:50Z" }, "spec": { @@ -2697,7 +2710,7 @@ { "metadata": { "name": "publicDashboardsEmailSharing", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-01-03T19:45:15Z" }, "spec": { @@ -2710,7 +2723,7 @@ { "metadata": { "name": "publicDashboardsScene", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-03-22T14:48:21Z" }, "spec": { @@ -2724,7 +2737,7 @@ { "metadata": { "name": "queryCacheRequestDeduplication", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -2738,7 +2751,7 @@ { "metadata": { "name": "queryLibrary", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-10-07T18:31:45Z", "deletionTimestamp": "2023-03-20T16:00:14Z" }, @@ -2751,7 +2764,7 @@ { "metadata": { "name": "queryService", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -2764,7 +2777,7 @@ { "metadata": { "name": "queryServiceFromExplore", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-02T10:00:33Z" }, "spec": { @@ -2777,7 +2790,7 @@ { "metadata": { "name": "queryServiceFromUI", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -2790,7 +2803,7 @@ { "metadata": { "name": "queryServiceRewrite", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -2803,7 +2816,7 @@ { "metadata": { "name": "queryServiceWithConnections", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-28T19:28:26Z", "deletionTimestamp": "2025-08-29T12:49:57Z" }, @@ -2817,7 +2830,7 @@ { "metadata": { "name": "refactorVariablesTimeRange", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-06-06T13:12:09Z" }, "spec": { @@ -2829,8 +2842,8 @@ { "metadata": { "name": "refreshTokenRequired", - "resourceVersion": "1763727063618", - "creationTimestamp": "2025-11-21T12:11:03Z" + "resourceVersion": "1763734583253", + "creationTimestamp": "2025-11-21T14:16:23Z" }, "spec": { "description": "Require that refresh tokens are present in oauth tokens.", @@ -2842,7 +2855,7 @@ { "metadata": { "name": "reloadDashboardsOnParamsChange", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-10-25T12:56:54Z" }, "spec": { @@ -2855,7 +2868,7 @@ { "metadata": { "name": "renderAuthJWT", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-04-03T16:53:38Z" }, "spec": { @@ -2867,7 +2880,7 @@ { "metadata": { "name": "rendererDisableAppPluginsPreload", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-02-24T14:43:06Z" }, "spec": { @@ -2881,7 +2894,7 @@ { "metadata": { "name": "reportingRetries", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-08-31T07:47:47Z" }, "spec": { @@ -2894,7 +2907,7 @@ { "metadata": { "name": "restoreDashboards", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-05-23T14:35:54Z" }, "spec": { @@ -2907,7 +2920,7 @@ { "metadata": { "name": "restrictedPluginApis", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -2923,7 +2936,7 @@ { "metadata": { "name": "rolePickerDrawer", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-09-26T12:51:38Z" }, "spec": { @@ -2935,8 +2948,8 @@ { "metadata": { "name": "rudderstackUpgrade", - "resourceVersion": "1763727063618", - "creationTimestamp": "2025-11-21T12:11:03Z" + "resourceVersion": "1763734583253", + "creationTimestamp": "2025-11-21T14:16:23Z" }, "spec": { "description": "Enables the new version of rudderstack", @@ -2949,7 +2962,7 @@ { "metadata": { "name": "scanRowInvalidDashboardParseFallbackEnabled", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-30T14:18:38Z" }, "spec": { @@ -2961,7 +2974,7 @@ { "metadata": { "name": "scopeApi", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-11-27T07:58:25Z" }, "spec": { @@ -2974,7 +2987,7 @@ { "metadata": { "name": "scopeFilters", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-03-05T15:41:19Z" }, "spec": { @@ -2987,7 +3000,7 @@ { "metadata": { "name": "scopeSearchAllLevels", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-14T07:42:16Z" }, "spec": { @@ -3000,7 +3013,7 @@ { "metadata": { "name": "secretsManagementAppPlatform", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-03-19T09:25:14Z" }, "spec": { @@ -3012,7 +3025,7 @@ { "metadata": { "name": "secretsManagementAppPlatformUI", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -3025,7 +3038,7 @@ { "metadata": { "name": "sharingDashboardImage", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-15T21:07:39Z" }, "spec": { @@ -3039,7 +3052,7 @@ { "metadata": { "name": "showDashboardValidationWarnings", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-10-14T13:51:05Z" }, "spec": { @@ -3051,7 +3064,7 @@ { "metadata": { "name": "sqlExpressions", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-02-27T21:16:00Z" }, "spec": { @@ -3063,7 +3076,7 @@ { "metadata": { "name": "sqlExpressionsColumnAutoComplete", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-23T21:49:58Z" }, "spec": { @@ -3076,7 +3089,7 @@ { "metadata": { "name": "sseGroupByDatasource", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-09-07T20:02:07Z" }, "spec": { @@ -3088,7 +3101,7 @@ { "metadata": { "name": "ssoSettingsLDAP", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-06-18T11:31:27Z" }, "spec": { @@ -3102,7 +3115,7 @@ { "metadata": { "name": "starsFromAPIServer", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -3117,7 +3130,7 @@ { "metadata": { "name": "storage", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2022-03-17T17:19:23Z" }, "spec": { @@ -3129,7 +3142,7 @@ { "metadata": { "name": "suggestedDashboards", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-11-07T13:38:59Z" }, "spec": { @@ -3141,7 +3154,7 @@ { "metadata": { "name": "tableSharedCrosshair", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-12-13T09:33:14Z" }, "spec": { @@ -3154,7 +3167,7 @@ { "metadata": { "name": "tabularNumbers", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-06-24T11:52:03Z" }, "spec": { @@ -3167,7 +3180,7 @@ { "metadata": { "name": "teamFolders", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -3181,7 +3194,7 @@ { "metadata": { "name": "teamHttpHeadersTempo", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-05-22T19:13:31Z" }, "spec": { @@ -3193,7 +3206,7 @@ { "metadata": { "name": "tempoAlerting", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-15T13:36:36Z" }, "spec": { @@ -3205,7 +3218,7 @@ { "metadata": { "name": "tempoSearchBackendMigration", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -3220,7 +3233,7 @@ { "metadata": { "name": "timeComparison", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-24T20:07:28Z" }, "spec": { @@ -3233,7 +3246,7 @@ { "metadata": { "name": "timeRangePan", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-11-05T01:39:46Z" }, "spec": { @@ -3246,7 +3259,7 @@ { "metadata": { "name": "timeRangeProvider", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-10-22T10:52:33Z" }, "spec": { @@ -3258,7 +3271,7 @@ { "metadata": { "name": "transformationsEmptyPlaceholder", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-11-17T13:57:05Z" }, "spec": { @@ -3271,7 +3284,7 @@ { "metadata": { "name": "ttlPluginInstanceManager", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-11-18T11:17:23Z" }, "spec": { @@ -3284,7 +3297,7 @@ { "metadata": { "name": "unifiedHistory", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-12-13T10:41:18Z" }, "spec": { @@ -3297,7 +3310,7 @@ { "metadata": { "name": "unifiedNavbars", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-04-09T12:51:22Z" }, "spec": { @@ -3311,7 +3324,7 @@ { "metadata": { "name": "unifiedRequestLog", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2023-03-31T13:38:09Z" }, "spec": { @@ -3324,7 +3337,7 @@ { "metadata": { "name": "unifiedStorageBigObjectsSupport", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-10-17T10:18:29Z" }, "spec": { @@ -3336,7 +3349,7 @@ { "metadata": { "name": "unifiedStorageGrpcConnectionPool", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-03-21T13:24:54Z" }, "spec": { @@ -3349,7 +3362,7 @@ { "metadata": { "name": "unifiedStorageSearch", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-09-30T19:46:14Z" }, "spec": { @@ -3362,7 +3375,7 @@ { "metadata": { "name": "unifiedStorageSearchDualReaderEnabled", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-18T12:43:56Z" }, "spec": { @@ -3375,7 +3388,7 @@ { "metadata": { "name": "unifiedStorageSearchSprinkles", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-12-18T17:00:54Z" }, "spec": { @@ -3388,7 +3401,7 @@ { "metadata": { "name": "unifiedStorageSearchUI", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-12-19T18:21:48Z" }, "spec": { @@ -3401,7 +3414,7 @@ { "metadata": { "name": "unlimitedLayoutsNesting", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-10T12:15:54Z" }, "spec": { @@ -3414,7 +3427,7 @@ { "metadata": { "name": "useKubernetesShortURLsAPI", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -3428,7 +3441,7 @@ { "metadata": { "name": "useMultipleScopeNodesEndpoint", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-08-29T14:46:39Z", "deletionTimestamp": "2025-09-01T09:33:33Z" }, @@ -3444,7 +3457,7 @@ { "metadata": { "name": "useScopeSingleNodeEndpoint", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T14:32:41Z" }, "spec": { @@ -3459,7 +3472,7 @@ { "metadata": { "name": "useScopesNavigationEndpoint", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-03-31T15:20:00Z" }, "spec": { @@ -3473,7 +3486,7 @@ { "metadata": { "name": "useSessionStorageForRedirection", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-09-23T09:31:23Z" }, "spec": { @@ -3486,7 +3499,7 @@ { "metadata": { "name": "vizActionsAuth", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-07-31T22:56:50Z", "deletionTimestamp": "2025-08-01T11:30:17Z" }, @@ -3501,7 +3514,7 @@ { "metadata": { "name": "zanzana", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2024-06-19T13:59:47Z" }, "spec": { @@ -3514,7 +3527,7 @@ { "metadata": { "name": "zanzanaNoLegacyClient", - "resourceVersion": "1763727063618", + "resourceVersion": "1763734583253", "creationTimestamp": "2025-10-21T14:03:17Z" }, "spec": { diff --git a/public/app/features/dashboard-scene/scene/PanelNonApplicableDrilldownsSubHeader.tsx b/public/app/features/dashboard-scene/scene/PanelNonApplicableDrilldownsSubHeader.tsx new file mode 100644 index 00000000000..fe8437f9dc5 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/PanelNonApplicableDrilldownsSubHeader.tsx @@ -0,0 +1,189 @@ +import { css, cx } from '@emotion/css'; +import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; +import { useMeasure } from 'react-use'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { AdHocFiltersVariable, GroupByVariable, SceneQueryRunner } from '@grafana/scenes'; +import { Tooltip, measureText, useStyles2, useTheme2 } from '@grafana/ui'; + +import { getDrilldownApplicability } from '../utils/drilldownUtils'; + +const GAP_SIZE = 8; +const FONT_SIZE = 12; + +interface Props { + filtersVar?: AdHocFiltersVariable; + groupByVar?: GroupByVariable; + queryRunner: SceneQueryRunner; +} + +export function PanelNonApplicableDrilldownsSubHeader({ filtersVar, groupByVar, queryRunner }: Props) { + const styles = useStyles2(getStyles); + const theme = useTheme2(); + const [sizeRef, { width: containerWidth }] = useMeasure(); + + // Subscribe to state changes (this triggers re-renders when state changes) + const filtersState = filtersVar?.useState(); + const groupByState = groupByVar?.useState(); + + const [nonApplicable, setNonApplicable] = useState([]); + const [visibleCount, setVisibleCount] = useState(0); + + // Create stable string representations to detect actual changes + const filterKey = useMemo(() => { + const filters = filtersState?.filters ?? []; + const originFilters = filtersState?.originFilters ?? []; + const filterValues = [...filters, ...originFilters]; + return JSON.stringify(filterValues.map((f) => `${f.key}${f.operator}${f.value}${f.origin ?? ''}`)); + }, [filtersState?.filters, filtersState?.originFilters]); + + const groupByKey = useMemo(() => { + const value = groupByState?.value ?? []; + const groupByValues = Array.isArray(value) ? value : value ? [value] : []; + return JSON.stringify(groupByValues); + }, [groupByState?.value]); + + useEffect(() => { + const fetchApplicability = async () => { + const filters = filtersState?.filters ?? []; + const originFilters = filtersState?.originFilters ?? []; + const value = groupByState?.value ?? []; + + const filterValues = [...filters, ...originFilters]; + const groupByValues = Array.isArray(value) ? value : value ? [value] : []; + + const labels: string[] = []; + + const applicability = await getDrilldownApplicability(queryRunner, filtersVar, groupByVar); + if (filterValues.length) { + const nonApplicableFilters = filterValues.filter((filter) => { + const result = applicability?.find((entry) => entry.key === filter.key && entry.origin === filter.origin); + return result && !result.applicable; + }); + labels.push(...nonApplicableFilters.map((filter) => `${filter.key} ${filter.operator} ${filter.value}`)); + } + + if (groupByValues.length) { + const nonApplicableKeys = groupByValues + .filter((groupByKey) => { + const result = applicability?.find((entry) => entry.key === groupByKey); + return result && !result.applicable; + }) + .map((key) => String(key)); + + labels.push(...nonApplicableKeys); + } + + setNonApplicable(labels); + }; + + fetchApplicability(); + }, [ + filterKey, + groupByKey, + filtersVar, + groupByVar, + queryRunner, + filtersState?.filters, + groupByState?.value, + filtersState?.originFilters, + ]); + + useLayoutEffect(() => { + if (!nonApplicable.length) { + setVisibleCount(0); + return; + } + + setVisibleCount(calculateVisibleCount(nonApplicable, containerWidth, theme)); + }, [containerWidth, nonApplicable, theme]); + + if (nonApplicable.length === 0) { + return null; + } + + const visibleFilters = nonApplicable.slice(0, visibleCount); + const remainingCount = nonApplicable.length - visibleCount; + const hasOverflow = remainingCount > 0; + const remainingFilters = nonApplicable.slice(visibleCount); + + return ( +
+ {visibleFilters.map((filter, index) => ( +
+ {filter} +
+ ))} + {hasOverflow && ( + +
+{remainingCount}
+
+ )} +
+ ); +} + +function getPillWidth(label: string) { + return Math.ceil(measureText(label, FONT_SIZE).width) + GAP_SIZE; +} + +function calculateVisibleCount(labels: string[], containerWidth: number, theme: GrafanaTheme2) { + let usedWidth = 0; + let visible = 0; + + for (let i = 0; i < labels.length; i++) { + const pillWidth = getPillWidth(labels[i]); + const gapBefore = visible > 0 ? GAP_SIZE : 0; + const nextWidth = usedWidth + gapBefore + pillWidth; + const remaining = labels.length - (i + 1); + + if (remaining > 0) { + const overflowWidth = getPillWidth(`+${remaining}`); + if (nextWidth + GAP_SIZE + overflowWidth > containerWidth) { + break; + } + } else if (nextWidth > containerWidth) { + break; + } + + usedWidth = nextWidth; + visible++; + } + + return visible; +} + +function getStyles(theme: GrafanaTheme2) { + return { + container: css({ + display: 'flex', + flexWrap: 'nowrap', + gap: theme.spacing(1), + width: '100%', + overflow: 'hidden', + }), + pill: css({ + padding: theme.spacing(0.2, 0.4), + borderRadius: theme.shape.radius.default, + whiteSpace: 'nowrap', + flexShrink: 0, + fontSize: theme.typography.bodySmall.fontSize, + }), + disabledPill: css({ + background: theme.colors.action.selected, + color: theme.colors.text.disabled, + border: 0, + '&:hover': { + background: theme.colors.action.selected, + }, + }), + strikethrough: css({ + textDecoration: 'line-through', + }), + }; +} diff --git a/public/app/features/dashboard-scene/scene/VizPanelSubHeader.test.tsx b/public/app/features/dashboard-scene/scene/VizPanelSubHeader.test.tsx new file mode 100644 index 00000000000..e05e9b20da4 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/VizPanelSubHeader.test.tsx @@ -0,0 +1,184 @@ +import { of } from 'rxjs'; + +import { DataQueryRequest, DataSourceApi, LoadingState } from '@grafana/data'; +import { getPanelPlugin } from '@grafana/data/test'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { + AdHocFiltersVariable, + GroupByVariable, + SceneDataTransformer, + SceneQueryRunner, + SceneVariableSet, + VizPanel, + VizPanelState, +} from '@grafana/scenes'; + +import { activateFullSceneTree } from '../utils/test-utils'; + +import { DashboardScene } from './DashboardScene'; +import { VizPanelSubHeader } from './VizPanelSubHeader'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; + +const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => { + return of({ + state: LoadingState.Loading, + series: [], + timeRange: request.range, + }); +}); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => { + return runRequestMock(ds, request); + }, + getDataSourceSrv: () => ({ + get: jest.fn().mockResolvedValue({ + getRef: () => ({ uid: 'ds-1', type: 'test' }), + }), + getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds-1', type: 'test' }), + }), + getPluginImportUtils: () => ({ + getPanelPluginFromCache: jest.fn(() => undefined), + }), +})); + +setPluginImportUtils({ + importPanelPlugin: () => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: () => undefined, +}); + +describe('VizPanelSubHeader', () => { + it('renders when the drilldown variables apply to the panel', async () => { + const { subHeader } = await buildScene(); + + expect(subHeader.state.supportsApplicability).toBe(true); + }); + + it('renders only one drilldown var is set', async () => { + const { subHeader } = await buildScene({ noGroupBy: true }); + + expect(subHeader.state.supportsApplicability).toBe(true); + }); + + it('does not render when applicability is disabled', async () => { + const { subHeader } = await buildScene({ applicabilityEnabled: false }); + + expect(subHeader.state.supportsApplicability).toBe(false); + }); + + it('does not render when the datasource uid does not match', async () => { + const { subHeader } = await buildScene({ + variableDatasourceUid: 'other-ds', + }); + + expect(subHeader.state.supportsApplicability).toBe(false); + }); + + it('no longer renders if variable ds changes to a different type', async () => { + const { subHeader, adhocFiltersVariable } = await buildScene({ + noGroupBy: true, + }); + + expect(subHeader.state.supportsApplicability).toBe(true); + + adhocFiltersVariable.setState({ datasource: { uid: 'ds-2' } }); + + expect(subHeader.state.supportsApplicability).toBe(false); + }); + + it('no longers renders if variable applicability becomes disabled', async () => { + const { subHeader, adhocFiltersVariable } = await buildScene({ + noGroupBy: true, + }); + + expect(subHeader.state.supportsApplicability).toBe(true); + + adhocFiltersVariable.setState({ applicabilityEnabled: false }); + + expect(subHeader.state.supportsApplicability).toBe(false); + }); + + it('continues to render if one adhoc is disabled, but groupby remains active', async () => { + const { subHeader, adhocFiltersVariable } = await buildScene(); + + expect(subHeader.state.supportsApplicability).toBe(true); + + adhocFiltersVariable.setState({ applicabilityEnabled: false }); + + expect(subHeader.state.supportsApplicability).toBe(true); + }); + + it('stops rendering if queryRunner changes datasource to different one than vars', async () => { + const { subHeader, queryRunner } = await buildScene(); + + expect(subHeader.state.supportsApplicability).toBe(true); + + queryRunner.setState({ datasource: { uid: 'ds-2' } }); + + expect(subHeader.state.supportsApplicability).toBe(false); + }); +}); + +interface BuildSceneOptions { + applicabilityEnabled?: boolean; + variableDatasourceUid?: string; + noGroupBy?: boolean; +} + +async function buildScene(options?: BuildSceneOptions) { + const subHeader = new VizPanelSubHeader({}); + + const queryRunner = new SceneQueryRunner({ + datasource: { uid: 'ds-1' }, + queries: [{ refId: 'A', datasource: { uid: 'ds-1' } }], + }); + + const adhocFiltersVariable = new AdHocFiltersVariable({ + name: 'adhoc', + label: 'adhoc', + filters: [{ key: 'filter-1', operator: '=', value: 'value-1' }], + datasource: { uid: options?.variableDatasourceUid ?? 'ds-1' }, + applicabilityEnabled: options?.applicabilityEnabled ?? true, + }); + + const groupByVariable = new GroupByVariable({ + name: 'group', + label: 'group', + value: ['groupBy'], + text: ['groupBy'], + options: [], + applicabilityEnabled: options?.applicabilityEnabled ?? true, + datasource: { uid: options?.variableDatasourceUid ?? 'ds-1' }, + }); + + const dataProvider = new SceneDataTransformer({ + $data: queryRunner, + transformations: [], + }); + + const panelState: VizPanelState = { + key: 'panel-1', + title: 'Panel A', + pluginId: 'timeseries', + subHeader, + $data: dataProvider, + options: {}, + fieldConfig: { defaults: {}, overrides: [] }, + }; + + const panel = new VizPanel(panelState); + + const scene = new DashboardScene({ + $variables: new SceneVariableSet({ + variables: options?.noGroupBy ? [adhocFiltersVariable] : [groupByVariable, adhocFiltersVariable], + }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), + }); + + activateFullSceneTree(scene); + + await new Promise((r) => setTimeout(r, 1)); + + return { subHeader, groupByVariable, adhocFiltersVariable, queryRunner }; +} diff --git a/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx b/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx new file mode 100644 index 00000000000..f2247661e04 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx @@ -0,0 +1,193 @@ +import { Unsubscribable } from 'rxjs'; + +import { + SceneComponentProps, + SceneObjectState, + SceneObjectBase, + sceneGraph, + AdHocFiltersVariable, + GroupByVariable, + SceneQueryRunner, + VizPanel, +} from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { verifyDrilldownApplicability } from '../utils/drilldownUtils'; + +import { PanelNonApplicableDrilldownsSubHeader } from './PanelNonApplicableDrilldownsSubHeader'; + +interface ApplicabilitySupportHelperState { + datasource: DataSourceRef | null; + applicabilityEnabled?: boolean; +} + +export interface VizPanelSubHeaderState extends SceneObjectState { + hideNonApplicableDrilldowns?: boolean; + supportsApplicability?: boolean; +} + +export class VizPanelSubHeader extends SceneObjectBase { + static Component = VizPanelSubHeaderRenderer; + + private _adHocVar?: AdHocFiltersVariable; + private _groupByVar?: GroupByVariable; + + private _adHocSub?: Unsubscribable; + private _groupBySub?: Unsubscribable; + + private _queryRunnerDatasource?: DataSourceRef; + + constructor(state: Partial) { + super({ + hideNonApplicableDrilldowns: state.hideNonApplicableDrilldowns ?? false, + ...state, + }); + + this.addActivationHandler(this._onActivate); + } + + private _onActivate = () => { + if (!this.parent || !(this.parent instanceof VizPanel)) { + throw new Error('VizPanelSubHeader can be used only with VizPanel'); + } + + if (!this.state.hideNonApplicableDrilldowns) { + this.subscribeToDrilldownVariableChanges(); + } + + return () => { + this._groupBySub?.unsubscribe(); + this._adHocSub?.unsubscribe(); + }; + }; + + private subscribeToDrilldownVariableChanges() { + const vars = sceneGraph.getVariables(this); + const queryRunner = this.getQueryRunner(); + + this._adHocVar = vars.state.variables.find((variable) => variable instanceof AdHocFiltersVariable); + this._groupByVar = vars.state.variables.find((variable) => variable instanceof GroupByVariable); + this._queryRunnerDatasource = queryRunner?.state.datasource; + + this.setDrilldownApplicabilitySupportHelper(); + + // keep track of queryRunner datasource updates andupdate rendering + this._subs.add( + queryRunner?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource) { + this._queryRunnerDatasource = n.datasource; + + this.setDrilldownApplicabilitySupportHelper(); + } + }) + ); + + // check when var set updates and search for drilldown vars + this._subs.add( + vars.subscribeToState((n) => { + this._adHocVar = n.variables.find((variable) => variable instanceof AdHocFiltersVariable); + this._groupByVar = n.variables.find((variable) => variable instanceof GroupByVariable); + + this.refreshDrilldownVarsSubscriptions(); + }) + ); + + // adhoc sub so if that changes, we potentially update rendering + this._adHocSub = this._adHocVar?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { + this.setDrilldownApplicabilitySupportHelper({ + datasource: n.datasource, + applicabilityEnabled: n.applicabilityEnabled, + }); + } + }); + + // same for groupBy + this._groupBySub = this._groupByVar?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { + this.setDrilldownApplicabilitySupportHelper(undefined, { + datasource: n.datasource, + applicabilityEnabled: n.applicabilityEnabled, + }); + } + }); + } + + private refreshDrilldownVarsSubscriptions() { + if (this._groupByVar) { + this._groupBySub?.unsubscribe(); + this._groupByVar?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { + this.setDrilldownApplicabilitySupportHelper(undefined, { + datasource: n.datasource, + applicabilityEnabled: n.applicabilityEnabled, + }); + } + }); + } + + if (this._adHocVar) { + this._adHocVar?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { + this.setDrilldownApplicabilitySupportHelper({ + datasource: n.datasource, + applicabilityEnabled: n.applicabilityEnabled, + }); + } + }); + } + } + + private setDrilldownApplicabilitySupportHelper( + adHocData?: ApplicabilitySupportHelperState, + groupByData?: ApplicabilitySupportHelperState + ) { + this.setState({ + supportsApplicability: + verifyDrilldownApplicability( + this, + this._queryRunnerDatasource, + adHocData?.datasource ?? this._adHocVar?.state.datasource ?? null, + adHocData?.applicabilityEnabled ?? this._adHocVar?.state.applicabilityEnabled ?? false + ) || + verifyDrilldownApplicability( + this, + this._queryRunnerDatasource, + groupByData?.datasource ?? this._groupByVar?.state.datasource ?? null, + groupByData?.applicabilityEnabled ?? this._groupByVar?.state.applicabilityEnabled ?? false + ), + }); + } + + public getQueryRunner() { + const panel = this.parent; + const dataObject = panel ? sceneGraph.getData(panel) : undefined; + const queryRunner = dataObject?.state.$data; + + if (!queryRunner || !(queryRunner instanceof SceneQueryRunner)) { + return null; + } + + return queryRunner; + } +} + +export function VizPanelSubHeaderRenderer({ model }: SceneComponentProps) { + const { supportsApplicability, hideNonApplicableDrilldowns } = model.useState(); + const variables = sceneGraph.getVariables(model); + const adhocFiltersVar = variables.state.variables.find((variable) => variable instanceof AdHocFiltersVariable); + const groupByVar = variables.state.variables.find((variable) => variable instanceof GroupByVariable); + const queryRunner = model.getQueryRunner(); + + if (!queryRunner || hideNonApplicableDrilldowns || !supportsApplicability) { + return null; + } + + return ( + + ); +} diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts index 566bb7c367a..512631961d3 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts @@ -31,6 +31,7 @@ import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior'; import { PanelNotices } from '../../scene/PanelNotices'; +import { VizPanelSubHeader } from '../../scene/VizPanelSubHeader'; import { AutoGridItem } from '../../scene/layout-auto-grid/AutoGridItem'; import { DashboardGridItem } from '../../scene/layout-default/DashboardGridItem'; import { PanelTimeRange } from '../../scene/panel-timerange/PanelTimeRange'; @@ -70,6 +71,9 @@ export function buildVizPanel(panel: PanelKind, id?: number): VizPanel { seriesLimit: config.panelSeriesLimit, $data: createPanelDataProvider(panel), titleItems, + subHeader: new VizPanelSubHeader({ + hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns, + }), $behaviors: [], extendPanelContext: setDashboardPanelContext, }; @@ -106,6 +110,9 @@ export function buildLibraryPanel(panel: LibraryPanelKind, id?: number): VizPane const vizPanelState: VizPanelState = { key: getVizPanelKeyForPanelId(id ?? panel.spec.id), titleItems, + subHeader: new VizPanelSubHeader({ + hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns, + }), seriesLimit: config.panelSeriesLimit, $behaviors: [ new LibraryPanelBehavior({ diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 25cb5d55e44..41dcc938222 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -45,6 +45,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { PanelNotices } from '../scene/PanelNotices'; +import { VizPanelSubHeader } from '../scene/VizPanelSubHeader'; import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; @@ -440,6 +441,9 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem { hoverHeaderOffset: 0, $data: createPanelDataProvider(panel), titleItems, + subHeader: new VizPanelSubHeader({ + hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns, + }), $behaviors: [], extendPanelContext: setDashboardPanelContext, _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), diff --git a/public/app/features/dashboard-scene/utils/drilldownUtils.test.ts b/public/app/features/dashboard-scene/utils/drilldownUtils.test.ts new file mode 100644 index 00000000000..eb8a30baea3 --- /dev/null +++ b/public/app/features/dashboard-scene/utils/drilldownUtils.test.ts @@ -0,0 +1,236 @@ +import type { DrilldownsApplicability } from '@grafana/data'; +import { DataSourceSrv, getDataSourceSrv } from '@grafana/runtime'; +import { + AdHocFiltersVariable, + GroupByVariable, + SceneQueryRunner, + SceneDataTransformer, + SceneVariableSet, + VizPanel, + VizPanelState, +} from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { VizPanelSubHeader } from '../scene/VizPanelSubHeader'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; + +import { getDrilldownApplicability, verifyDrilldownApplicability } from './drilldownUtils'; + +jest.mock('@grafana/runtime', () => { + const actual = jest.requireActual('@grafana/runtime'); + return { + ...actual, + getDataSourceSrv: jest.fn(), + }; +}); + +const getDataSourceSrvMock = getDataSourceSrv as jest.MockedFunction; + +// Helper to create partial DataSourceSrv mock +const createMockDataSourceSrv = (overrides: Partial = {}): DataSourceSrv => ({ + get: jest.fn(), + getList: jest.fn(), + getInstanceSettings: jest.fn(), + reload: jest.fn(), + registerRuntimeDataSource: jest.fn(), + ...overrides, +}); + +describe('verifyDrilldownApplicability', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('returns true when applicability is enabled and datasources match', () => { + const subHeader = new VizPanelSubHeader({}); + + const result = verifyDrilldownApplicability( + subHeader, + { uid: 'ds-1' } as DataSourceRef, + { uid: 'ds-1' } as DataSourceRef, + true + ); + + expect(result).toBe(true); + }); + + it('returns false when datasources differ or applicability disabled', () => { + const subHeader = new VizPanelSubHeader({}); + + const mismatch = verifyDrilldownApplicability( + subHeader, + { uid: 'ds-1' } as DataSourceRef, + { uid: 'ds-2' } as DataSourceRef, + true + ); + + expect(mismatch).toBe(false); + + const disabled = verifyDrilldownApplicability( + subHeader, + { uid: 'ds-1' } as DataSourceRef, + { uid: 'ds-1' } as DataSourceRef, + false + ); + + expect(disabled).toBe(false); + }); +}); + +describe('getDrilldownApplicability', () => { + beforeEach(() => { + const applicabilityFn = jest.fn().mockResolvedValue([]); + getDataSourceSrvMock.mockReturnValue( + createMockDataSourceSrv({ + get: jest.fn().mockResolvedValue({ getDrilldownsApplicability: applicabilityFn }), + getInstanceSettings: jest.fn(), + }) + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns undefined when no drilldown variables are provided', async () => { + const queryRunner = new SceneQueryRunner({ + datasource: { uid: 'ds-1' }, + queries: [{ refId: 'A' }], + }); + + const result = await getDrilldownApplicability(queryRunner); + + expect(result).toBeUndefined(); + expect(getDataSourceSrvMock).not.toHaveBeenCalled(); + }); + + it('returns undefined when datasource lacks getDrilldownsApplicability', async () => { + getDataSourceSrvMock.mockReturnValue( + createMockDataSourceSrv({ + get: jest.fn().mockResolvedValue({}), + getInstanceSettings: jest.fn(), + }) + ); + + const { queryRunner, groupByVariable } = buildScene(); + + const result = await getDrilldownApplicability(queryRunner, undefined, groupByVariable); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when drilldown variables use a different datasource', async () => { + const applicabilityFn = jest.fn().mockResolvedValue([]); + getDataSourceSrvMock.mockReturnValue( + createMockDataSourceSrv({ + get: jest.fn().mockResolvedValue({ getDrilldownsApplicability: applicabilityFn }), + getInstanceSettings: jest.fn(), + }) + ); + + const { queryRunner, adhocFiltersVariable, groupByVariable } = buildScene({ + datasourceUid: 'ds-query', + filtersDatasourceUid: 'ds-other', + groupByDatasourceUid: 'ds-other', + }); + + const result = await getDrilldownApplicability(queryRunner, adhocFiltersVariable, groupByVariable); + + expect(result).toBeUndefined(); + expect(applicabilityFn).not.toHaveBeenCalled(); + }); + + it('returns applicability data when datasource and variables align', async () => { + const applicability: DrilldownsApplicability[] = [{ key: 'region', applicable: true }]; + const getApplicability = jest.fn().mockResolvedValue(applicability); + getDataSourceSrvMock.mockReturnValue( + createMockDataSourceSrv({ + get: jest.fn().mockResolvedValue({ getDrilldownsApplicability: getApplicability }), + getInstanceSettings: jest.fn(), + }) + ); + + const { queryRunner, adhocFiltersVariable, groupByVariable } = buildScene({ + datasourceUid: 'ds-apply', + filtersDatasourceUid: 'ds-apply', + groupByDatasourceUid: 'ds-apply', + groupValues: ['region', 'instance'], + }); + + const result = await getDrilldownApplicability(queryRunner, adhocFiltersVariable, groupByVariable); + + expect(getApplicability).toHaveBeenCalledTimes(1); + const payload = getApplicability.mock.calls[0][0]; + expect(payload.groupByKeys).toEqual(['region', 'instance']); + expect(payload.filters).toEqual([]); + expect(payload.queries).toBe(queryRunner.state.data?.request?.targets); + expect(payload.timeRange).toBeDefined(); + expect(result).toBe(applicability); + }); +}); + +interface BuildSceneOptions { + datasourceUid?: string; + groupByDatasourceUid?: string; + filtersDatasourceUid?: string; + groupValues?: Array; +} + +function buildScene({ + datasourceUid = 'ds-1', + groupByDatasourceUid = datasourceUid, + filtersDatasourceUid = datasourceUid, + groupValues = ['groupBy'], +}: BuildSceneOptions = {}) { + const subHeader = new VizPanelSubHeader({}); + + const queryRunner = new SceneQueryRunner({ + datasource: { uid: datasourceUid }, + queries: [{ refId: 'A', datasource: { uid: datasourceUid } }], + }); + + const adhocFiltersVariable = new AdHocFiltersVariable({ + name: 'adhoc', + label: 'adhoc', + filters: [], + datasource: { uid: filtersDatasourceUid }, + applicabilityEnabled: true, + }); + + const groupByVariable = new GroupByVariable({ + name: 'group', + label: 'group', + value: groupValues, + text: groupValues.map((val) => String(val)), + options: [], + applicabilityEnabled: true, + datasource: { uid: groupByDatasourceUid }, + }); + + const dataProvider = new SceneDataTransformer({ + $data: queryRunner, + transformations: [], + }); + + const panelState: VizPanelState = { + key: 'panel-1', + title: 'Panel A', + pluginId: 'timeseries', + subHeader, + $data: dataProvider, + options: {}, + fieldConfig: { defaults: {}, overrides: [] }, + }; + + const panel = new VizPanel(panelState); + + new DashboardScene({ + $variables: new SceneVariableSet({ + variables: [groupByVariable, adhocFiltersVariable], + }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), + }); + + return { queryRunner, adhocFiltersVariable, groupByVariable }; +} diff --git a/public/app/features/dashboard-scene/utils/drilldownUtils.ts b/public/app/features/dashboard-scene/utils/drilldownUtils.ts new file mode 100644 index 00000000000..2ff0ecf7c6e --- /dev/null +++ b/public/app/features/dashboard-scene/utils/drilldownUtils.ts @@ -0,0 +1,75 @@ +import { DrilldownsApplicability } from '@grafana/data'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { AdHocFiltersVariable, GroupByVariable, sceneGraph, SceneObject, SceneQueryRunner } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +export function verifyDrilldownApplicability( + sourceObject: SceneObject, + queriesDataSource: DataSourceRef | undefined, + drilldownDatasource: DataSourceRef | null, + isApplicabilityEnabled?: boolean +): boolean { + const datasourceUid = sceneGraph.interpolate(sourceObject, queriesDataSource?.uid); + + return Boolean( + isApplicabilityEnabled && datasourceUid === sceneGraph.interpolate(sourceObject, drilldownDatasource?.uid) + ); +} + +export async function getDrilldownApplicability( + queryRunner: SceneQueryRunner, + filtersVar?: AdHocFiltersVariable, + groupByVar?: GroupByVariable +): Promise { + //if no drilldown vars return + if (!filtersVar && !groupByVar) { + return; + } + + const datasource = queryRunner.state.datasource; + const queries = queryRunner.state.data?.request?.targets; + + const ds = await getDataSourceSrv().get(datasource?.uid); + + // return if method not implemented + if (!ds.getDrilldownsApplicability) { + return; + } + + const dsUid = sceneGraph.interpolate(queryRunner, datasource?.uid); + const timeRange = sceneGraph.getTimeRange(queryRunner).state.value; + const groupByKeys = []; + const filters = []; + + const hasGroupByApplicability = + groupByVar && dsUid === sceneGraph.interpolate(groupByVar, groupByVar?.state.datasource?.uid); + const hasFiltersApplicability = + filtersVar && dsUid === sceneGraph.interpolate(filtersVar, filtersVar.state?.datasource?.uid); + + // if neither vars use the ds from the queries, return + if (!hasGroupByApplicability && !hasFiltersApplicability) { + return; + } + + if (hasGroupByApplicability) { + groupByKeys.push( + ...(Array.isArray(groupByVar.state.value) + ? groupByVar.state.value.map((v) => String(v)) + : groupByVar.state.value + ? [String(groupByVar.state.value)] + : []) + ); + } + + if (hasFiltersApplicability) { + filters.push(...filtersVar.state.filters, ...(filtersVar.state.originFilters ?? [])); + } + + return await ds.getDrilldownsApplicability({ + groupByKeys, + filters, + queries, + timeRange, + scopes: sceneGraph.getScopes(queryRunner), + }); +} diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 36ccabdca23..7691b52f3b8 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -24,6 +24,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel'; +import { VizPanelSubHeader } from '../scene/VizPanelSubHeader'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; @@ -267,6 +268,9 @@ export function getDefaultVizPanel(): VizPanel { titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], hoverHeaderOffset: 0, $behaviors: [], + subHeader: new VizPanelSubHeader({ + hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns, + }), extendPanelContext: setDashboardPanelContext, menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior], diff --git a/yarn.lock b/yarn.lock index 1e810d79f5c..778173eec14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1473,7 +1473,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7": version: 7.28.4 resolution: "@babel/runtime@npm:7.28.4" checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163 @@ -2324,15 +2324,15 @@ __metadata: languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:2.3.6": - version: 2.3.6 - resolution: "@formatjs/ecma402-abstract@npm:2.3.6" +"@formatjs/ecma402-abstract@npm:2.3.4": + version: 2.3.4 + resolution: "@formatjs/ecma402-abstract@npm:2.3.4" dependencies: "@formatjs/fast-memoize": "npm:2.2.7" - "@formatjs/intl-localematcher": "npm:0.6.2" + "@formatjs/intl-localematcher": "npm:0.6.1" decimal.js: "npm:^10.4.3" tslib: "npm:^2.8.0" - checksum: 10/30b1b5cd6b62ba46245f934429936592df5500bc1b089dc92dd49c826757b873dd92c305dcfe370701e4df6b057bf007782113abb9b65db550d73be4961718bc + checksum: 10/573971ffc291096a4b9fcc80b4708124e89bf2e3ac50e0f78b41eb797e9aa1b842f4dc3665e4467a853c738386821769d9e40408a1d25bc73323a1f057a16cf2 languageName: node linkType: hard @@ -2376,13 +2376,13 @@ __metadata: linkType: hard "@formatjs/intl-durationformat@npm:^0.7.0": - version: 0.7.6 - resolution: "@formatjs/intl-durationformat@npm:0.7.6" + version: 0.7.4 + resolution: "@formatjs/intl-durationformat@npm:0.7.4" dependencies: - "@formatjs/ecma402-abstract": "npm:2.3.6" - "@formatjs/intl-localematcher": "npm:0.6.2" + "@formatjs/ecma402-abstract": "npm:2.3.4" + "@formatjs/intl-localematcher": "npm:0.6.1" tslib: "npm:^2.8.0" - checksum: 10/442236ba85bcd9cb7296c43a708271fa09f110b1ca9d5899066d00812fc2965eaeaec6b5240be421b80daba62860352131088449ba0fcd2061f671cec6240f0b + checksum: 10/d62273ecd635475ca91e9b501301f3f396403fa91b584c550734b19b2d194ba1316b27303fed985c1d42ae933d54eb220da6540edfdf376b0d9371ecfd0d4e15 languageName: node linkType: hard @@ -2395,12 +2395,12 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-localematcher@npm:0.6.2": - version: 0.6.2 - resolution: "@formatjs/intl-localematcher@npm:0.6.2" +"@formatjs/intl-localematcher@npm:0.6.1": + version: 0.6.1 + resolution: "@formatjs/intl-localematcher@npm:0.6.1" dependencies: tslib: "npm:^2.8.0" - checksum: 10/eb12a7f5367bbecdfafc20d7f005559ce840f420e970f425c5213d35e94e86dfe75bde03464971a26494bf8427d4961269db22ecad2834f2a19d888b5d9cc064 + checksum: 10/c7b3bc8395d18670677f207b2fd107561fff5d6394a9b4273c29e0bea920300ec3a2eefead600ebb7761c04a770cada28f78ac059f84d00520bfb57a9db36998 languageName: node linkType: hard @@ -3596,11 +3596,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:^6.46.0": - version: 6.46.1 - resolution: "@grafana/scenes-react@npm:6.46.1" +"@grafana/scenes-react@npm:6.47.1": + version: 6.47.1 + resolution: "@grafana/scenes-react@npm:6.47.1" dependencies: - "@grafana/scenes": "npm:6.46.1" + "@grafana/scenes": "npm:6.47.1" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3612,7 +3612,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/47007ae998dd52c774f1b53cc1cbd62cbff6c077886ba36381f371454918bd7fbe9ea9373e021a38ba0144e507c8c51a435a2be1aa8250a36c37d7bbfa72fb9d + checksum: 10/dc20f9ee80eaf648665f7449e3ccb3b640a931f8f4a1be89599dce17eb0f52e763e3a603a4d491d9886b3e6cdf2ad3634124252c223315917206200f7cd6da16 languageName: node linkType: hard @@ -3642,9 +3642,9 @@ __metadata: languageName: node linkType: hard -"@grafana/scenes@npm:6.46.1, @grafana/scenes@npm:^6.46.0": - version: 6.46.1 - resolution: "@grafana/scenes@npm:6.46.1" +"@grafana/scenes@npm:6.47.1": + version: 6.47.1 + resolution: "@grafana/scenes@npm:6.47.1" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3664,7 +3664,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/e72a90833a94307c6a4633d9f637e407f681d2be9fa27938158b0bdbb9520e31a3ff804eaab3f5e146673af5a21822b6a8279b4b8fd2ead79b94e2593e366144 + checksum: 10/bc0c76258955058e7493b04e7cdd5d59dcc4159adf06da0837e992716ea15700b54f8403614df04326350363dc3344fb2602a2e8f7807724571659b4bd95aded languageName: node linkType: hard @@ -4118,13 +4118,20 @@ __metadata: languageName: node linkType: hard -"@inquirer/figures@npm:^1.0.14, @inquirer/figures@npm:^1.0.3": +"@inquirer/figures@npm:^1.0.14": version: 1.0.14 resolution: "@inquirer/figures@npm:1.0.14" checksum: 10/39df361eb607cea5a020d457e25f9c6aee3a1de8975c6295a4b3bfe86ba7e7f7bfbefa6a52b145b1790f2690e5c8f10eb822e5bc764aff7ba00a6cd24eec5a25 languageName: node linkType: hard +"@inquirer/figures@npm:^1.0.3": + version: 1.0.11 + resolution: "@inquirer/figures@npm:1.0.11" + checksum: 10/357ddd2e83718bc3c9189d518b93fd69099af9c860354df9a5ac0ec024cb5df1228ae4608d2de7625624d2adcd047db813f29426a610eaae7b9e449f8c753c6b + languageName: node + linkType: hard + "@inquirer/input@npm:^4.2.5": version: 4.2.5 resolution: "@inquirer/input@npm:4.2.5" @@ -6096,9 +6103,9 @@ __metadata: linkType: hard "@openfeature/core@npm:^1.9.0": - version: 1.9.1 - resolution: "@openfeature/core@npm:1.9.1" - checksum: 10/6099e16b1b4cd6e3c45c05ab4acd44c9cb4ab501b676ab6f3e77f0be1b56abc7506c5629187381333a02975d315d831e0905582435b356d9676255e72a465899 + version: 1.9.0 + resolution: "@openfeature/core@npm:1.9.0" + checksum: 10/c6d20edc09053afd99752fe46d8328158680950bca4b86679f67f79249d7226eea127b31fffdc38e26ecb729f2bab5a4a5a7c1db708ae76b7fbbac68cd56f094 languageName: node linkType: hard @@ -6123,11 +6130,11 @@ __metadata: linkType: hard "@openfeature/web-sdk@npm:^1.6.1": - version: 1.7.1 - resolution: "@openfeature/web-sdk@npm:1.7.1" + version: 1.6.1 + resolution: "@openfeature/web-sdk@npm:1.6.1" peerDependencies: "@openfeature/core": ^1.9.0 - checksum: 10/358cabfddda8bf67a9bd96ce28f0f84a3218416907f8df4f2a58750ee840d164d86a1c859e8c9aa92d132fa28fcf72a0f17ceece798ff8af297d3e1c1e30cdab + checksum: 10/8bd7d1ea386e21cdd7492cab2fd1d2b138b4e6a376a4c0a40244633e5955f6452039bc2633fc5230bd7b494506a4137ba7210d40850634f9618f77a0ee435f9d languageName: node linkType: hard @@ -7107,7 +7114,7 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:2.10.1, @reduxjs/toolkit@npm:^2.9.0": +"@reduxjs/toolkit@npm:2.10.1": version: 2.10.1 resolution: "@reduxjs/toolkit@npm:2.10.1" dependencies: @@ -7129,6 +7136,28 @@ __metadata: languageName: node linkType: hard +"@reduxjs/toolkit@npm:^2.9.0": + version: 2.9.2 + resolution: "@reduxjs/toolkit@npm:2.9.2" + dependencies: + "@standard-schema/spec": "npm:^1.0.0" + "@standard-schema/utils": "npm:^0.3.0" + immer: "npm:^10.0.3" + redux: "npm:^5.0.1" + redux-thunk: "npm:^3.1.0" + reselect: "npm:^5.1.0" + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + checksum: 10/7a8fa57086bc44dafe2f04428d6557edad46ee6eb46b2fe80468ebf934a441a7d4d750b50a6d41bc360d9ab61baadb1dddb5a8f556cfac68977f56e832f509a2 + languageName: node + linkType: hard + "@remix-run/router@npm:1.19.1": version: 1.19.1 resolution: "@remix-run/router@npm:1.19.1" @@ -9007,7 +9036,7 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.13.19, @swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22": +"@swc/core@npm:1.13.19": version: 1.13.19 resolution: "@swc/core@npm:1.13.19" dependencies: @@ -9053,7 +9082,7 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.13.3": +"@swc/core@npm:1.13.3, @swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22": version: 1.13.3 resolution: "@swc/core@npm:1.13.3" dependencies: @@ -12287,7 +12316,18 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1, axios@npm:^1.10.0, axios@npm:^1.12.2, axios@npm:^1.6.1, axios@npm:^1.8.3": +"axios@npm:^1, axios@npm:^1.10.0, axios@npm:^1.6.1, axios@npm:^1.8.3": + version: 1.12.2 + resolution: "axios@npm:1.12.2" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10/886a79770594eaad76493fecf90344b567bd956240609b5dcd09bd0afe8d3e6f1ad6d3257a93a483b6192b409d4b673d9515a34619e3e3ed1b2c0ec2a83b20ba + languageName: node + linkType: hard + +"axios@npm:^1.12.2": version: 1.13.2 resolution: "axios@npm:1.13.2" dependencies: @@ -13211,7 +13251,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:5.6.2, chalk@npm:^5.2.0, chalk@npm:^5.3.0, chalk@npm:^5.4.1, chalk@npm:^5.6.2": +"chalk@npm:5.6.2, chalk@npm:^5.6.2": version: 5.6.2 resolution: "chalk@npm:5.6.2" checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0 @@ -13249,6 +13289,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.2.0, chalk@npm:^5.3.0, chalk@npm:^5.4.1": + version: 5.4.1 + resolution: "chalk@npm:5.4.1" + checksum: 10/29df3ffcdf25656fed6e95962e2ef86d14dfe03cd50e7074b06bad9ffbbf6089adbb40f75c00744d843685c8d008adaf3aed31476780312553caf07fa86e5bc7 + languageName: node + linkType: hard + "chance@npm:^1.1.13": version: 1.1.13 resolution: "chance@npm:1.1.13" @@ -13852,7 +13899,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:14.0.1, commander@npm:~14.0.0": +"commander@npm:14.0.1": version: 14.0.1 resolution: "commander@npm:14.0.1" checksum: 10/783115e9403caeca29c0fcbd4e0358f70c67760e4e4933f3453fcdd5ddba2ec44173c8da5213d7ce5e404f51c7e71203a42c548164dbe27b668b32a8981577f1 @@ -13929,6 +13976,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:~14.0.0": + version: 14.0.0 + resolution: "commander@npm:14.0.0" + checksum: 10/c05418bfc35a3e8b5c67bd9f75f5b773f386f9b85f83e70e7c926047f270929cb06cf13cd68f387dd6e7e23c6157de8171b28ba606abd3e6256028f1f789becf + languageName: node + linkType: hard + "comment-parser@npm:1.4.1": version: 1.4.1 resolution: "comment-parser@npm:1.4.1" @@ -16176,7 +16230,16 @@ __metadata: languageName: node linkType: hard -"enquirer@npm:^2.3.6, enquirer@npm:^2.4.1": +"enquirer@npm:^2.3.6, enquirer@npm:~2.3.6": + version: 2.3.6 + resolution: "enquirer@npm:2.3.6" + dependencies: + ansi-colors: "npm:^4.1.1" + checksum: 10/751d14f037eb7683997e696fb8d5fe2675e0b0cde91182c128cf598acf3f5bd9005f35f7c2a9109e291140af496ebec237b6dac86067d59a9b44f3688107f426 + languageName: node + linkType: hard + +"enquirer@npm:^2.4.1": version: 2.4.1 resolution: "enquirer@npm:2.4.1" dependencies: @@ -16186,15 +16249,6 @@ __metadata: languageName: node linkType: hard -"enquirer@npm:~2.3.6": - version: 2.3.6 - resolution: "enquirer@npm:2.3.6" - dependencies: - ansi-colors: "npm:^4.1.1" - checksum: 10/751d14f037eb7683997e696fb8d5fe2675e0b0cde91182c128cf598acf3f5bd9005f35f7c2a9109e291140af496ebec237b6dac86067d59a9b44f3688107f426 - languageName: node - linkType: hard - "ensure-posix-path@npm:^1.1.0": version: 1.1.1 resolution: "ensure-posix-path@npm:1.1.1" @@ -18604,19 +18658,19 @@ __metadata: languageName: node linkType: hard -"glob@npm:*": - version: 12.0.0 - resolution: "glob@npm:12.0.0" +"glob@npm:*, glob@npm:11.0.3": + version: 11.0.3 + resolution: "glob@npm:11.0.3" dependencies: foreground-child: "npm:^3.3.1" jackspeak: "npm:^4.1.1" - minimatch: "npm:^10.1.1" + minimatch: "npm:^10.0.3" minipass: "npm:^7.1.2" package-json-from-dist: "npm:^1.0.0" path-scurry: "npm:^2.0.0" bin: glob: dist/esm/bin.mjs - checksum: 10/6e21b3f1f1fa635836d45e54bbe50704884cc3e310e0cc011cfb5429db65a030e12936d99b07e66236370efe45dc8c8b26fa5334dbf555d6f8709e0315c77c30 + checksum: 10/2ae536c1360c0266b523b2bfa6aadc10144a8b7e08869b088e37ac3c27cd30774f82e4bfb291cde796776e878f9e13200c7ff44010eb7054e00f46f649397893 languageName: node linkType: hard @@ -18651,22 +18705,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:11.0.3": - version: 11.0.3 - resolution: "glob@npm:11.0.3" - dependencies: - foreground-child: "npm:^3.3.1" - jackspeak: "npm:^4.1.1" - minimatch: "npm:^10.0.3" - minipass: "npm:^7.1.2" - package-json-from-dist: "npm:^1.0.0" - path-scurry: "npm:^2.0.0" - bin: - glob: dist/esm/bin.mjs - checksum: 10/2ae536c1360c0266b523b2bfa6aadc10144a8b7e08869b088e37ac3c27cd30774f82e4bfb291cde796776e878f9e13200c7ff44010eb7054e00f46f649397893 - languageName: node - linkType: hard - "glob@npm:11.1.0, glob@npm:^11.0.0": version: 11.1.0 resolution: "glob@npm:11.1.0" @@ -18926,8 +18964,8 @@ __metadata: "@grafana/plugin-ui": "npm:^0.11.1" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:^6.46.0" - "@grafana/scenes-react": "npm:^6.46.0" + "@grafana/scenes": "npm:6.47.1" + "@grafana/scenes-react": "npm:6.47.1" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/test-utils": "workspace:*" @@ -20051,22 +20089,22 @@ __metadata: linkType: hard "i18next@npm:^23.5.1 || ^24.2.0": - version: 24.2.3 - resolution: "i18next@npm:24.2.3" + version: 24.2.2 + resolution: "i18next@npm:24.2.2" dependencies: - "@babel/runtime": "npm:^7.26.10" + "@babel/runtime": "npm:^7.23.2" peerDependencies: typescript: ^5 peerDependenciesMeta: typescript: optional: true - checksum: 10/6c73d964f2a98b1aa2c2717fe6da66fc265bcbbc5fcd52b2bfff51ff013d30d4f7d7449c4eb7f464d27af43e2e73f2e7f1d46a144d731bd3bdb1385d4c199e4c + checksum: 10/f66ed9e56d9412e59502f5df39163631daf9f1264774732fb21edbd66a528ca7a6b67dc2e2aec95683c6c7956e42c651587a54bd8ee082bd12008880ce6cd326 languageName: node linkType: hard "i18next@npm:^25.0.0, i18next@npm:^25.5.2": - version: 25.6.2 - resolution: "i18next@npm:25.6.2" + version: 25.5.2 + resolution: "i18next@npm:25.5.2" dependencies: "@babel/runtime": "npm:^7.27.6" peerDependencies: @@ -20074,7 +20112,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/bb7f3e200caac72bacaead766287dbad5ed5945430bd9f2be1c14fbccd49cf78ab751f78cf53cd0be3ec063bc4c40837d7d771f7b14d50fcb9022e1ed5d6e72f + checksum: 10/8d52e82386722a228f4465aa5cf39d82bd9861ea9cc8b31d7cc4d22d5e70b2740ee006068b09f486d5273cabd227a2ac14f37d68fab26344524200083f679bcd languageName: node linkType: hard @@ -20169,7 +20207,7 @@ __metadata: languageName: node linkType: hard -"immer@npm:10.2.0, immer@npm:^10.2.0": +"immer@npm:10.2.0, immer@npm:^10.0.3, immer@npm:^10.2.0": version: 10.2.0 resolution: "immer@npm:10.2.0" checksum: 10/d73e218c8f8ffbb39f9290dfafa478b94af73403dcf26b5672eef35233bb30f09ffe231f8a78a6c9cb442968510edd89e851776ec90a5ddfa82cee6db6b35137 @@ -23175,9 +23213,9 @@ __metadata: linkType: hard "lossless-json@npm:^4.1.1": - version: 4.3.0 - resolution: "lossless-json@npm:4.3.0" - checksum: 10/a984a882c79b6e62a917d0202518472c17587bdee002f1427d7ec61115f9fbdeb5f0baa9f0e9fff8d9dbacd17da6b6c910012b2dcab69dd33d151cb7c13d5a37 + version: 4.2.0 + resolution: "lossless-json@npm:4.2.0" + checksum: 10/b29cbf9a90b8f3108219ff410223a90a3a6ad14cf902eed4038571d9421c69ade7077fb59737e1ca20f3f404ddd68fad69be11365b0d79b4b61421b054759551 languageName: node linkType: hard @@ -23652,9 +23690,9 @@ __metadata: linkType: hard "micro-memoize@npm:^4.1.2": - version: 4.2.0 - resolution: "micro-memoize@npm:4.2.0" - checksum: 10/260e27a5c15809f7dc435a89a424d93e49ccca62b743fa96ee72f254264df58c95edacbd847a2899a6d7f8ad0daef188548a8b12add5ccff83e88869a50a762c + version: 4.1.3 + resolution: "micro-memoize@npm:4.1.3" + checksum: 10/4e9c7767911cc76ae9c9779584ec87844437af9446b295a01774640a732c2c7f91944794027f44625031f7330ab7f9147740d0a9fb612680d1d2d858dad43402 languageName: node linkType: hard @@ -26460,6 +26498,15 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.54.1": + version: 1.54.1 + resolution: "playwright-core@npm:1.54.1" + bin: + playwright-core: cli.js + checksum: 10/c0acfb7ecb48e9fb6c22f71298b966244bd4f8659093a798cc7f9a509deee22109560e44f2d507124e7718779640e0b4cb05a05c068b034405418d8740ec31ff + languageName: node + linkType: hard + "playwright-core@npm:1.56.1, playwright-core@npm:>=1.2.0": version: 1.56.1 resolution: "playwright-core@npm:1.56.1" @@ -26469,7 +26516,7 @@ __metadata: languageName: node linkType: hard -"playwright@npm:1.56.1, playwright@npm:^1.14.0": +"playwright@npm:1.56.1": version: 1.56.1 resolution: "playwright@npm:1.56.1" dependencies: @@ -26484,6 +26531,21 @@ __metadata: languageName: node linkType: hard +"playwright@npm:^1.14.0": + version: 1.54.1 + resolution: "playwright@npm:1.54.1" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.54.1" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10/ebad2924b589a4cfedf48288e67b4afc81047222e607321c9452a37610025e6e249bdfeff010c1c70cf9a9d35c0b9e5cb7934a58986f67928dd469d9fde1a035 + languageName: node + linkType: hard + "plop@npm:^4.0.1": version: 4.0.1 resolution: "plop@npm:4.0.1" From c2c443757dda6230b695526c27e78f47516ed0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Calisto?= Date: Fri, 21 Nov 2025 16:42:15 +0000 Subject: [PATCH 059/378] Unified Storage: allow rebuilding indexes for resource with a new grpc endpoint (#113748) * Unified Storage: allow rebuilding indexes for resource from a new grpc endpoint * remove log line * fix trace def * lint * fix after rebase * addressing code review changes * update with one channel per rebuild request * other review suggestions * update with review suggestions * run mockery generate for MockResourceClient * update tests * update tests and lint * fix test --- pkg/registry/apis/dashboard/legacy/client.go | 5 + .../apis/dashboard/legacy/sql_dashboards.go | 4 + pkg/registry/apis/folders/validate_test.go | 5 + pkg/server/search_server_distributor_test.go | 23 ++ pkg/storage/unified/proto/search.proto | 22 ++ pkg/storage/unified/resource/client_mock.go | 76 ++++- pkg/storage/unified/resource/search.go | 95 ++++++- pkg/storage/unified/resource/search_client.go | 5 + .../unified/resource/search_client_test.go | 5 + .../resource/search_server_distributor.go | 98 +++++++ pkg/storage/unified/resource/search_test.go | 100 ++++++- pkg/storage/unified/resource/server.go | 8 + pkg/storage/unified/resourcepb/search.pb.go | 265 ++++++++++++++---- .../unified/resourcepb/search_grpc.pb.go | 42 ++- pkg/storage/unified/sql/backend.go | 8 +- 15 files changed, 681 insertions(+), 80 deletions(-) diff --git a/pkg/registry/apis/dashboard/legacy/client.go b/pkg/registry/apis/dashboard/legacy/client.go index 1eeebd7b689..913dd02d14d 100644 --- a/pkg/registry/apis/dashboard/legacy/client.go +++ b/pkg/registry/apis/dashboard/legacy/client.go @@ -90,3 +90,8 @@ func (d *directResourceClient) Watch(ctx context.Context, in *resourcepb.WatchRe func (d *directResourceClient) BulkProcess(ctx context.Context, opts ...grpc.CallOption) (resourcepb.BulkStore_BulkProcessClient, error) { return nil, fmt.Errorf("BulkProcess not supported with direct resource client") } + +// RebuildIndexes implements resource.ResourceClient. +func (b *directResourceClient) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 7c17ec8b6aa..d4997c39c7d 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -1046,3 +1046,7 @@ func parseLibraryPanelRow(p panel) (dashboardV0.LibraryPanel, error) { return item, nil } + +func (b *dashboardSqlAccess) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/pkg/registry/apis/folders/validate_test.go b/pkg/registry/apis/folders/validate_test.go index 1f67c0da9d8..c4bf07bc71c 100644 --- a/pkg/registry/apis/folders/validate_test.go +++ b/pkg/registry/apis/folders/validate_test.go @@ -581,3 +581,8 @@ func (m *mockSearchClient) GetStats(ctx context.Context, in *resourcepb.Resource func (m *mockSearchClient) Search(ctx context.Context, in *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) { return m.search, m.searchErr } + +// RebuildIndexes implements resourcepb.ResourceIndexClient. +func (m *mockSearchClient) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/pkg/server/search_server_distributor_test.go b/pkg/server/search_server_distributor_test.go index 92182586105..65f6346d0a8 100644 --- a/pkg/server/search_server_distributor_test.go +++ b/pkg/server/search_server_distributor_test.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "strconv" + "strings" "sync" "testing" "time" @@ -175,6 +176,28 @@ func TestIntegrationDistributor(t *testing.T) { } }) + t.Run("RebuildIndexes", func(t *testing.T) { + instanceResponseCount := make(map[string]int) + + // simulate RebuildIndexes for a single namespace + testNamespace := testNamespaces[0] + + req := &resourcepb.RebuildIndexesRequest{ + Namespace: testNamespace, + Keys: []*resourcepb.ResourceKey{{ + Namespace: testNamespace, + Group: "folder.grafana.app", + Resource: "folders", + }}, + } + distributorRes := getDistributorResponse(t, req, distributorServer.resourceClient.RebuildIndexes, instanceResponseCount) + require.Nil(t, distributorRes.Error) + + // assert all instances got the response by looking at the merged details + count := strings.Count(distributorRes.Details, "{instance:") + require.Equal(t, len(testServers), count) + }) + var wg sync.WaitGroup for _, testServer := range testServers { wg.Add(1) diff --git a/pkg/storage/unified/proto/search.proto b/pkg/storage/unified/proto/search.proto index d206060daf4..5018c97c9db 100644 --- a/pkg/storage/unified/proto/search.proto +++ b/pkg/storage/unified/proto/search.proto @@ -13,6 +13,8 @@ service ResourceIndex { // Get the resource stats rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse); + + rpc RebuildIndexes(RebuildIndexesRequest) returns (RebuildIndexesResponse); } // Get statistics across multiple resources @@ -138,3 +140,23 @@ message ResourceSearchResponse { // Facet results map facet = 7; } + +message RebuildIndexesRequest { + // Namespace (tenant) must be the same as all keys' namespace + string namespace = 1; + + // List of ResourceKeys (Namespace + Group + Resource) + repeated ResourceKey keys = 2; +} + +message RebuildIndexesResponse { + // Total count of rebuilt indexes + int64 rebuildCount = 1; + + // Result message + string details = 2; + + // Error details + ErrorResult error = 3; +} + diff --git a/pkg/storage/unified/resource/client_mock.go b/pkg/storage/unified/resource/client_mock.go index 23688b097ec..fcc7392880e 100644 --- a/pkg/storage/unified/resource/client_mock.go +++ b/pkg/storage/unified/resource/client_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. +// Code generated by mockery v2.53.5. DO NOT EDIT. package resource @@ -838,6 +838,80 @@ func (_c *MockResourceClient_Read_Call) RunAndReturn(run func(context.Context, * return _c } +// RebuildIndexes provides a mock function with given fields: ctx, in, opts +func (_m *MockResourceClient) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for RebuildIndexes") + } + + var r0 *resourcepb.RebuildIndexesResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.RebuildIndexesRequest, ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.RebuildIndexesRequest, ...grpc.CallOption) *resourcepb.RebuildIndexesResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*resourcepb.RebuildIndexesResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *resourcepb.RebuildIndexesRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockResourceClient_RebuildIndexes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RebuildIndexes' +type MockResourceClient_RebuildIndexes_Call struct { + *mock.Call +} + +// RebuildIndexes is a helper method to define mock.On call +// - ctx context.Context +// - in *resourcepb.RebuildIndexesRequest +// - opts ...grpc.CallOption +func (_e *MockResourceClient_Expecter) RebuildIndexes(ctx interface{}, in interface{}, opts ...interface{}) *MockResourceClient_RebuildIndexes_Call { + return &MockResourceClient_RebuildIndexes_Call{Call: _e.mock.On("RebuildIndexes", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *MockResourceClient_RebuildIndexes_Call) Run(run func(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption)) *MockResourceClient_RebuildIndexes_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]grpc.CallOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(grpc.CallOption) + } + } + run(args[0].(context.Context), args[1].(*resourcepb.RebuildIndexesRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockResourceClient_RebuildIndexes_Call) Return(_a0 *resourcepb.RebuildIndexesResponse, _a1 error) *MockResourceClient_RebuildIndexes_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockResourceClient_RebuildIndexes_Call) RunAndReturn(run func(context.Context, *resourcepb.RebuildIndexesRequest, ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error)) *MockResourceClient_RebuildIndexes_Call { + _c.Call.Return(run) + return _c +} + // Search provides a mock function with given fields: ctx, in, opts func (_m *MockResourceClient) Search(ctx context.Context, in *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) { _va := make([]interface{}, len(opts)) diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index dce40f884cf..bca5fb98491 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -234,6 +234,9 @@ func combineRebuildRequests(a, b rebuildRequest) (c rebuildRequest, ok bool) { ret.lastImportTime = b.lastImportTime } + // Combine complete channels + ret.completeChannels = append(a.completeChannels, b.completeChannels...) + return ret, true } @@ -512,6 +515,52 @@ func (s *searchSupport) GetStats(ctx context.Context, req *resourcepb.ResourceSt return rsp, nil } +func (s *searchSupport) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) { + ctx, span := tracer.Start(ctx, "resource.searchSupport.RebuildIndexes") + defer span.End() + + filterKeys := make([]NamespacedResource, len(req.Keys)) + for _, key := range req.Keys { + if req.Namespace != key.Namespace { + return &resourcepb.RebuildIndexesResponse{ + Error: NewBadRequestError("key namespace does not match request namespace"), + }, nil + } + filterKeys = append(filterKeys, NamespacedResource{ + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + }) + } + + importTimes, err := s.getLastImportTimes(ctx) + if err != nil { + return &resourcepb.RebuildIndexesResponse{ + Error: AsErrorResult(err), + }, nil + } + + completeChs := s.findIndexesToRebuild(importTimes, filterKeys, time.Now()) + rebuildCount := len(completeChs) + for _, ch := range completeChs { + select { + case <-ch: + continue + case <-ctx.Done(): // request was done before all indexes rebuilt + return &resourcepb.RebuildIndexesResponse{ + RebuildCount: int64(rebuildCount), + Details: fmt.Sprintf("returning before all index rebuilds completed for %d indexes", rebuildCount), + }, nil + } + } + + // All rebuilds completed successfully + return &resourcepb.RebuildIndexesResponse{ + RebuildCount: int64(rebuildCount), + Details: fmt.Sprintf("completed %d index rebuilds", rebuildCount), + }, nil +} + func (s *searchSupport) buildIndexes(ctx context.Context) (int, error) { totalBatchesIndexed := 0 group := errgroup.Group{} @@ -602,16 +651,23 @@ func (s *searchSupport) runPeriodicScanForIndexesToRebuild(ctx context.Context) if err != nil { s.log.Error("failed to get import times", "error", err) } - s.findIndexesToRebuild(importTimes, time.Now()) + s.findIndexesToRebuild(importTimes, nil, time.Now()) } } } -func (s *searchSupport) findIndexesToRebuild(lastImportTimes map[NamespacedResource]time.Time, now time.Time) { +func (s *searchSupport) findIndexesToRebuild(lastImportTimes map[NamespacedResource]time.Time, filterKeys []NamespacedResource, now time.Time) []chan struct{} { // Check all open indexes and see if any of them need to be rebuilt. // This is done periodically to make sure that the indexes are up to date. - keys := s.search.GetOpenIndexes() + var keys []NamespacedResource + if filterKeys != nil { + keys = filterKeys + } else { + keys = s.search.GetOpenIndexes() + } + + var completeChs []chan struct{} for _, key := range keys { idx := s.search.GetIndex(key) if idx == nil { @@ -638,18 +694,17 @@ func (s *searchSupport) findIndexesToRebuild(lastImportTimes map[NamespacedResou } if shouldRebuildIndex(bi, s.minBuildVersion, minBuildTime, lastImportTime, nil) { - s.rebuildQueue.Add(rebuildRequest{ - NamespacedResource: key, - minBuildTime: minBuildTime, - minBuildVersion: s.minBuildVersion, - lastImportTime: lastImportTime, - }) + completeCh := make(chan struct{}) + completeChs = append(completeChs, completeCh) + rebuildReq := newRebuildRequest(key, minBuildTime, lastImportTime, s.minBuildVersion, completeCh) + s.rebuildQueue.Add(rebuildReq) if s.indexMetrics != nil { s.indexMetrics.RebuildQueueLength.Set(float64(s.rebuildQueue.Len())) } } } + return completeChs } func (s *searchSupport) getLastImportTimes(ctx context.Context) (map[NamespacedResource]time.Time, error) { @@ -690,6 +745,12 @@ func (s *searchSupport) rebuildIndex(ctx context.Context, req rebuildRequest) { l := s.log.New("namespace", req.Namespace, "group", req.Group, "resource", req.Resource) + defer func() { + for _, ch := range req.completeChannels { + close(ch) + } + }() + idx := s.search.GetIndex(req.NamespacedResource) if idx == nil { span.AddEvent("index not found") @@ -782,6 +843,22 @@ type rebuildRequest struct { minBuildTime time.Time // if not zero, rebuild index if it has been built before this timestamp lastImportTime time.Time // if not zero, rebuild index if it has been built before this timestamp. minBuildVersion *semver.Version // if not nil, rebuild index with build version older than this. + + completeChannels []chan<- struct{} // signal rebuild index is complete +} + +func newRebuildRequest(key NamespacedResource, minBuildTime, lastImportTime time.Time, minBuildVersion *semver.Version, completeCh chan<- struct{}) rebuildRequest { + var completeChannels []chan<- struct{} // setup a list as requests can be combined + if completeCh != nil { + completeChannels = []chan<- struct{}{completeCh} + } + return rebuildRequest{ + NamespacedResource: key, + minBuildTime: minBuildTime, + minBuildVersion: minBuildVersion, + lastImportTime: lastImportTime, + completeChannels: completeChannels, + } } func (s *searchSupport) getOrCreateIndex(ctx context.Context, stats *SearchStats, key NamespacedResource, reason string) (ResourceIndex, error) { diff --git a/pkg/storage/unified/resource/search_client.go b/pkg/storage/unified/resource/search_client.go index c4320114ba5..cfe0b0e594f 100644 --- a/pkg/storage/unified/resource/search_client.go +++ b/pkg/storage/unified/resource/search_client.go @@ -211,6 +211,11 @@ func (s *searchWrapper) Search(ctx context.Context, in *resourcepb.ResourceSearc return client.Search(ctx, in, opts...) } +func (s *searchWrapper) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest, + opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) { + return s.unifiedClient.RebuildIndexes(ctx, in, opts...) +} + // compareSearchResults compares legacy and unified search results and logs/metrics the outcome func (s *searchWrapper) compareSearchResults(legacyResponse, unifiedResponse *resourcepb.ResourceSearchResponse, requestKey *resourcepb.ResourceKey) { if legacyResponse == nil || unifiedResponse == nil { diff --git a/pkg/storage/unified/resource/search_client_test.go b/pkg/storage/unified/resource/search_client_test.go index 4b0a73895ec..f7676ad981e 100644 --- a/pkg/storage/unified/resource/search_client_test.go +++ b/pkg/storage/unified/resource/search_client_test.go @@ -112,6 +112,11 @@ func (m *MockResourceIndexClient) GetStats(ctx context.Context, in *resourcepb.R return args.Get(0).(*resourcepb.ResourceStatsResponse), args.Error(1) } +func (m *MockResourceIndexClient) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) { + args := m.Called(ctx, in, opts) + return args.Get(0).(*resourcepb.RebuildIndexesResponse), args.Error(1) +} + func setupTestSearchClient(t *testing.T) (schema.GroupResource, *MockResourceIndexClient, *MockResourceIndexClient, featuremgmt.FeatureToggles) { t.Helper() gr := schema.GroupResource{Group: "test", Resource: "items"} diff --git a/pkg/storage/unified/resource/search_server_distributor.go b/pkg/storage/unified/resource/search_server_distributor.go index 1363c2452f3..cceb0845b11 100644 --- a/pkg/storage/unified/resource/search_server_distributor.go +++ b/pkg/storage/unified/resource/search_server_distributor.go @@ -2,8 +2,12 @@ package resource import ( "context" + "errors" + "fmt" "hash/fnv" "math/rand" + "sync" + "sync/atomic" "time" "github.com/grafana/dskit/ring" @@ -114,6 +118,100 @@ func (ds *distributorServer) GetStats(ctx context.Context, r *resourcepb.Resourc return client.GetStats(ctx, r) } +func (ds *distributorServer) RebuildIndexes(ctx context.Context, r *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) { + ctx, span := ds.tracing.Start(ctx, "distributor.RebuildIndexes") + defer span.End() + + // validate input + for _, key := range r.Keys { + if r.Namespace != key.Namespace { + return &resourcepb.RebuildIndexesResponse{ + Error: NewBadRequestError("key namespace does not match request namespace"), + }, nil + } + } + + // distribute the request to all search pods to minimize risk of stale index + // it will not rebuild on those which don't have the index open + rs, err := ds.ring.GetAllHealthy(searchRingRead) + if err != nil { + return nil, fmt.Errorf("failed to get all healthy instances from the ring") + } + + err = grpc.SetHeader(ctx, metadata.Pairs("proxied-instance-id", "all")) + if err != nil { + ds.log.Debug("error setting grpc header", "err", err) + } + + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + md = make(metadata.MD) + } + rCtx := userutils.InjectOrgID(metadata.NewOutgoingContext(ctx, md), r.Namespace) + + var wg sync.WaitGroup + var totalRebuildCount atomic.Int64 + detailsCh := make(chan string, len(rs.Instances)) + errorCh := make(chan error, len(rs.Instances)) + + for _, inst := range rs.Instances { + wg.Add(1) + go func() { + defer wg.Done() + + client, err := ds.clientPool.GetClientForInstance(inst) + if err != nil { + errorCh <- fmt.Errorf("instance %s: failed to get client, %w", inst.Id, err) + return + } + + rsp, err := client.(*RingClient).Client.RebuildIndexes(rCtx, r) + if err != nil { + errorCh <- fmt.Errorf("instance %s: failed to distribute rebuild index request, %w", inst.Id, err) + return + } + + if rsp.Error != nil { + errorCh <- fmt.Errorf("instance %s: rebuild index request returned the error %s", inst.Id, rsp.Error.Message) + return + } + + if rsp.Details != "" { + detailsCh <- fmt.Sprintf("{instance: %s, details: %s}", inst.Id, rsp.Details) + } + + totalRebuildCount.Add(rsp.RebuildCount) + }() + } + + wg.Wait() + close(errorCh) + close(detailsCh) + + errs := make([]error, 0, len(errorCh)) + for err := range errorCh { + ds.log.Error("rebuild indexes call failed with %w", err) + errs = append(errs, err) + } + + var details string + for d := range detailsCh { + if len(details) > 0 { + details += ", " + } + details += d + } + + response := &resourcepb.RebuildIndexesResponse{ + RebuildCount: totalRebuildCount.Load(), + Details: details, + } + if len(errs) > 0 { + response.Error = AsErrorResult(errors.Join(errs...)) + } + return response, nil +} + func (ds *distributorServer) CountManagedObjects(ctx context.Context, r *resourcepb.CountManagedObjectsRequest) (*resourcepb.CountManagedObjectsResponse, error) { ctx, span := ds.tracing.Start(ctx, "distributor.CountManagedObjects") defer span.End() diff --git a/pkg/storage/unified/resource/search_test.go b/pkg/storage/unified/resource/search_test.go index 215a88cd2cd..154608a4eee 100644 --- a/pkg/storage/unified/resource/search_test.go +++ b/pkg/storage/unified/resource/search_test.go @@ -10,6 +10,8 @@ import ( "time" "github.com/Masterminds/semver" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/grafana/authlib/types" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -85,7 +87,8 @@ func (m *MockDocumentBuilder) BuildDocument(ctx context.Context, key *resourcepb // mockStorageBackend implements StorageBackend for testing type mockStorageBackend struct { - resourceStats []ResourceStats + resourceStats []ResourceStats + lastImportTimes []ResourceLastImportTime } func (m *mockStorageBackend) GetResourceStats(ctx context.Context, nsr NamespacedResource, minCount int) ([]ResourceStats, error) { @@ -127,7 +130,11 @@ func (m *mockStorageBackend) ListModifiedSince(ctx context.Context, key Namespac func (m *mockStorageBackend) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[ResourceLastImportTime, error] { return func(yield func(ResourceLastImportTime, error) bool) { - yield(ResourceLastImportTime{}, errors.New("not implemented")) + for _, ti := range m.lastImportTimes { + if !yield(ti, nil) { + return + } + } } } @@ -605,14 +612,14 @@ func TestFindIndexesForRebuild(t *testing.T) { {Namespace: "resource-v6", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}: lastImportTime, } - support.findIndexesToRebuild(importTimes, now) + support.findIndexesToRebuild(importTimes, nil, now) require.Equal(t, 7, support.rebuildQueue.Len()) now5m := now.Add(5 * time.Minute) // Running findIndexesToRebuild again should not add any new indexes to the rebuild queue, and all existing // ones should be "combined" with new ones (this will "bump" minBuildTime) - support.findIndexesToRebuild(importTimes, now5m) + support.findIndexesToRebuild(importTimes, nil, now5m) require.Equal(t, 7, support.rebuildQueue.Len()) // Values that we expect to find in rebuild requests. @@ -621,7 +628,7 @@ func TestFindIndexesForRebuild(t *testing.T) { minBuildTimeDashboard := now5m.Add(-1 * time.Hour) vals := support.rebuildQueue.Elements() - require.ElementsMatch(t, vals, []rebuildRequest{ + expected := []rebuildRequest{ {NamespacedResource: NamespacedResource{Namespace: "resource-2h-v5", Group: "group", Resource: "folder"}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTime}, {NamespacedResource: NamespacedResource{Namespace: "resource-10h-v5", Group: "group", Resource: "folder"}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTime}, {NamespacedResource: NamespacedResource{Namespace: "resource-10h-v6", Group: "group", Resource: "folder"}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTime}, @@ -631,7 +638,10 @@ func TestFindIndexesForRebuild(t *testing.T) { {NamespacedResource: NamespacedResource{Namespace: "resource-2h-v6", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTimeDashboard}, {NamespacedResource: NamespacedResource{Namespace: "resource-recently-imported", Group: "group", Resource: dashboardv1.DASHBOARD_RESOURCE}, minBuildVersion: minBuildVersion, minBuildTime: minBuildTimeDashboard, lastImportTime: lastImportTime}, - }) + } + if diff := cmp.Diff(expected, vals, cmpopts.IgnoreFields(rebuildRequest{}, "completeChannels"), cmp.AllowUnexported(rebuildRequest{})); diff != "" { + t.Errorf("rebuildQueue mismatch (-want +got):\n%s", diff) + } } func TestRebuildIndexes(t *testing.T) { @@ -748,3 +758,81 @@ func checkRebuildIndex(t *testing.T, support *searchSupport, req rebuildRequest, require.Nil(t, idxAfter, "index should not exist after rebuildIndex") } } + +func TestRebuildIndexesForResource(t *testing.T) { + key := NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"} + + storage := &mockStorageBackend{ + resourceStats: []ResourceStats{ + {NamespacedResource: key, Count: 50, ResourceVersion: 11111111}, + }, + lastImportTimes: []ResourceLastImportTime{{ + NamespacedResource: key, + LastImportTime: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC), + }}, + } + + search := &mockSearchBackend{} + supplier := &TestDocumentBuilderSupplier{ + GroupsResources: map[string]string{ + "group": "resource", + }, + } + + opts := SearchOptions{ + Backend: search, + Resources: supplier, + InitMinCount: 1, + } + + support, err := newSearchSupport(opts, storage, nil, nil, nil, nil) + require.NoError(t, err) + require.NotNil(t, support) + + err = support.init(t.Context()) + require.NoError(t, err) + + require.Equal(t, 0, support.rebuildQueue.Len()) + + // invalid request + rebuildReq := &resourcepb.RebuildIndexesRequest{ + Namespace: "some-other-namespace", + Keys: []*resourcepb.ResourceKey{{ + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + }}} + rsp, err := support.RebuildIndexes(t.Context(), rebuildReq) + require.NoError(t, err) + require.Equal(t, "key namespace does not match request namespace", rsp.Error.Message) + + rebuildReq.Namespace = key.Namespace + + // cached index info + search.cache[key] = &MockResourceIndex{ + buildInfo: IndexBuildInfo{BuildVersion: semver.MustParse("5.0.0"), BuildTime: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)}, + } + + // old import time will not be rebuilt + storage.lastImportTimes = []ResourceLastImportTime{{ + NamespacedResource: key, + LastImportTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), + }} + rsp, err = support.RebuildIndexes(t.Context(), rebuildReq) + require.NoError(t, err) + require.Equal(t, int64(0), rsp.RebuildCount) + require.Equal(t, 0, support.rebuildQueue.Len()) + + // recent import time gets added to rebuild queue and processed + storage.lastImportTimes = []ResourceLastImportTime{{ + NamespacedResource: key, + LastImportTime: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC), + }} + + rsp, err = support.RebuildIndexes(t.Context(), rebuildReq) + require.NoError(t, err) + require.Equal(t, int64(1), rsp.RebuildCount) + + // rebuild waited for rebuild queue to process + require.Equal(t, 0, support.rebuildQueue.Len()) +} diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index ca9f87896e1..4c0adddf3c8 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -1541,3 +1541,11 @@ func (s *server) runInQueue(ctx context.Context, tenantID string, runnable func( return queueCtx.Err() // Timed out or canceled while waiting for execution. } } + +func (s *server) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) { + if s.search == nil { + return nil, fmt.Errorf("search index not configured") + } + + return s.search.RebuildIndexes(ctx, req) +} diff --git a/pkg/storage/unified/resourcepb/search.pb.go b/pkg/storage/unified/resourcepb/search.pb.go index c10b2f2fe53..91f5aadfa97 100644 --- a/pkg/storage/unified/resourcepb/search.pb.go +++ b/pkg/storage/unified/resourcepb/search.pb.go @@ -386,6 +386,123 @@ func (x *ResourceSearchResponse) GetFacet() map[string]*ResourceSearchResponse_F return nil } +type RebuildIndexesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Namespace (tenant) + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // List of ResourceKeys (Namespace + Group + Resource) + Keys []*ResourceKey `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RebuildIndexesRequest) Reset() { + *x = RebuildIndexesRequest{} + mi := &file_search_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RebuildIndexesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RebuildIndexesRequest) ProtoMessage() {} + +func (x *RebuildIndexesRequest) ProtoReflect() protoreflect.Message { + mi := &file_search_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RebuildIndexesRequest.ProtoReflect.Descriptor instead. +func (*RebuildIndexesRequest) Descriptor() ([]byte, []int) { + return file_search_proto_rawDescGZIP(), []int{4} +} + +func (x *RebuildIndexesRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *RebuildIndexesRequest) GetKeys() []*ResourceKey { + if x != nil { + return x.Keys + } + return nil +} + +type RebuildIndexesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Total count of rebuilt indexes + RebuildCount int64 `protobuf:"varint,1,opt,name=rebuildCount,proto3" json:"rebuildCount,omitempty"` + // Result message + Details string `protobuf:"bytes,2,opt,name=details,proto3" json:"details,omitempty"` + // Error details + Error *ErrorResult `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RebuildIndexesResponse) Reset() { + *x = RebuildIndexesResponse{} + mi := &file_search_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RebuildIndexesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RebuildIndexesResponse) ProtoMessage() {} + +func (x *RebuildIndexesResponse) ProtoReflect() protoreflect.Message { + mi := &file_search_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RebuildIndexesResponse.ProtoReflect.Descriptor instead. +func (*RebuildIndexesResponse) Descriptor() ([]byte, []int) { + return file_search_proto_rawDescGZIP(), []int{5} +} + +func (x *RebuildIndexesResponse) GetRebuildCount() int64 { + if x != nil { + return x.RebuildCount + } + return 0 +} + +func (x *RebuildIndexesResponse) GetDetails() string { + if x != nil { + return x.Details + } + return "" +} + +func (x *RebuildIndexesResponse) GetError() *ErrorResult { + if x != nil { + return x.Error + } + return nil +} + type ResourceStatsResponse_Stats struct { state protoimpl.MessageState `protogen:"open.v1"` // Resource group @@ -400,7 +517,7 @@ type ResourceStatsResponse_Stats struct { func (x *ResourceStatsResponse_Stats) Reset() { *x = ResourceStatsResponse_Stats{} - mi := &file_search_proto_msgTypes[4] + mi := &file_search_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -412,7 +529,7 @@ func (x *ResourceStatsResponse_Stats) String() string { func (*ResourceStatsResponse_Stats) ProtoMessage() {} func (x *ResourceStatsResponse_Stats) ProtoReflect() protoreflect.Message { - mi := &file_search_proto_msgTypes[4] + mi := &file_search_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -459,7 +576,7 @@ type ResourceSearchRequest_Sort struct { func (x *ResourceSearchRequest_Sort) Reset() { *x = ResourceSearchRequest_Sort{} - mi := &file_search_proto_msgTypes[5] + mi := &file_search_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -471,7 +588,7 @@ func (x *ResourceSearchRequest_Sort) String() string { func (*ResourceSearchRequest_Sort) ProtoMessage() {} func (x *ResourceSearchRequest_Sort) ProtoReflect() protoreflect.Message { - mi := &file_search_proto_msgTypes[5] + mi := &file_search_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -511,7 +628,7 @@ type ResourceSearchRequest_Facet struct { func (x *ResourceSearchRequest_Facet) Reset() { *x = ResourceSearchRequest_Facet{} - mi := &file_search_proto_msgTypes[6] + mi := &file_search_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -523,7 +640,7 @@ func (x *ResourceSearchRequest_Facet) String() string { func (*ResourceSearchRequest_Facet) ProtoMessage() {} func (x *ResourceSearchRequest_Facet) ProtoReflect() protoreflect.Message { - mi := &file_search_proto_msgTypes[6] + mi := &file_search_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -568,7 +685,7 @@ type ResourceSearchResponse_Facet struct { func (x *ResourceSearchResponse_Facet) Reset() { *x = ResourceSearchResponse_Facet{} - mi := &file_search_proto_msgTypes[8] + mi := &file_search_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -580,7 +697,7 @@ func (x *ResourceSearchResponse_Facet) String() string { func (*ResourceSearchResponse_Facet) ProtoMessage() {} func (x *ResourceSearchResponse_Facet) ProtoReflect() protoreflect.Message { - mi := &file_search_proto_msgTypes[8] + mi := &file_search_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -634,7 +751,7 @@ type ResourceSearchResponse_TermFacet struct { func (x *ResourceSearchResponse_TermFacet) Reset() { *x = ResourceSearchResponse_TermFacet{} - mi := &file_search_proto_msgTypes[9] + mi := &file_search_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -646,7 +763,7 @@ func (x *ResourceSearchResponse_TermFacet) String() string { func (*ResourceSearchResponse_TermFacet) ProtoMessage() {} func (x *ResourceSearchResponse_TermFacet) ProtoReflect() protoreflect.Message { - mi := &file_search_proto_msgTypes[9] + mi := &file_search_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -780,22 +897,42 @@ var file_search_proto_rawDesc = string([]byte{ 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, - 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, - 0xa9, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, - 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, - 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, - 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, - 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x63, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0x60, 0x0a, 0x15, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65, 0x79, + 0x73, 0x22, 0x83, 0x01, 0x0a, 0x16, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, + 0x65, 0x78, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, + 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x32, 0xfe, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4b, 0x0a, 0x06, 0x53, 0x65, 0x61, + 0x72, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, + 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, + 0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, + 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -810,47 +947,53 @@ func file_search_proto_rawDescGZIP() []byte { return file_search_proto_rawDescData } -var file_search_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_search_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_search_proto_goTypes = []any{ (*ResourceStatsRequest)(nil), // 0: resource.ResourceStatsRequest (*ResourceStatsResponse)(nil), // 1: resource.ResourceStatsResponse (*ResourceSearchRequest)(nil), // 2: resource.ResourceSearchRequest (*ResourceSearchResponse)(nil), // 3: resource.ResourceSearchResponse - (*ResourceStatsResponse_Stats)(nil), // 4: resource.ResourceStatsResponse.Stats - (*ResourceSearchRequest_Sort)(nil), // 5: resource.ResourceSearchRequest.Sort - (*ResourceSearchRequest_Facet)(nil), // 6: resource.ResourceSearchRequest.Facet - nil, // 7: resource.ResourceSearchRequest.FacetEntry - (*ResourceSearchResponse_Facet)(nil), // 8: resource.ResourceSearchResponse.Facet - (*ResourceSearchResponse_TermFacet)(nil), // 9: resource.ResourceSearchResponse.TermFacet - nil, // 10: resource.ResourceSearchResponse.FacetEntry - (*ErrorResult)(nil), // 11: resource.ErrorResult - (*ListOptions)(nil), // 12: resource.ListOptions - (*ResourceKey)(nil), // 13: resource.ResourceKey - (*ResourceTable)(nil), // 14: resource.ResourceTable + (*RebuildIndexesRequest)(nil), // 4: resource.RebuildIndexesRequest + (*RebuildIndexesResponse)(nil), // 5: resource.RebuildIndexesResponse + (*ResourceStatsResponse_Stats)(nil), // 6: resource.ResourceStatsResponse.Stats + (*ResourceSearchRequest_Sort)(nil), // 7: resource.ResourceSearchRequest.Sort + (*ResourceSearchRequest_Facet)(nil), // 8: resource.ResourceSearchRequest.Facet + nil, // 9: resource.ResourceSearchRequest.FacetEntry + (*ResourceSearchResponse_Facet)(nil), // 10: resource.ResourceSearchResponse.Facet + (*ResourceSearchResponse_TermFacet)(nil), // 11: resource.ResourceSearchResponse.TermFacet + nil, // 12: resource.ResourceSearchResponse.FacetEntry + (*ErrorResult)(nil), // 13: resource.ErrorResult + (*ListOptions)(nil), // 14: resource.ListOptions + (*ResourceKey)(nil), // 15: resource.ResourceKey + (*ResourceTable)(nil), // 16: resource.ResourceTable } var file_search_proto_depIdxs = []int32{ - 11, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult - 4, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats - 12, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions - 13, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey - 5, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort - 7, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry - 11, // 6: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult - 13, // 7: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey - 14, // 8: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable - 10, // 9: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry - 6, // 10: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet - 9, // 11: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet - 8, // 12: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet - 2, // 13: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest - 0, // 14: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest - 3, // 15: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse - 1, // 16: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse - 15, // [15:17] is the sub-list for method output_type - 13, // [13:15] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 13, // 0: resource.ResourceStatsResponse.error:type_name -> resource.ErrorResult + 6, // 1: resource.ResourceStatsResponse.stats:type_name -> resource.ResourceStatsResponse.Stats + 14, // 2: resource.ResourceSearchRequest.options:type_name -> resource.ListOptions + 15, // 3: resource.ResourceSearchRequest.federated:type_name -> resource.ResourceKey + 7, // 4: resource.ResourceSearchRequest.sortBy:type_name -> resource.ResourceSearchRequest.Sort + 9, // 5: resource.ResourceSearchRequest.facet:type_name -> resource.ResourceSearchRequest.FacetEntry + 13, // 6: resource.ResourceSearchResponse.error:type_name -> resource.ErrorResult + 15, // 7: resource.ResourceSearchResponse.key:type_name -> resource.ResourceKey + 16, // 8: resource.ResourceSearchResponse.results:type_name -> resource.ResourceTable + 12, // 9: resource.ResourceSearchResponse.facet:type_name -> resource.ResourceSearchResponse.FacetEntry + 15, // 10: resource.RebuildIndexesRequest.keys:type_name -> resource.ResourceKey + 13, // 11: resource.RebuildIndexesResponse.error:type_name -> resource.ErrorResult + 8, // 12: resource.ResourceSearchRequest.FacetEntry.value:type_name -> resource.ResourceSearchRequest.Facet + 11, // 13: resource.ResourceSearchResponse.Facet.terms:type_name -> resource.ResourceSearchResponse.TermFacet + 10, // 14: resource.ResourceSearchResponse.FacetEntry.value:type_name -> resource.ResourceSearchResponse.Facet + 2, // 15: resource.ResourceIndex.Search:input_type -> resource.ResourceSearchRequest + 0, // 16: resource.ResourceIndex.GetStats:input_type -> resource.ResourceStatsRequest + 4, // 17: resource.ResourceIndex.RebuildIndexes:input_type -> resource.RebuildIndexesRequest + 3, // 18: resource.ResourceIndex.Search:output_type -> resource.ResourceSearchResponse + 1, // 19: resource.ResourceIndex.GetStats:output_type -> resource.ResourceStatsResponse + 5, // 20: resource.ResourceIndex.RebuildIndexes:output_type -> resource.RebuildIndexesResponse + 18, // [18:21] is the sub-list for method output_type + 15, // [15:18] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_search_proto_init() } @@ -865,7 +1008,7 @@ func file_search_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_search_proto_rawDesc), len(file_search_proto_rawDesc)), NumEnums: 0, - NumMessages: 11, + NumMessages: 13, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/storage/unified/resourcepb/search_grpc.pb.go b/pkg/storage/unified/resourcepb/search_grpc.pb.go index 2fa3f16c611..d69cbd14e38 100644 --- a/pkg/storage/unified/resourcepb/search_grpc.pb.go +++ b/pkg/storage/unified/resourcepb/search_grpc.pb.go @@ -19,8 +19,9 @@ import ( const _ = grpc.SupportPackageIsVersion8 const ( - ResourceIndex_Search_FullMethodName = "/resource.ResourceIndex/Search" - ResourceIndex_GetStats_FullMethodName = "/resource.ResourceIndex/GetStats" + ResourceIndex_Search_FullMethodName = "/resource.ResourceIndex/Search" + ResourceIndex_GetStats_FullMethodName = "/resource.ResourceIndex/GetStats" + ResourceIndex_RebuildIndexes_FullMethodName = "/resource.ResourceIndex/RebuildIndexes" ) // ResourceIndexClient is the client API for ResourceIndex service. @@ -33,6 +34,7 @@ type ResourceIndexClient interface { Search(ctx context.Context, in *ResourceSearchRequest, opts ...grpc.CallOption) (*ResourceSearchResponse, error) // Get the resource stats GetStats(ctx context.Context, in *ResourceStatsRequest, opts ...grpc.CallOption) (*ResourceStatsResponse, error) + RebuildIndexes(ctx context.Context, in *RebuildIndexesRequest, opts ...grpc.CallOption) (*RebuildIndexesResponse, error) } type resourceIndexClient struct { @@ -63,6 +65,16 @@ func (c *resourceIndexClient) GetStats(ctx context.Context, in *ResourceStatsReq return out, nil } +func (c *resourceIndexClient) RebuildIndexes(ctx context.Context, in *RebuildIndexesRequest, opts ...grpc.CallOption) (*RebuildIndexesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RebuildIndexesResponse) + err := c.cc.Invoke(ctx, ResourceIndex_RebuildIndexes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // ResourceIndexServer is the server API for ResourceIndex service. // All implementations should embed UnimplementedResourceIndexServer // for forward compatibility @@ -73,6 +85,7 @@ type ResourceIndexServer interface { Search(context.Context, *ResourceSearchRequest) (*ResourceSearchResponse, error) // Get the resource stats GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error) + RebuildIndexes(context.Context, *RebuildIndexesRequest) (*RebuildIndexesResponse, error) } // UnimplementedResourceIndexServer should be embedded to have forward compatible implementations. @@ -85,6 +98,9 @@ func (UnimplementedResourceIndexServer) Search(context.Context, *ResourceSearchR func (UnimplementedResourceIndexServer) GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetStats not implemented") } +func (UnimplementedResourceIndexServer) RebuildIndexes(context.Context, *RebuildIndexesRequest) (*RebuildIndexesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RebuildIndexes not implemented") +} // UnsafeResourceIndexServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to ResourceIndexServer will @@ -133,6 +149,24 @@ func _ResourceIndex_GetStats_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _ResourceIndex_RebuildIndexes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RebuildIndexesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceIndexServer).RebuildIndexes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceIndex_RebuildIndexes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceIndexServer).RebuildIndexes(ctx, req.(*RebuildIndexesRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ResourceIndex_ServiceDesc is the grpc.ServiceDesc for ResourceIndex service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -148,6 +182,10 @@ var ResourceIndex_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetStats", Handler: _ResourceIndex_GetStats_Handler, }, + { + MethodName: "RebuildIndexes", + Handler: _ResourceIndex_RebuildIndexes_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "search.proto", diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index d3dae729fd4..8de43f318b0 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -994,7 +994,9 @@ func (b *backend) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[reso b.lastImportTimeDeletionTime.Store(now) } - rows, err := dbutil.QueryRows(ctx, b.db, sqlResourceLastImportTimeQuery, &sqlResourceLastImportTimeQueryRequest{SQLTemplate: sqltemplate.New(b.dialect)}) + rows, err := dbutil.QueryRows(ctx, b.db, sqlResourceLastImportTimeQuery, &sqlResourceLastImportTimeQueryRequest{ + SQLTemplate: sqltemplate.New(b.dialect), + }) if err != nil { return func(yield func(resource.ResourceLastImportTime, error) bool) { yield(resource.ResourceLastImportTime{}, err) @@ -1037,3 +1039,7 @@ func (b *backend) GetResourceLastImportTimes(ctx context.Context) iter.Seq2[reso } } } + +func (b *backend) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) { + return nil, fmt.Errorf("rebuild indexes not supported by unistore sql backend") +} From 1b2fe6d6a8fb74acf1b3a98af40f3797ebb92c60 Mon Sep 17 00:00:00 2001 From: Beverly Buchanan <131809838+BeverlyJaneJ@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:44:00 -0600 Subject: [PATCH 060/378] RBAC plugin doc: add k8s rbac to list (#114265) --- .../rbac-for-app-plugins/index.md | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md index de442b3d5d6..15cf895a5e8 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md @@ -29,7 +29,7 @@ refs: - pattern: /docs/grafana/ destination: docs/grafana//administration/roles-and-permissions/access-control/custom-role-actions-scopes/#cloud-access-policies-action-definitions - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//administration/roles-and-permissions/access-control/custom-role-actions-scopes/#cloud-access-policies-action-definitions + destination: /docs/grafana//administration/roles-and-permissions/access-control/custom-role-actions-scopes/#create-access-policies rbac-role-definitions: - pattern: /docs/grafana/ destination: /docs/grafana//administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/ @@ -66,16 +66,17 @@ Please refer to plugin documentation to see what RBAC permissions the plugin has The following list contains app plugins that have fine-grained RBAC support. -| App plugin | App plugin ID | App plugin permission documentation | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) | -| [Adaptive metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) | -| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a | -| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) | -| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) | -| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a | -| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) | -| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) | +| App plugin | App plugin ID | App plugin permission documentation | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) | +| [Adaptive Metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) | +| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) | +| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a | +| [Kubernetes Monitoring](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/) | `grafana-k8s-app` | [Kubernetes Monitoring role-based access control](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/configuration/control-access/#precision-access-with-rbac-custom-plugin-roles) | +| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) | +| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) | +| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a | +| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) | ### Revoke fine-grained access from app plugins From 7299651a3220e3660a51f16a629fdc1df6adec19 Mon Sep 17 00:00:00 2001 From: Daniele Stefano Ferru Date: Fri, 21 Nov 2025 17:50:25 +0100 Subject: [PATCH 061/378] Hotfix: Granting None role Viewer access for a fixed API group list (#114310) --- pkg/services/apiserver/auth/authorizer/role.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/services/apiserver/auth/authorizer/role.go b/pkg/services/apiserver/auth/authorizer/role.go index 82e80b38164..39b3b440665 100644 --- a/pkg/services/apiserver/auth/authorizer/role.go +++ b/pkg/services/apiserver/auth/authorizer/role.go @@ -3,6 +3,7 @@ package authorizer import ( "context" "fmt" + "slices" "k8s.io/apiserver/pkg/authorization/authorizer" @@ -12,6 +13,10 @@ import ( var _ authorizer.Authorizer = &roleAuthorizer{} +var orgRoleNoneAsViewerAPIGroups = []string{ + "productactivation.ext.grafana.com", +} + type roleAuthorizer struct{} func newRoleAuthorizer() *roleAuthorizer { @@ -43,6 +48,16 @@ func (auth roleAuthorizer) Authorize(ctx context.Context, a authorizer.Attribute return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } case org.RoleNone: + // HOTFIX: granting Viewer actions to None roles to a fixed group of APIs, + // while we work on a proper fix. + if slices.Contains(orgRoleNoneAsViewerAPIGroups, a.GetAPIGroup()) { + switch a.GetVerb() { + case "get", "list", "watch": + return authorizer.DecisionAllow, "", nil + default: + return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil + } + } return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } return authorizer.DecisionDeny, "", nil From 43217025cf09f044064b3bd89354eabc66504a3c Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Fri, 21 Nov 2025 18:01:04 +0100 Subject: [PATCH 062/378] processing: set log clone as not collapsed (#114313) --- public/app/features/logs/components/panel/processing.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 7ad570acad8..21324f942ef 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -128,6 +128,7 @@ export class LogListModel implements LogRowModel { clone._wrapLogMessage = true; clone._body = undefined; clone._highlightTokens = undefined; + clone.collapsed = false; return clone; } From 0e460a267e1e9be1637658febacbf2d7652546ac Mon Sep 17 00:00:00 2001 From: Denis Vodopianov Date: Fri, 21 Nov 2025 18:43:42 +0100 Subject: [PATCH 063/378] chore : Deprecating FeatureToggles.IsEnabled (#113062) * Deprecating features.IsEnabled * add one more nolint * add one more nolint * Give better hints to devs in the deprecation message of IsEnabledGlobally * adding more doc strings * fix linter after rebase * Extend deprecation message --- pkg/api/accesscontrol.go | 2 ++ pkg/api/annotations.go | 9 ++++++ pkg/api/dashboard.go | 1 + pkg/api/frontendsettings.go | 4 +++ pkg/api/index.go | 2 ++ pkg/api/login.go | 1 + pkg/api/pluginproxy/pluginproxy.go | 1 + pkg/api/plugins.go | 2 ++ pkg/expr/converter.go | 1 + pkg/expr/dataplane.go | 1 + pkg/expr/graph.go | 2 +- pkg/middleware/loggermw/logger.go | 1 + pkg/middleware/request_metrics.go | 1 + .../apis/dashboard/legacy/sql_dashboards.go | 32 ++++++++++--------- .../apis/dashboard/schema_validation.go | 3 ++ pkg/services/accesscontrol/acimpl/service.go | 2 ++ .../ossaccesscontrol/dashboard.go | 3 ++ .../accesscontrol/accesscontrol.go | 2 ++ pkg/services/authn/authnimpl/registration.go | 1 + pkg/services/caching/service.go | 1 + pkg/services/featuremgmt/models.go | 8 ++++- pkg/services/libraryelements/api.go | 1 + pkg/services/navtree/navtreeimpl/admin.go | 3 ++ pkg/services/navtree/navtreeimpl/navtree.go | 4 +++ pkg/services/ngalert/api/api_configuration.go | 1 + pkg/services/ngalert/api/api_testing.go | 3 ++ pkg/services/ngalert/ngalert.go | 4 +++ pkg/services/ngalert/sender/router.go | 1 + pkg/services/ngalert/store/alert_rule.go | 1 + .../pluginsintegration/installsync/syncer.go | 2 ++ pkg/services/preference/prefapi/api.go | 2 ++ pkg/services/promtypemigration/migrator.go | 1 + pkg/services/rendering/auth.go | 1 + pkg/services/secrets/manager/manager.go | 4 +++ 34 files changed, 91 insertions(+), 17 deletions(-) diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 1bddc9490cc..81d866fe04f 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -313,6 +313,7 @@ func (hs *HTTPServer) declareFixedRoles() error { Grants: []string{string(org.RoleEditor)}, } + //nolint:staticcheck // not yet migrated to OpenFeature if hs.Features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { // Keeping the name to avoid breaking changes (for users who have assigned this role to grant permissions on organization annotations) annotationsReaderRole = ac.RoleRegistration{ @@ -619,6 +620,7 @@ func (hs *HTTPServer) declareFixedRoles() error { libraryPanelsReaderRole, libraryPanelsWriterRole, libraryPanelsGeneralReaderRole, libraryPanelsGeneralWriterRole, snapshotsCreatorRole, snapshotsDeleterRole, snapshotsReaderRole} + //nolint:staticcheck // not yet migrated to OpenFeature if hs.Features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { allAnnotationsReaderRole := ac.RoleRegistration{ Role: ac.RoleDTO{ diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 287db4e04df..2a1b0216701 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -126,6 +126,7 @@ func (hs *HTTPServer) PostAnnotation(c *contextmodel.ReqContext) response.Respon } if canSave, err := hs.canCreateAnnotation(c, cmd.DashboardUID); err != nil || !canSave { + //nolint:staticcheck // not yet migrated to OpenFeature if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { return dashboardGuardianResponse(err) } else if err != nil { @@ -271,6 +272,7 @@ func (hs *HTTPServer) UpdateAnnotation(c *contextmodel.ReqContext) response.Resp return resp } + //nolint:staticcheck // not yet migrated to OpenFeature if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { if canSave, err := hs.canSaveAnnotation(c, hs.AccessControl, annotation); err != nil || !canSave { return dashboardGuardianResponse(err) @@ -329,6 +331,7 @@ func (hs *HTTPServer) PatchAnnotation(c *contextmodel.ReqContext) response.Respo return resp } + //nolint:staticcheck // not yet migrated to OpenFeature if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { if canSave, err := hs.canSaveAnnotation(c, hs.AccessControl, annotation); err != nil || !canSave { return dashboardGuardianResponse(err) @@ -439,6 +442,7 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *contextmodel.ReqContext) response canSave, err := hs.canMassDeleteAnnotations(c, dashboardUID) if err != nil || !canSave { + //nolint:staticcheck // not yet migrated to OpenFeature if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { return dashboardGuardianResponse(err) } else if err != nil { @@ -500,6 +504,7 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *contextmodel.ReqContext) response. return response.Error(http.StatusBadRequest, "annotationId is invalid", err) } + //nolint:staticcheck // not yet migrated to OpenFeature if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { annotation, resp := findAnnotationByID(c.Req.Context(), hs.annotationsRepo, annotationID, c.SignedInUser) if resp != nil { @@ -610,6 +615,7 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature }, } + //nolint:staticcheck // not yet migrated to OpenFeature if features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) { tempUser = &user.SignedInUser{ OrgID: orgID, @@ -626,6 +632,7 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature return nil, errors.New("could not resolve annotation type") } + //nolint:staticcheck // not yet migrated to OpenFeature if !features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) { switch annotation.GetType() { case annotations.Organization: @@ -662,6 +669,7 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature } func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardUID string) (bool, error) { + //nolint:staticcheck // not yet migrated to OpenFeature if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { if dashboardUID != "" { evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboardUID)) @@ -686,6 +694,7 @@ func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardU } func (hs *HTTPServer) canMassDeleteAnnotations(c *contextmodel.ReqContext, dashboardUID string) (bool, error) { + //nolint:staticcheck // not yet migrated to OpenFeature if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) { if dashboardUID == "" { evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeOrganization) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 7bbf48ab0f2..1383f007c49 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -166,6 +166,7 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response } annotationPermissions := &dashboardsV1.AnnotationPermission{} + //nolint:staticcheck // not yet migrated to OpenFeature if hs.Features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) { hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Dashboard, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID)) } else { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 3554eb84244..8acfdea512e 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -150,6 +150,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro continue } + //nolint:staticcheck // not yet migrated to OpenFeature if panel.ID == "datagrid" && !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagEnableDatagridEditing) { continue } @@ -190,6 +191,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro hasAccess := accesscontrol.HasAccess(hs.AccessControl, c) trustedTypesDefaultPolicyEnabled := (hs.Cfg.CSPEnabled && strings.Contains(hs.Cfg.CSPTemplate, "require-trusted-types-for")) || (hs.Cfg.CSPReportOnlyEnabled && strings.Contains(hs.Cfg.CSPReportOnlyTemplate, "require-trusted-types-for")) + //nolint:staticcheck // not yet migrated to OpenFeature isCloudMigrationTarget := hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagOnPremToCloudMigrations) && hs.Cfg.CloudMigration.IsTarget featureToggles := hs.Features.GetEnabled(c.Req.Context()) // this is needed for backwards compatibility with external plugins @@ -406,6 +408,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro DisableSignoutMenu: hs.Cfg.DisableSignoutMenu, } + //nolint:staticcheck // not yet migrated to OpenFeature if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) { hasEnabledProviders := hs.samlEnabled() || hs.authnService.IsClientEnabled(authn.ClientLDAP) @@ -444,6 +447,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro frontendSettings.Namespace = hs.namespacer(c.OrgID) // experimental scope features + //nolint:staticcheck // not yet migrated to OpenFeature if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagScopeFilters) { frontendSettings.ListScopesEndpoint = hs.Cfg.ScopesListScopesURL frontendSettings.ListDashboardScopesEndpoint = hs.Cfg.ScopesListDashboardsURL diff --git a/pkg/api/index.go b/pkg/api/index.go index afbebb3b013..197a83d1013 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -62,6 +62,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV return nil, err } + //nolint:staticcheck // not yet migrated to OpenFeature if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagIndividualCookiePreferences) { if !prefs.Cookies("analytics") { settings.GoogleAnalytics4Id = "" @@ -94,6 +95,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV } var regionalFormat string + //nolint:staticcheck // not yet migrated to OpenFeature if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagLocaleFormatPreference) { regionalFormat = "en" diff --git a/pkg/api/login.go b/pkg/api/login.go index 89f452cd103..8c0bd9df5be 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -361,6 +361,7 @@ func (hs *HTTPServer) RedirectResponseWithError(c *contextmodel.ReqContext, err func (hs *HTTPServer) redirectURLWithErrorCookie(c *contextmodel.ReqContext, err error) string { setCookie := true + //nolint:staticcheck // not yet migrated to OpenFeature if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagIndividualCookiePreferences) { var userID int64 if c.SignedInUser != nil && !c.IsNil() { diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go index d6a333a05d1..0c20a98d2d3 100644 --- a/pkg/api/pluginproxy/pluginproxy.go +++ b/pkg/api/pluginproxy/pluginproxy.go @@ -81,6 +81,7 @@ func (proxy *PluginProxy) HandleRequest() { hasSlash := strings.HasSuffix(proxy.proxyPath, "/") proxy.proxyPath = path + //nolint:staticcheck // not yet migrated to OpenFeature if hasSlash && !strings.HasSuffix(path, "/") && proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagPluginProxyPreserveTrailingSlash) { proxy.proxyPath += "/" } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 517d2c3b295..4fa7292dcb5 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -144,6 +144,7 @@ func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Respons AngularDetected: pluginDef.Angular.Detected, } + //nolint:staticcheck // not yet migrated to OpenFeature if hs.Cfg.ManagedServiceAccountsEnabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) { listItem.IAM = pluginDef.IAM } @@ -490,6 +491,7 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons return response.ErrOrFallback(http.StatusInternalServerError, "Failed to install plugin", err) } + //nolint:staticcheck // not yet migrated to OpenFeature if hs.Cfg.ManagedServiceAccountsEnabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) { // This is a non-blocking function that verifies that the installer has // the permissions that the plugin requests to have on Grafana. diff --git a/pkg/expr/converter.go b/pkg/expr/converter.go index 3017afaeef1..70c20a5b47e 100644 --- a/pkg/expr/converter.go +++ b/pkg/expr/converter.go @@ -29,6 +29,7 @@ func (c *ResultConverter) Convert(ctx context.Context, } var dt data.FrameType + //nolint:staticcheck // not yet migrated to OpenFeature dt, useDataplane, _ := shouldUseDataplane(frames, logger, c.Features.IsEnabled(ctx, featuremgmt.FlagDisableSSEDataplane)) if useDataplane { logger.Debug("Handling SSE data source query through dataplane", "datatype", dt) diff --git a/pkg/expr/dataplane.go b/pkg/expr/dataplane.go index 3f79b11c428..c0931c4f3ed 100644 --- a/pkg/expr/dataplane.go +++ b/pkg/expr/dataplane.go @@ -70,6 +70,7 @@ func handleDataplaneFrames(ctx context.Context, tracer tracing.Tracer, features case data.KindTimeSeries: return handleDataplaneTimeseries(frames) case data.KindNumeric: + //nolint:staticcheck // not yet migrated to OpenFeature sortMetrics := !features.IsEnabled(ctx, featuremgmt.FlagDisableNumericMetricsSortingInExpressions) return handleDataplaneNumeric(frames, sortMetrics) default: diff --git a/pkg/expr/graph.go b/pkg/expr/graph.go index ea711b635f3..25b2359cf38 100644 --- a/pkg/expr/graph.go +++ b/pkg/expr/graph.go @@ -68,7 +68,7 @@ type DataPipeline []Node // map of the refId of the of each command func (dp *DataPipeline) execute(c context.Context, now time.Time, s *Service) (mathexp.Vars, error) { vars := make(mathexp.Vars) - + //nolint:staticcheck // not yet migrated to OpenFeature groupByDSFlag := s.features.IsEnabled(c, featuremgmt.FlagSseGroupByDatasource) // Execute datasource nodes first, and grouped by datasource. if groupByDSFlag { diff --git a/pkg/middleware/loggermw/logger.go b/pkg/middleware/loggermw/logger.go index e83129cec3a..1255d87aa0e 100644 --- a/pkg/middleware/loggermw/logger.go +++ b/pkg/middleware/loggermw/logger.go @@ -64,6 +64,7 @@ func (l *loggerImpl) Middleware() web.Middleware { // put the start time on context so we can measure it later. r = r.WithContext(log.InitstartTime(r.Context(), time.Now())) + //nolint:staticcheck // not yet migrated to OpenFeature if l.flags.IsEnabled(r.Context(), featuremgmt.FlagUnifiedRequestLog) { r = r.WithContext(errutil.SetUnifiedLogging(r.Context())) } diff --git a/pkg/middleware/request_metrics.go b/pkg/middleware/request_metrics.go index cdcaacd6e51..9e32b75f7c9 100644 --- a/pkg/middleware/request_metrics.go +++ b/pkg/middleware/request_metrics.go @@ -114,6 +114,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR handler = "notfound" } else { // log requests where we could not identify handler so we can register them. + //nolint:staticcheck // not yet migrated to OpenFeature if features.IsEnabled(r.Context(), featuremgmt.FlagLogRequestsInstrumentedAsUnknown) { log.Warn("request instrumented as unknown", "path", r.URL.Path, "status_code", status) } diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index d4997c39c7d..81ed308cdcc 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -109,13 +109,14 @@ func ProvideMigratorDashboardAccessor( features featuremgmt.FeatureToggles, ) MigrationDashboardAccessor { return &dashboardSqlAccess{ - sql: sql, - namespacer: claims.OrgNamespaceFormatter, - dashStore: nil, // not needed for migration - provisioning: provisioning, - dashboardPermissionSvc: nil, // not needed for migration - libraryPanelSvc: nil, // not needed for migration - accessControl: accessControl, + sql: sql, + namespacer: claims.OrgNamespaceFormatter, + dashStore: nil, // not needed for migration + provisioning: provisioning, + dashboardPermissionSvc: nil, // not needed for migration + libraryPanelSvc: nil, // not needed for migration + accessControl: accessControl, + //nolint:staticcheck // not yet migrated to OpenFeature invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled), } } @@ -132,14 +133,15 @@ func NewDashboardSQLAccess(sql legacysql.LegacyDatabaseProvider, ) *dashboardSqlAccess { dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter) return &dashboardSqlAccess{ - sql: sql, - namespacer: namespacer, - dashStore: dashStore, - provisioning: provisioning, - dashboardSearchClient: *dashboardSearchClient, - dashboardPermissionSvc: dashboardPermissionSvc, - libraryPanelSvc: libraryPanelSvc, - accessControl: accessControl, + sql: sql, + namespacer: namespacer, + dashStore: dashStore, + provisioning: provisioning, + dashboardSearchClient: *dashboardSearchClient, + dashboardPermissionSvc: dashboardPermissionSvc, + libraryPanelSvc: libraryPanelSvc, + accessControl: accessControl, + //nolint:staticcheck // not yet migrated to OpenFeature invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled), } } diff --git a/pkg/registry/apis/dashboard/schema_validation.go b/pkg/registry/apis/dashboard/schema_validation.go index c53ed82481a..375455470c8 100644 --- a/pkg/registry/apis/dashboard/schema_validation.go +++ b/pkg/registry/apis/dashboard/schema_validation.go @@ -33,9 +33,11 @@ func (b *DashboardsAPIBuilder) ValidateDashboardSpec(ctx context.Context, obj ru case *v0.Dashboard: errorOnSchemaMismatches = false // Never error for v0 case *v1.Dashboard: + //nolint:staticcheck // not yet migrated to OpenFeature errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV1) case *v2alpha1.Dashboard: case *v2beta1.Dashboard: + //nolint:staticcheck // not yet migrated to OpenFeature errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV2) default: return nil, fmt.Errorf("invalid dashboard type: %T", obj) @@ -45,6 +47,7 @@ func (b *DashboardsAPIBuilder) ValidateDashboardSpec(ctx context.Context, obj ru return nil, apierrors.NewBadRequest("Not supported: FieldValidationMode: Warn") } + //nolint:staticcheck // not yet migrated to OpenFeature alwaysLogSchemaValidationErrors := b.features.IsEnabled(ctx, featuremgmt.FlagDashboardSchemaValidationLogging) var errors field.ErrorList diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 3c1deeb1935..1ea8bf95f77 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -741,6 +741,7 @@ func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.SaveExternalServiceRole") defer span.End() + //nolint:staticcheck // not yet migrated to OpenFeature if !s.cfg.ManagedServiceAccountsEnabled || !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { s.log.Debug("Registering an external service role is behind a feature flag, enable it to use this feature.") return nil @@ -757,6 +758,7 @@ func (s *Service) DeleteExternalServiceRole(ctx context.Context, externalService ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.DeleteExternalServiceRole") defer span.End() + //nolint:staticcheck // not yet migrated to OpenFeature if !s.cfg.ManagedServiceAccountsEnabled || !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { s.log.Debug("Deleting an external service role is behind a feature flag, enable it to use this feature.") return nil diff --git a/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go b/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go index a588ac14ce7..317585ef8ac 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go @@ -28,6 +28,7 @@ var DashboardEditActions = append(DashboardViewActions, []string{dashboards.Acti var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.ActionDashboardsPermissionsRead, dashboards.ActionDashboardsPermissionsWrite}...) func getDashboardViewActions(features featuremgmt.FeatureToggles) []string { + //nolint:staticcheck // not yet migrated to OpenFeature if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { return append(DashboardViewActions, accesscontrol.ActionAnnotationsRead) } @@ -35,6 +36,7 @@ func getDashboardViewActions(features featuremgmt.FeatureToggles) []string { } func getDashboardEditActions(features featuremgmt.FeatureToggles) []string { + //nolint:staticcheck // not yet migrated to OpenFeature if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { return append(DashboardEditActions, []string{accesscontrol.ActionAnnotationsRead, accesscontrol.ActionAnnotationsWrite, accesscontrol.ActionAnnotationsDelete, accesscontrol.ActionAnnotationsCreate}...) } @@ -42,6 +44,7 @@ func getDashboardEditActions(features featuremgmt.FeatureToggles) []string { } func getDashboardAdminActions(features featuremgmt.FeatureToggles) []string { + //nolint:staticcheck // not yet migrated to OpenFeature if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { return append(DashboardAdminActions, []string{accesscontrol.ActionAnnotationsRead, accesscontrol.ActionAnnotationsWrite, accesscontrol.ActionAnnotationsDelete, accesscontrol.ActionAnnotationsCreate}...) } diff --git a/pkg/services/annotations/accesscontrol/accesscontrol.go b/pkg/services/annotations/accesscontrol/accesscontrol.go index 1627469f213..19260e63c27 100644 --- a/pkg/services/annotations/accesscontrol/accesscontrol.go +++ b/pkg/services/annotations/accesscontrol/accesscontrol.go @@ -61,6 +61,7 @@ func (authz *AuthService) Authorize(ctx context.Context, query annotations.ItemQ scopeTypes := annotationScopeTypes(scopes) _, canAccessOrgAnnotations := scopeTypes[annotations.Organization.String()] _, canAccessDashAnnotations := scopeTypes[annotations.Dashboard.String()] + //nolint:staticcheck // not yet migrated to OpenFeature if authz.features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) { canAccessDashAnnotations = true } @@ -122,6 +123,7 @@ func (authz *AuthService) dashboardsWithVisibleAnnotations(ctx context.Context, } filterType := searchstore.TypeDashboard + //nolint:staticcheck // not yet migrated to OpenFeature if authz.features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) { filterType = searchstore.TypeAnnotation } diff --git a/pkg/services/authn/authnimpl/registration.go b/pkg/services/authn/authnimpl/registration.go index 434ffabdea9..19cbe411b78 100644 --- a/pkg/services/authn/authnimpl/registration.go +++ b/pkg/services/authn/authnimpl/registration.go @@ -83,6 +83,7 @@ func ProvideRegistration( } } + //nolint:staticcheck // not yet migrated to OpenFeature if cfg.PasswordlessMagicLinkAuth.Enabled && features.IsEnabled(context.Background(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) { hasEnabledProviders := authnSvc.IsClientEnabled(authn.ClientSAML) || authnSvc.IsClientEnabled(authn.ClientLDAP) if !hasEnabledProviders { diff --git a/pkg/services/caching/service.go b/pkg/services/caching/service.go index c5bc577b122..d874554cae9 100644 --- a/pkg/services/caching/service.go +++ b/pkg/services/caching/service.go @@ -210,6 +210,7 @@ func (c *CachingServiceClient) WithQueryDataCaching(ctx context.Context, req *ba // Update the query cache with the result for this metrics request if err == nil && cr.UpdateCacheFn != nil { // If AWS async caching is not enabled, use the old code path + //nolint:staticcheck // not yet migrated to OpenFeature if c.features == nil || !c.features.IsEnabled(ctx, featuremgmt.FlagAwsAsyncQueryCaching) { cr.UpdateCacheFn(ctx, resp) } else if reqCtx != nil { diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go index 45d3dc810f4..304d17ee075 100644 --- a/pkg/services/featuremgmt/models.go +++ b/pkg/services/featuremgmt/models.go @@ -10,6 +10,10 @@ import ( type FeatureToggles interface { // IsEnabled checks if a feature is enabled for a given context. // The settings may be per user, tenant, or globally set in the cloud + // + // Deprecated: FeatureToggles.IsEnabled is deprecated and will be removed in a future release. + // Evaluate with OpenFeature instead (see [github.com/open-feature/go-sdk/openfeature.Client]), for example: + // openfeature.NewDefaultClient().Boolean(ctx, "your-flag", false, openfeature.TransactionContext(ctx)) IsEnabled(ctx context.Context, flag string) bool // IsEnabledGlobally checks if a flag is configured globally. For now, this is the same @@ -19,7 +23,9 @@ type FeatureToggles interface { // a full server restart for a change to take place. // // Deprecated: FeatureToggles.IsEnabledGlobally is deprecated and will be removed in a future release. - // Evaluate with OpenFeature instead (see [github.com/open-feature/go-sdk/openfeature.Client]) + // Toggles that must be reliably evaluated at the service startup should be + // changed to settings (see setting.StartupSettings), and/or removed entirely. + // For app registration please use `grafana-apiserver.runtime_config` in settings.ini IsEnabledGlobally(flag string) bool // Get the enabled flags -- this *may* also include disabled flags (with value false) diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 312905cf0c8..4895e7e6505 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -148,6 +148,7 @@ func (l *LibraryElementService) deleteHandler(c *contextmodel.ReqContext) respon // 404: notFoundError // 500: internalServerError func (l *LibraryElementService) getHandler(c *contextmodel.ReqContext) response.Response { + //nolint:staticcheck // not yet migrated to OpenFeature if l.features.IsEnabled(c.Req.Context(), featuremgmt.FlagKubernetesLibraryPanels) { l.k8sHandler.getK8sLibraryElement(c) return nil // already handled in the k8s handler diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 7e2af182a87..a12ed52b2f6 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -44,6 +44,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink Text: "Organizations", SubTitle: "Isolated instances of Grafana running on the same server", Id: "global-orgs", Url: s.cfg.AppSubURL + "/admin/orgs", Icon: "building", }) } + //nolint:staticcheck // not yet migrated to OpenFeature if hasAccess(cloudmigration.MigrationAssistantAccess) && s.features.IsEnabled(ctx, featuremgmt.FlagOnPremToCloudMigrations) { generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ Text: "Migrate to Grafana Cloud", @@ -99,6 +100,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink }) } + //nolint:staticcheck // not yet migrated to OpenFeature if (s.cfg.Env == setting.Dev) || s.features.IsEnabled(ctx, featuremgmt.FlagEnableExtensionsAdminPage) && hasAccess(pluginaccesscontrol.AdminAccessEvaluator) { pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{ Text: "Extensions", @@ -147,6 +149,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink }) } + //nolint:staticcheck // not yet migrated to OpenFeature if s.license.FeatureEnabled("groupsync") && s.features.IsEnabled(ctx, featuremgmt.FlagGroupAttributeSync) && hasAccess(ac.EvalAny( diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 0c059c204b5..5b9eb985bd8 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -407,6 +407,7 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt }) } + //nolint:staticcheck // not yet migrated to OpenFeature if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagRestoreDashboards) && (c.GetOrgRole() == org.RoleAdmin || c.IsGrafanaAdmin) { dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{ Text: "Recently deleted", @@ -435,6 +436,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na hasAccess := ac.HasAccess(s.accessControl, c) var alertChildNavs []*navtree.NavLink + //nolint:staticcheck // not yet migrated to OpenFeature if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingTriage) { if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) { alertChildNavs = append(alertChildNavs, &navtree.NavLink{ @@ -492,6 +494,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Alert groups", SubTitle: "See grouped alerts with active notifications", Id: "groups", Url: s.cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"}) } + //nolint:staticcheck // not yet migrated to OpenFeature if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingCentralAlertHistory) { if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead))) { alertChildNavs = append(alertChildNavs, &navtree.NavLink{ @@ -503,6 +506,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na }) } } + //nolint:staticcheck // not yet migrated to OpenFeature if c.GetOrgRole() == org.RoleAdmin && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertRuleRestore) && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingRuleRecoverDeleted) { alertChildNavs = append(alertChildNavs, &navtree.NavLink{ Text: "Recently deleted", diff --git a/pkg/services/ngalert/api/api_configuration.go b/pkg/services/ngalert/api/api_configuration.go index 20f131f927b..01e0291c585 100644 --- a/pkg/services/ngalert/api/api_configuration.go +++ b/pkg/services/ngalert/api/api_configuration.go @@ -77,6 +77,7 @@ func (srv ConfigSrv) RoutePostNGalertConfig(c *contextmodel.ReqContext, body api return response.Error(http.StatusBadRequest, "Invalid alertmanager choice specified", err) } + //nolint:staticcheck // not yet migrated to OpenFeature disableExternal := srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingDisableSendAlertsExternal) if disableExternal && sendAlertsTo != ngmodels.InternalAlertmanager { return response.Error(http.StatusBadRequest, "Sending alerts to external alertmanagers is disallowed on this instance", err) diff --git a/pkg/services/ngalert/api/api_testing.go b/pkg/services/ngalert/api/api_testing.go index ceda51a7a22..3bda2e3f28f 100644 --- a/pkg/services/ngalert/api/api_testing.go +++ b/pkg/services/ngalert/api/api_testing.go @@ -79,6 +79,7 @@ func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *contextmodel.ReqContext, return response.ErrOrFallback(http.StatusInternalServerError, "failed to authorize access to rule group", err) } + //nolint:staticcheck // not yet migrated to OpenFeature if srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingQueryOptimization) { if _, err := store.OptimizeAlertQueries(rule.Data); err != nil { return ErrResp(http.StatusInternalServerError, err, "Failed to optimize query") @@ -178,6 +179,7 @@ func (srv TestingApiSrv) RouteEvalQueries(c *contextmodel.ReqContext, cmd apimod } var optimizations []store.Optimization + //nolint:staticcheck // not yet migrated to OpenFeature if srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingQueryOptimization) { var err error optimizations, err = store.OptimizeAlertQueries(cond.Data) @@ -223,6 +225,7 @@ func addOptimizedQueryWarnings(evalResults *backend.QueryDataResponse, optimizat } func (srv TestingApiSrv) BacktestAlertRule(c *contextmodel.ReqContext, cmd apimodels.BacktestConfig) response.Response { + //nolint:staticcheck // not yet migrated to OpenFeature if !srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingBacktesting) { return ErrResp(http.StatusNotFound, nil, "Backgtesting API is not enabled") } diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 299a511c35e..53e49621117 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -194,8 +194,11 @@ func (ng *AlertNG) init() error { var opts []notifier.Option moaLogger := log.New("ngalert.multiorg.alertmanager") crypto := notifier.NewCrypto(ng.SecretsService, ng.store, moaLogger) + //nolint:staticcheck // not yet migrated to OpenFeature remotePrimary := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemotePrimary) + //nolint:staticcheck // not yet migrated to OpenFeature remoteSecondary := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemoteSecondary) + //nolint:staticcheck // not yet migrated to OpenFeature remoteSecondaryWithRemoteState := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemoteSecondaryWithRemoteState) if remotePrimary || remoteSecondary || remoteSecondaryWithRemoteState { m := ng.Metrics.GetRemoteAlertmanagerMetrics() @@ -717,6 +720,7 @@ func configureNotificationHistorian( l log.Logger, tracer tracing.Tracer, ) (nfstatus.NotificationHistorian, error) { + //nolint:staticcheck // not yet migrated to OpenFeature if !featureToggles.IsEnabled(ctx, featuremgmt.FlagAlertingNotificationHistory) || !cfg.Enabled { met.Info.Set(0) return nil, nil diff --git a/pkg/services/ngalert/sender/router.go b/pkg/services/ngalert/sender/router.go index ea2648feac3..7f1f8f3a897 100644 --- a/pkg/services/ngalert/sender/router.go +++ b/pkg/services/ngalert/sender/router.go @@ -89,6 +89,7 @@ func (d *AlertsRouter) SyncAndApplyConfigFromDatabase(ctx context.Context) error d.logger.Debug("Attempting to sync admin configs", "count", len(cfgs)) + //nolint:staticcheck // not yet migrated to OpenFeature disableExternal := d.featureManager.IsEnabled(ctx, featuremgmt.FlagAlertingDisableSendAlertsExternal) orgsFound := make(map[int64]struct{}, len(cfgs)) diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index b05ba01a562..076974ea9c9 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -1097,6 +1097,7 @@ func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodel continue } } + //nolint:staticcheck // not yet migrated to OpenFeature if st.FeatureToggles.IsEnabled(ctx, featuremgmt.FlagAlertingQueryOptimization) { if optimizations, err := OptimizeAlertQueries(converted.Data); err != nil { st.Logger.Error("Could not migrate rule from range to instant query", "rule", rule.UID, "err", err) diff --git a/pkg/services/pluginsintegration/installsync/syncer.go b/pkg/services/pluginsintegration/installsync/syncer.go index 1f056a6e85b..8639834610f 100644 --- a/pkg/services/pluginsintegration/installsync/syncer.go +++ b/pkg/services/pluginsintegration/installsync/syncer.go @@ -87,10 +87,12 @@ func ProvideSyncer( } func (s *syncer) Sync(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error { + //nolint:staticcheck // not yet migrated to OpenFeature if !s.featureToggles.IsEnabled(ctx, featuremgmt.FlagPluginInstallAPISync) { return nil } + //nolint:staticcheck // not yet migrated to OpenFeature if !s.featureToggles.IsEnabled(ctx, featuremgmt.FlagPluginStoreServiceLoading) { logging.DefaultLogger.Warn("pluginInstallAPISync is enabled, but pluginStoreServiceLoading is disabled. skipping plugin sync.") return nil diff --git a/pkg/services/preference/prefapi/api.go b/pkg/services/preference/prefapi/api.go index 1375bb27f28..6bf8057ac68 100644 --- a/pkg/services/preference/prefapi/api.go +++ b/pkg/services/preference/prefapi/api.go @@ -61,6 +61,7 @@ func UpdatePreferencesFor(ctx context.Context, Navbar: dtoCmd.Navbar, } + //nolint:staticcheck // not yet migrated to OpenFeature if features.IsEnabled(ctx, featuremgmt.FlagLocaleFormatPreference) { saveCmd.RegionalFormat = dtoCmd.RegionalFormat } @@ -101,6 +102,7 @@ func GetPreferencesFor(ctx context.Context, dto.Language = &preference.JSONData.Language } + //nolint:staticcheck // not yet migrated to OpenFeature if features.IsEnabled(ctx, featuremgmt.FlagLocaleFormatPreference) { if preference.JSONData.RegionalFormat != "" { dto.RegionalFormat = &preference.JSONData.RegionalFormat diff --git a/pkg/services/promtypemigration/migrator.go b/pkg/services/promtypemigration/migrator.go index abdc80fc7e7..2bccc6ea822 100644 --- a/pkg/services/promtypemigration/migrator.go +++ b/pkg/services/promtypemigration/migrator.go @@ -43,6 +43,7 @@ func ProvidePromTypeMigrationProvider( } func (s *PromTypeMigrationProviderImpl) Run(ctx context.Context) error { + //nolint:staticcheck // not yet migrated to OpenFeature if !s.features.IsEnabled(ctx, featuremgmt.FlagPrometheusTypeMigration) { return nil } diff --git a/pkg/services/rendering/auth.go b/pkg/services/rendering/auth.go index 450d99d241c..25364fce3d9 100644 --- a/pkg/services/rendering/auth.go +++ b/pkg/services/rendering/auth.go @@ -37,6 +37,7 @@ func (rs *RenderingService) GetRenderUser(ctx context.Context, key string) (*Ren var renderUser *RenderUser + //nolint:staticcheck // not yet migrated to OpenFeature if looksLikeJWT(key) && rs.features.IsEnabled(ctx, featuremgmt.FlagRenderAuthJWT) { from = "jwt" renderUser = rs.getRenderUserFromJWT(key) diff --git a/pkg/services/secrets/manager/manager.go b/pkg/services/secrets/manager/manager.go index ced2def7325..21c978d0528 100644 --- a/pkg/services/secrets/manager/manager.go +++ b/pkg/services/secrets/manager/manager.go @@ -122,6 +122,7 @@ func (s *SecretsService) registerUsageMetrics() { // Enabled / disabled usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 0 + //nolint:staticcheck // not yet migrated to OpenFeature if !s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) { usageMetrics["stats.encryption.envelope_encryption_enabled.count"] = 1 } @@ -167,6 +168,7 @@ func (s *SecretsService) Encrypt(ctx context.Context, payload []byte, opt secret defer span.End() // Use legacy encryption service if featuremgmt.FlagDisableEnvelopeEncryption toggle is on + //nolint:staticcheck // not yet migrated to OpenFeature if s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) { return s.enc.Encrypt(ctx, payload, s.cfg.SecretKey) } @@ -343,6 +345,7 @@ func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, e // If encrypted with envelope encryption, the feature is disabled and // no provider is initialized, then we throw an error. + //nolint:staticcheck // not yet migrated to OpenFeature if s.encryptedWithEnvelopeEncryption(payload) && s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) && !s.providersInitialized() { @@ -480,6 +483,7 @@ func (s *SecretsService) RotateDataKeys(ctx context.Context) error { func (s *SecretsService) ReEncryptDataKeys(ctx context.Context) error { s.log.Info("Data keys re-encryption triggered") + //nolint:staticcheck // not yet migrated to OpenFeature if s.features.IsEnabled(ctx, featuremgmt.FlagDisableEnvelopeEncryption) { s.log.Info("Envelope encryption is not enabled but trying to init providers anyway...") From 8cf777b5e80210da8aefb076f9e1d77e47e35982 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:42:41 -0600 Subject: [PATCH 064/378] VizSuggestions: Empty state (#114049) * newVizSuggestions feature toggle * panel empty state * add data check * select first suggestion * update text * PanelEmptyState component * add test * ? * remove fake translation * updates * move empty state to UnconfiguredPanel --- .../panel-edit/PanelEditor.tsx | 39 ++++++++++- .../scene/UnconfiguredPanel.tsx | 69 ++++++++++++++++--- .../features/dashboard-scene/utils/utils.ts | 12 +++- .../VisualizationSuggestions.tsx | 31 ++++++++- public/locales/en-US/grafana.json | 6 +- 5 files changed, 144 insertions(+), 13 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 1fe646ca69b..1ff7eef89ca 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -13,6 +13,7 @@ import { SceneObjectState, SceneObjectStateChangedEvent, SceneQueryRunner, + sceneGraph, sceneUtils, VizPanel, isSceneObject, @@ -21,6 +22,7 @@ import { Panel } from '@grafana/schema'; import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions'; import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard'; import { saveLibPanel } from 'app/features/library-panels/state/api'; +import { getAllSuggestions } from 'app/features/panel/suggestions/getAllSuggestions'; import { DashboardEditActionEvent } from '../edit-pane/shared'; import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker'; @@ -29,6 +31,7 @@ import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DashboardLayoutItem, isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem'; import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; +import { PanelModelCompatibilityWrapper } from '../utils/PanelModelCompatibilityWrapper'; import { activateSceneObjectAndParentTree, getDashboardSceneFor, @@ -84,7 +87,11 @@ export class PanelEditor extends SceneObjectBase { const panel = this.state.panelRef.resolve(); if (panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) { - panel.changePluginType('timeseries'); + if (config.featureToggles.newVizSuggestions) { + this._autoSelectVisualization(panel); + } else { + panel.changePluginType('timeseries'); + } } this._subs.add( @@ -117,6 +124,31 @@ export class PanelEditor extends SceneObjectBase { }; } + private async _autoSelectVisualization(panel: VizPanel) { + const dataObject = sceneGraph.getData(panel); + + this._subs.add( + dataObject.subscribeToState(async () => { + const { data } = dataObject.state; + const hasData = data && data.series && data.series.length > 0 && data.series.some((frame) => frame.length > 0); + + if (hasData && panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) { + const panelModel = new PanelModelCompatibilityWrapper(panel); + const suggestions = await getAllSuggestions(data, panelModel); + + if (suggestions.length > 0) { + const defaultFirstSuggestion = suggestions[0]; + await panel.changePluginType( + defaultFirstSuggestion.pluginId, + defaultFirstSuggestion.options, + defaultFirstSuggestion.fieldConfig + ); + } + } + }) + ); + } + private commitChanges() { if (!this.state.isDirty && !this._changesHaveBeenMade) { // Nothing to commit @@ -228,12 +260,17 @@ export class PanelEditor extends SceneObjectBase { }) ); + const isUnconfigured = Boolean( + config.featureToggles.newVizSuggestions && panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID + ); + // Setup options pane this.setState({ optionsPane: new PanelOptionsPane({ panelRef: this.state.panelRef, searchQuery: '', listMode: OptionFilter.All, + isVizPickerOpen: isUnconfigured, }), isInitializing: false, }); diff --git a/public/app/features/dashboard-scene/scene/UnconfiguredPanel.tsx b/public/app/features/dashboard-scene/scene/UnconfiguredPanel.tsx index eda81bf019a..2be7201ad74 100644 --- a/public/app/features/dashboard-scene/scene/UnconfiguredPanel.tsx +++ b/public/app/features/dashboard-scene/scene/UnconfiguredPanel.tsx @@ -1,11 +1,13 @@ -import { useCallback, useState } from 'react'; +import { css } from '@emotion/css'; +import { useCallback, useEffect, useState } from 'react'; -import { PanelPlugin, PanelProps } from '@grafana/data'; +import { CoreApp, GrafanaTheme2, PanelPlugin, PanelProps } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { locationService } from '@grafana/runtime'; +import { config, locationService } from '@grafana/runtime'; import { sceneUtils } from '@grafana/scenes'; -import { Box, Button, ButtonGroup, Dropdown, Menu, Stack } from '@grafana/ui'; +import { Box, Button, ButtonGroup, Dropdown, Icon, Menu, Stack, Text, usePanelContext, useStyles2 } from '@grafana/ui'; +import { NEW_PANEL_TITLE } from '../../dashboard/utils/dashboard'; import { DashboardInteractions } from '../utils/interactions'; import { findVizPanelByKey, getVizPanelKeyForPanelId } from '../utils/utils'; @@ -16,6 +18,8 @@ const UnconfiguredPanel = new PanelPlugin(UnconfiguredPanelComp); function UnconfiguredPanelComp(props: PanelProps) { const [isOpen, setIsOpen] = useState(false); + const panelContext = usePanelContext(); + const styles = useStyles2(getStyles); const onMenuClick = useCallback((isOpen: boolean) => { setIsOpen(isOpen); @@ -26,14 +30,15 @@ function UnconfiguredPanelComp(props: PanelProps) { DashboardInteractions.panelActionClicked('configure', props.id, 'panel'); }; - const onUseLibraryPanel = () => { - const dashboard = window.__grafanaSceneContext; + const dashboard = window.__grafanaSceneContext; + const panel = + dashboard instanceof DashboardScene ? findVizPanelByKey(dashboard, getVizPanelKeyForPanelId(props.id)) : null; - if (!(dashboard instanceof DashboardScene)) { + const onUseLibraryPanel = () => { + if (!dashboard || !(dashboard instanceof DashboardScene)) { throw new Error('DashboardScene not found'); } - const panel = findVizPanelByKey(dashboard, getVizPanelKeyForPanelId(props.id)); if (!panel) { throw new Error('Panel not found'); } @@ -41,6 +46,18 @@ function UnconfiguredPanelComp(props: PanelProps) { dashboard.onShowAddLibraryPanelDrawer(panel.getRef()); }; + useEffect(() => { + if (!panel || !config.featureToggles.newVizSuggestions) { + return; + } + + if (panelContext.app === CoreApp.PanelEditor) { + panel.setState({ title: '' }); + } else if (!panel.state.title) { + panel.setState({ title: NEW_PANEL_TITLE }); + } + }, [panel, panelContext.app]); + const MenuActions = () => ( ); + const showEmptyState = config.featureToggles.newVizSuggestions && panelContext.app === CoreApp.PanelEditor; + + if (showEmptyState) { + const defaultContent = ( + + Run a query to visualize it here or go to all visualizations to add other panel types + + ); + + return ( +
+ + + {defaultContent} + +
+ ); + } + return ( @@ -79,3 +115,20 @@ sceneUtils.registerRuntimePanelPlugin({ pluginId: UNCONFIGURED_PANEL_PLUGIN_ID, plugin: UnconfiguredPanel, }); + +function getStyles(theme: GrafanaTheme2) { + return { + emptyStateWrapper: css({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + textAlign: 'center', + }), + emptyStateIcon: css({ + color: theme.colors.text.secondary, + marginBottom: theme.spacing(2), + }), + }; +} diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 7691b52f3b8..7188756b055 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -96,6 +96,7 @@ function findVizPanelInternal(scene: SceneObject, key: string | undefined): VizP return null; } + export function findEditPanel(scene: SceneObject, key: string | undefined): VizPanel | null { if (!key) { return null; @@ -258,8 +259,15 @@ export function getClosestVizPanel(sceneObject: SceneObject): VizPanel | null { } export function getDefaultVizPanel(): VizPanel { - const defaultPluginId = config.featureToggles.dashboardNewLayouts ? UNCONFIGURED_PANEL_PLUGIN_ID : 'timeseries'; - const newPanelTitle = t('dashboard.new-panel-title', 'New panel'); + const defaultPluginId = + config.featureToggles.dashboardNewLayouts || config.featureToggles.newVizSuggestions + ? UNCONFIGURED_PANEL_PLUGIN_ID + : 'timeseries'; + + const newPanelTitle = + config.featureToggles.newVizSuggestions && defaultPluginId === UNCONFIGURED_PANEL_PLUGIN_ID + ? '' + : t('dashboard.new-panel-title', 'New panel'); return new VizPanel({ title: newPanelTitle, diff --git a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx index 46e5bdbe012..46232beff41 100644 --- a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx +++ b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx @@ -5,7 +5,8 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2, PanelData, PanelModel, PanelPluginVisualizationSuggestion } from '@grafana/data'; import { Trans } from '@grafana/i18n'; -import { useStyles2 } from '@grafana/ui'; +import { config } from '@grafana/runtime'; +import { Icon, Text, useStyles2 } from '@grafana/ui'; import { getAllSuggestions } from '../../suggestions/getAllSuggestions'; @@ -31,6 +32,21 @@ export function VisualizationSuggestions({ searchQuery, onChange, data, panel, t return result; }, [searchQuery, suggestions, trackSearch]); + const hasData = data?.series && data.series.length > 0 && !data.series.every((frame) => frame.length === 0); + + if (config.featureToggles.newVizSuggestions && !hasData && !searchQuery) { + return ( +
+ + + + Run a query to start seeing suggested visualizations + + +
+ ); + } + return ( // This div is needed in some places to make AutoSizer work
@@ -116,5 +132,18 @@ const getStyles = (theme: GrafanaTheme2) => { marginBottom: theme.spacing(1), justifyContent: 'space-evenly', }), + emptyStateWrapper: css({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: theme.spacing(4), + textAlign: 'center', + minHeight: '200px', + }), + emptyStateIcon: css({ + color: theme.colors.text.secondary, + marginBottom: theme.spacing(2), + }), }; }; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 0c9b8b63682..874be92a233 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5100,8 +5100,12 @@ "new-panel": { "configure-button": "Configure", "configure-button-menu": "Toggle menu", + "empty-state-message": "Run a query to visualize it here or go to all visualizations to add other panel types", "menu-open-panel-editor": "Configure", - "menu-use-library-panel": "Use library panel" + "menu-use-library-panel": "Use library panel", + "suggestions": { + "empty-state-message": "Run a query to start seeing suggested visualizations" + } }, "new-panel-title": "New panel", "on-create-new-row": { From 8108d3c7952eeaebbfe01f046d2a6fe668d621be Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Fri, 21 Nov 2025 15:00:35 -0500 Subject: [PATCH 065/378] Alerting: Add tracing to prometheus rules api (#114282) To better observe and identify performance bottlenecks in the prometheus rules API. The following spans were added: - `api.prometheus.RouteGetRuleStatuses` - `api.prometheus.PrepareRuleGroupStatusesV2` The `api.prometheus.PrepareRuleGroupStatusesV2` includes attributes to capture the parameters used in the request to help with debugging and performance analysis. --- .../ngalert/api/prometheus/api_prometheus.go | 94 +++++++++++++++++-- 1 file changed, 86 insertions(+), 8 deletions(-) diff --git a/pkg/services/ngalert/api/prometheus/api_prometheus.go b/pkg/services/ngalert/api/prometheus/api_prometheus.go index c835fb2acdd..00002c61593 100644 --- a/pkg/services/ngalert/api/prometheus/api_prometheus.go +++ b/pkg/services/ngalert/api/prometheus/api_prometheus.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "net/url" "slices" "sort" @@ -26,6 +27,9 @@ import ( ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/util" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" ) type RuleStoreReader interface { @@ -54,6 +58,9 @@ type PrometheusSrv struct { provenanceStore ProvenanceStore } +// Package-level OpenTelemetry tracer per Grafana instrumentation conventions. +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/ngalert/api/prometheus") + func NewPrometheusSrv(log log.Logger, manager state.AlertInstanceManager, status StatusReader, store RuleStoreReader, authz RuleGroupAccessControlService, provenanceStore ProvenanceStore) *PrometheusSrv { return &PrometheusSrv{ log, @@ -219,6 +226,14 @@ func GetStatesFromQuery(v url.Values) (map[eval.State]struct{}, error) { return states, nil } +func MapStateSetToStrings(stateSet map[eval.State]struct{}) []string { + states := make([]string, 0, len(stateSet)) + for state := range stateSet { + states = append(states, state.String()) + } + return states +} + func GetHealthFromQuery(v url.Values) (map[string]struct{}, error) { health := make(map[string]struct{}) for _, s := range v["health"] { @@ -252,6 +267,13 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon // As we are using req.Form directly, this triggers a call to ParseForm() if needed. c.Query("") + ctx, span := tracer.Start(c.Req.Context(), "api.prometheus.RouteGetRuleStatuses") + defer span.End() + // Propagate the new context so child spans can attach to it. + c.Req = c.Req.WithContext(ctx) + orgID := c.GetOrgID() + span.SetAttributes(attribute.Int64("org_id", orgID)) + ruleResponse := apimodels.RuleResponse{ DiscoveryBase: apimodels.DiscoveryBase{ Status: "success", @@ -261,13 +283,14 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon }, } - namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.GetOrgID(), c.SignedInUser) + namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), orgID, c.SignedInUser) if err != nil { ruleResponse.Status = "error" ruleResponse.Error = fmt.Sprintf("failed to get namespaces visible to the user: %s", err.Error()) ruleResponse.ErrorType = apiv1.ErrServer return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) } + span.AddEvent("User visible namespaces retrieved") allowedNamespaces := map[string]string{} for namespaceUID, folder := range namespaceMap { @@ -283,6 +306,8 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon allowedNamespaces[namespaceUID] = folder.Fullpath } } + span.AddEvent("User permissions checked") + span.SetAttributes(attribute.Int("allowedNamespaces", len(allowedNamespaces))) provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.GetOrgID(), (&ngmodels.AlertRule{}).ResourceType()) if err != nil { @@ -297,7 +322,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon srv.store, RuleGroupStatusesOptions{ Ctx: c.Req.Context(), - OrgID: c.OrgID, + OrgID: orgID, Query: c.Req.Form, AllowedNamespaces: allowedNamespaces, }, @@ -405,6 +430,10 @@ func RuleAlertStateMutatorGenerator(manager state.AlertInstanceManager) RuleAler } func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opts RuleGroupStatusesOptions, ruleStatusMutator RuleStatusMutator, alertStateMutator RuleAlertStateMutator, provenanceRecords map[string]ngmodels.Provenance) apimodels.RuleResponse { + ctx, span := tracer.Start(opts.Ctx, "api.prometheus.PrepareRuleGroupStatusesV2") + defer span.End() + opts.Ctx = ctx + ruleResponse := apimodels.RuleResponse{ DiscoveryBase: apimodels.DiscoveryBase{ Status: "success", @@ -428,9 +457,17 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt ruleResponse.ErrorType = apiv1.ErrBadData return ruleResponse } + span.SetAttributes( + attribute.String("dashboard_uid", dashboardUID), + attribute.Int64("panel_id", panelID), + ) limitRulesPerGroup := getInt64WithDefault(opts.Query, "limit_rules", -1) limitAlertsPerRule := getInt64WithDefault(opts.Query, "limit_alerts", -1) + span.SetAttributes( + attribute.Int64("limit_rules", limitRulesPerGroup), + attribute.Int64("limit_alerts", limitAlertsPerRule), + ) matchers, err := getMatchersFromQuery(opts.Query) if err != nil { ruleResponse.Status = "error" @@ -438,6 +475,8 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt ruleResponse.ErrorType = apiv1.ErrBadData return ruleResponse } + span.SetAttributes(attribute.Int("matcher_count", len(matchers))) + stateFilterSet, err := GetStatesFromQuery(opts.Query) if err != nil { ruleResponse.Status = "error" @@ -445,6 +484,10 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt ruleResponse.ErrorType = apiv1.ErrBadData return ruleResponse } + span.SetAttributes( + attribute.Int("state_filter_count", len(stateFilterSet)), + attribute.StringSlice("state_filter", MapStateSetToStrings(stateFilterSet)), + ) healthFilterSet, err := GetHealthFromQuery(opts.Query) if err != nil { @@ -453,11 +496,18 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt ruleResponse.ErrorType = apiv1.ErrBadData return ruleResponse } + span.SetAttributes( + attribute.Int("health_filter_count", len(healthFilterSet)), + attribute.StringSlice("health_filter", slices.Collect(maps.Keys(healthFilterSet))), + ) var labelOptions []ngmodels.LabelOption if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) { labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels()) } + span.SetAttributes( + attribute.Bool("include_internal_labels", len(labelOptions) == 0), + ) if len(opts.AllowedNamespaces) == 0 { log.Debug("User does not have access to any namespaces") @@ -476,19 +526,36 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt } } + span.SetAttributes( + attribute.Bool("folder_uid_set", folderUID != ""), + attribute.Int("namespace_count", len(namespaceUIDs)), + ) + ruleGroups := opts.Query["rule_group"] ruleUIDs := opts.Query["rule_uid"] + span.SetAttributes( + attribute.Int("rule_group_count", len(ruleGroups)), + attribute.Int("rule_uid_count", len(ruleUIDs)), + ) + receiverName := opts.Query.Get("receiver_name") + span.SetAttributes(attribute.Bool("receiver_name_set", receiverName != "")) + title := opts.Query.Get("search.rule_name") + span.SetAttributes(attribute.Bool("search_rule_name_set", title != "")) + searchRuleGroup := opts.Query.Get("search.rule_group") + span.SetAttributes(attribute.Bool("search_rule_group_set", searchRuleGroup != "")) var ruleType ngmodels.RuleTypeFilter switch ngmodels.RuleType(opts.Query.Get("rule_type")) { case ngmodels.RuleTypeAlerting: ruleType = ngmodels.RuleTypeFilterAlerting + span.SetAttributes(attribute.Bool("alerting_only", true)) case ngmodels.RuleTypeRecording: ruleType = ngmodels.RuleTypeFilterRecording + span.SetAttributes(attribute.Bool("recording_only", true)) default: ruleType = ngmodels.RuleTypeFilterAll } @@ -507,11 +574,23 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt maxGroups := getInt64WithDefault(opts.Query, "group_limit", -1) maxRules := getInt64WithDefault(opts.Query, "rule_limit", -1) nextToken := opts.Query.Get("group_next_token") + span.SetAttributes( + attribute.Int64("group_limit", maxGroups), + attribute.Int64("rule_limit", maxRules), + attribute.Bool("group_next_token_set", nextToken != ""), + ) if maxGroups == 0 || maxRules == 0 { return ruleResponse } + ruleNames := opts.Query["rule_name"] + ruleNamesSet := make(map[string]struct{}, len(ruleNames)) + for _, rn := range ruleNames { + ruleNamesSet[rn] = struct{}{} + } + span.SetAttributes(attribute.Int("rule_name_count", len(ruleNamesSet))) + byGroupQuery := ngmodels.ListAlertRulesExtendedQuery{ ListAlertRulesQuery: ngmodels.ListAlertRulesQuery{ OrgID: opts.OrgID, @@ -536,12 +615,11 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt ruleResponse.ErrorType = apiv1.ErrServer return ruleResponse } - - ruleNames := opts.Query["rule_name"] - ruleNamesSet := make(map[string]struct{}, len(ruleNames)) - for _, rn := range ruleNames { - ruleNamesSet[rn] = struct{}{} - } + span.SetAttributes( + attribute.Int("store_rule_list_len", len(ruleList)), + attribute.Bool("store_continue_token_set", continueToken != ""), + ) + span.AddEvent("Alert rules retrieved from store") groupedRules := getGroupedRules(log, ruleList, ruleNamesSet, opts.AllowedNamespaces) rulesTotals := make(map[string]int64, len(groupedRules)) From 01eee72c80529032ed3c103f3406f2992416cbd0 Mon Sep 17 00:00:00 2001 From: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:22:38 -0600 Subject: [PATCH 066/378] DOCS: Fixed PDC link for CloudWatch data source (#114200) fixed broken link (refURI) for PDC --- docs/sources/datasources/aws-cloudwatch/configure/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/datasources/aws-cloudwatch/configure/index.md b/docs/sources/datasources/aws-cloudwatch/configure/index.md index 9663010505c..7e80433756b 100644 --- a/docs/sources/datasources/aws-cloudwatch/configure/index.md +++ b/docs/sources/datasources/aws-cloudwatch/configure/index.md @@ -62,9 +62,9 @@ refs: destination: /docs/grafana//datasources/aws-CloudWatch/aws-authentication/ private-data-source-connect: - pattern: /docs/grafana/ - destination: docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/ + destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/ - pattern: /docs/grafana-cloud/ - destination: docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/ + destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/ configure-pdc: - pattern: /docs/grafana/ destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/#configure-grafana-private-data-source-connect-pdc From 2ee956192064343c73009ffe106178bf4e983844 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:35:25 -0600 Subject: [PATCH 067/378] Chore: Fix panels smoke test (#113187) * update test * update --- .../smoke-tests-suite/panels.spec.ts | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/e2e-playwright/smoke-tests-suite/panels.spec.ts b/e2e-playwright/smoke-tests-suite/panels.spec.ts index 08a4189f735..ded102294ce 100644 --- a/e2e-playwright/smoke-tests-suite/panels.spec.ts +++ b/e2e-playwright/smoke-tests-suite/panels.spec.ts @@ -7,13 +7,13 @@ test.describe( tag: ['@acceptance'], }, () => { - test.skip('Tests each panel type in the panel edit view to ensure no crash', async ({ + test('Tests each panel type in the panel edit view to ensure no crash', async ({ gotoDashboardPage, selectors, page, }) => { // this test can absolutely take longer than the default 30s timeout - test.setTimeout(60000); + test.setTimeout(120000); // Create new dashboard const dashboardPage = await gotoDashboardPage({}); @@ -29,22 +29,32 @@ test.describe( return win.grafanaBootData?.settings?.panels ?? {}; }); + const vizPicker = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker); + // Loop through every panel type and ensure no crash for (const [_, panel] of Object.entries(panelTypes)) { if (panel.hideFromList || panel.state === 'deprecated') { continue; // Skip hidden and deprecated panels } - // Select the panel type in the viz picker - const vizPicker = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker); - await vizPicker.click(); - await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(panel.name)).click(); + try { + // Select the panel type in the viz picker + await expect(vizPicker).toBeVisible(); + await vizPicker.click({ force: true }); - // Verify panel type is selected - await expect(vizPicker).toHaveText(panel.name); + await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(panel.name)).click(); - // Ensure no unexpected error occurred - await expect(page.getByText('An unexpected error happened')).toBeHidden(); + // Verify panel type is selected + await expect(vizPicker).toHaveText(panel.name, { timeout: 10000 }); + + // Wait for panel to finish rendering + await expect(page.getByLabel('Panel loading bar')).toHaveCount(0, { timeout: 10000 }); + + // Ensure no unexpected error occurred + await expect(page.getByText('An unexpected error happened')).toBeHidden(); + } catch (error) { + throw new Error(`Panel '${panel.name}' failed: ${error}`); + } } }); } From 4b0d6a2e1af3213ff20676a008c05f8d5fcae6c9 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 00:32:34 +0000 Subject: [PATCH 068/378] I18n: Download translations from Crowdin (#114332) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 22 ++++++++++++++-------- public/locales/de-DE/grafana.json | 22 ++++++++++++++-------- public/locales/es-ES/grafana.json | 22 ++++++++++++++-------- public/locales/fr-FR/grafana.json | 22 ++++++++++++++-------- public/locales/hu-HU/grafana.json | 22 ++++++++++++++-------- public/locales/id-ID/grafana.json | 22 ++++++++++++++-------- public/locales/it-IT/grafana.json | 22 ++++++++++++++-------- public/locales/ja-JP/grafana.json | 22 ++++++++++++++-------- public/locales/ko-KR/grafana.json | 22 ++++++++++++++-------- public/locales/nl-NL/grafana.json | 22 ++++++++++++++-------- public/locales/pl-PL/grafana.json | 22 ++++++++++++++-------- public/locales/pt-BR/grafana.json | 22 ++++++++++++++-------- public/locales/pt-PT/grafana.json | 22 ++++++++++++++-------- public/locales/ru-RU/grafana.json | 22 ++++++++++++++-------- public/locales/sv-SE/grafana.json | 22 ++++++++++++++-------- public/locales/tr-TR/grafana.json | 22 ++++++++++++++-------- public/locales/zh-Hans/grafana.json | 22 ++++++++++++++-------- public/locales/zh-Hant/grafana.json | 22 ++++++++++++++-------- 18 files changed, 252 insertions(+), 144 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 01d21c3f657..1e2530c7e6f 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -1994,9 +1994,11 @@ }, "notification-preview": { "alertmanager": "Správce výstrah:", - "disabled-tooltip": "", "error": "Nelze načíst náhled směrování pro {{alertmanager}}", "initialized": "Na základě přidaného štítku jsou instance výstrahy přesměrovány do následujících zásad oznamování. Další podrobnosti zobrazíte rozbalením jednotlivých zásad oznamování níže.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Zobrazit náhled směrování", "select-folder-tooltip": "", "text-loading-preview": "Načítání náhledu…", @@ -5138,8 +5140,12 @@ "new-panel": { "configure-button": "Konfigurovat", "configure-button-menu": "Přepnout nabídku", + "empty-state-message": "", "menu-open-panel-editor": "Konfigurovat", - "menu-use-library-panel": "Použít panel knihovny" + "menu-use-library-panel": "Použít panel knihovny", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Nový panel", "on-create-new-row": { @@ -7266,6 +7272,7 @@ "logs-volume": { "add-filters": "Chcete-li zúžit vyhledávání, přidejte do dotazu další štítky.", "decrease-timerange": "Zkraťte časový rozsah dotazu.", + "max-bytes-error": "", "much-data": "Dotaz se pokouší získat přístup k příliš velkému množství dat. Zkuste jeden nebo více následujících kroků:" }, "maximum-pinned-logs": "Bylo dosaženo maximálního počtu připnutých protokolů: {{PINNED_LOGS_LIMIT}}. Odepněte protokol pro přidání dalšího protokolu.", @@ -7315,12 +7322,15 @@ "tooltip-select-query-visualize-table": "Vyberte dotaz, který chcete vizualizovat v tabulce" }, "logs-volume-panel": { - "content-streaming": "Streamování" + "content-streaming": "Streamování", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Znovu načíst objem protokolu", "label-reload-log-volume": "Znovu načíst objem protokolu", "loading": "Načítání…", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Objem protokolu pro tento dotaz se nepodařilo načíst", "title-no-logs-volume-available": "Není k dispozici žádný objem protokolů", "title-showing-partial-data": "Zobrazují se částečná data", @@ -10061,11 +10071,7 @@ "wrap-lines": "Zalomit řádky" }, "logs-navigation": { - "newer-logs": "Novější protokoly", - "older-logs": "Starší protokoly", - "scroll-bottom": "Posunout dolů", - "scroll-top": "Posunout nahoru", - "start-of-range": "Začátek rozsahu" + "scroll-top": "Posunout nahoru" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 9a3b21b70aa..546fb6800d6 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -1982,9 +1982,11 @@ }, "notification-preview": { "alertmanager": "Alertmanager:", - "disabled-tooltip": "", "error": "Die Weiterleitungsvorschau für {{alertmanager}} konnte nicht geladen werden", "initialized": "Basierend auf den hinzugefügten Labels werden Warninstanzen an die folgenden Benachrichtigungsrichtlinien weitergeleitet. Erweitern Sie unten die einzelnen Benachrichtigungsrichtlinien, um weitere Details anzuzeigen.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Weiterleitungsvorschau", "select-folder-tooltip": "", "text-loading-preview": "Vorschau wird geladen ...", @@ -5098,8 +5100,12 @@ "new-panel": { "configure-button": "Konfigurieren", "configure-button-menu": "Menü umschalten", + "empty-state-message": "", "menu-open-panel-editor": "Konfigurieren", - "menu-use-library-panel": "Bibliotheks-Panel verwenden" + "menu-use-library-panel": "Bibliotheks-Panel verwenden", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Neues Panel", "on-create-new-row": { @@ -7218,6 +7224,7 @@ "logs-volume": { "add-filters": "Fügen Sie Ihrer Abfrage weitere Labels hinzu, um Ihre Suche einzugrenzen.", "decrease-timerange": "Verringern Sie den Zeitbereich Ihrer Abfrage.", + "max-bytes-error": "", "much-data": "Die Abfrage versucht, auf zu viele Daten zuzugreifen. Probieren Sie eine oder mehrere der folgenden Möglichkeiten aus:" }, "maximum-pinned-logs": "Maximale Anzahl von {{PINNED_LOGS_LIMIT}} fixierten Protokollen erreicht. Heben Sie die Fixierung eines Protokolls auf, um ein weiteres hinzuzufügen.", @@ -7267,12 +7274,15 @@ "tooltip-select-query-visualize-table": "Wählen Sie eine Abfrage für die Visualisierung in der Tabelle" }, "logs-volume-panel": { - "content-streaming": "Streaming" + "content-streaming": "Streaming", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Log-Volumen neu laden", "label-reload-log-volume": "Log-Volumen neu laden", "loading": "Wird geladen ...", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Das Log-Volumen für diese Abfrage konnte nicht geladen werden", "title-no-logs-volume-available": "Kein Logs-Volumen verfügbar", "title-showing-partial-data": "Teildaten werden angezeigt", @@ -9973,11 +9983,7 @@ "wrap-lines": "Zeilen umbrechen" }, "logs-navigation": { - "newer-logs": "Neuere Protokolle", - "older-logs": "Ältere Protokolle", - "scroll-bottom": "Nach unten scrollen", - "scroll-top": "Nach oben scrollen", - "start-of-range": "Anfang des Bereichs" + "scroll-top": "Nach oben scrollen" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index b46f38a8442..35b1730e477 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -1982,9 +1982,11 @@ }, "notification-preview": { "alertmanager": "Alertmanager:", - "disabled-tooltip": "", "error": "No se ha podido cargar la vista previa del enrutamiento para {{alertmanager}}", "initialized": "Según las etiquetas añadidas, las instancias de alerta se enrutan a las siguientes políticas de notificación. Amplía cada política de notificación a continuación para ver más detalles.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Vista previa del enrutamiento", "select-folder-tooltip": "", "text-loading-preview": "Cargando vista previa...", @@ -5098,8 +5100,12 @@ "new-panel": { "configure-button": "Configurar", "configure-button-menu": "Activar o desactivar menú", + "empty-state-message": "", "menu-open-panel-editor": "Configurar", - "menu-use-library-panel": "Usar panel de la librería" + "menu-use-library-panel": "Usar panel de la librería", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Nuevo panel", "on-create-new-row": { @@ -7218,6 +7224,7 @@ "logs-volume": { "add-filters": "Añade más etiquetas a tu consulta para acotar la búsqueda.", "decrease-timerange": "Disminuye el intervalo de tiempo de tu consulta.", + "max-bytes-error": "", "much-data": "La consulta está intentando acceder a demasiados datos. Prueba alguna de las siguientes opciones:" }, "maximum-pinned-logs": "Se ha alcanzado el máximo de {{PINNED_LOGS_LIMIT}} registros anclados. Desancla un registro para añadir otro.", @@ -7267,12 +7274,15 @@ "tooltip-select-query-visualize-table": "Selecciona una consulta para visualizar en la tabla" }, "logs-volume-panel": { - "content-streaming": "Transmisión" + "content-streaming": "Transmisión", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Volver a cargar el volumen de logs", "label-reload-log-volume": "Volver a cargar el volumen de logs", "loading": "Cargando...", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Error al cargar el volumen de logs para esta consulta", "title-no-logs-volume-available": "No hay volumen de logs disponible", "title-showing-partial-data": "Mostrando datos parciales", @@ -9973,11 +9983,7 @@ "wrap-lines": "Ajustar líneas" }, "logs-navigation": { - "newer-logs": "Registros más recientes", - "older-logs": "Registros más antiguos", - "scroll-bottom": "Desplazarse al final", - "scroll-top": "Desplazarse al inicio", - "start-of-range": "Inicio del intervalo" + "scroll-top": "Desplazarse al inicio" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 428c251e152..74e96034e55 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -1982,9 +1982,11 @@ }, "notification-preview": { "alertmanager": "Alertmanager :", - "disabled-tooltip": "", "error": "Impossible de charger l'aperçu de l'acheminement pour {{alertmanager}}", "initialized": "En fonction des étiquettes ajoutées, les instances d'alerte sont acheminées vers les politiques de notification suivantes. Développez chaque politique de notification ci-dessous pour afficher plus de détails.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Aperçu de l'acheminement", "select-folder-tooltip": "", "text-loading-preview": "Chargement de l’aperçu...", @@ -5098,8 +5100,12 @@ "new-panel": { "configure-button": "Configurer", "configure-button-menu": "Afficher/Masquer le menu", + "empty-state-message": "", "menu-open-panel-editor": "Configurer", - "menu-use-library-panel": "Utiliser le panneau de bibliothèque" + "menu-use-library-panel": "Utiliser le panneau de bibliothèque", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Nouveau panneau", "on-create-new-row": { @@ -7218,6 +7224,7 @@ "logs-volume": { "add-filters": "Ajoutez d'autres étiquettes à votre requête pour affiner votre recherche.", "decrease-timerange": "Réduisez la plage de temps de votre requête.", + "max-bytes-error": "", "much-data": "La requête tente d'accéder à trop de données. Essayez une ou plusieurs des opérations suivantes :" }, "maximum-pinned-logs": "Nombre maximum de {{PINNED_LOGS_LIMIT}} journaux épinglés atteint. Désépinglez un journal pour en ajouter un autre.", @@ -7267,12 +7274,15 @@ "tooltip-select-query-visualize-table": "Sélectionner une requête à visualiser dans le tableau" }, "logs-volume-panel": { - "content-streaming": "Diffusion" + "content-streaming": "Diffusion", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Recharger le volume de journaux", "label-reload-log-volume": "Recharger le volume de journal", "loading": "Chargement en cours...", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Échec du chargement du volume de journal pour cette requête", "title-no-logs-volume-available": "Aucun volume de journaux disponible", "title-showing-partial-data": "Affichage de données partielles", @@ -9973,11 +9983,7 @@ "wrap-lines": "Enrouler les lignes" }, "logs-navigation": { - "newer-logs": "Journaux plus récents", - "older-logs": "Journaux plus anciens", - "scroll-bottom": "Faire défiler vers le bas", - "scroll-top": "Retour en haut de page", - "start-of-range": "Jour de début de la plage" + "scroll-top": "Retour en haut de page" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index ac46a842a13..02d75808711 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -1982,9 +1982,11 @@ }, "notification-preview": { "alertmanager": "Riasztáskezelő:", - "disabled-tooltip": "", "error": "Nem sikerült betölteni az irányítási előnézetet ehhez: {{alertmanager}}", "initialized": "A hozzáadott címkék alapján a riasztáspéldányok a következő értesítési házirendekhez vannak irányítva. További részletek megtekintéséhez bontsa ki az alábbi értesítési házirendeket.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Irányítás előnézete", "select-folder-tooltip": "", "text-loading-preview": "Előnézet betöltése…", @@ -5098,8 +5100,12 @@ "new-panel": { "configure-button": "Konfigurálás", "configure-button-menu": "Menü átkapcsolása", + "empty-state-message": "", "menu-open-panel-editor": "Konfigurálás", - "menu-use-library-panel": "Könyvtárpanel használata" + "menu-use-library-panel": "Könyvtárpanel használata", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Új panel", "on-create-new-row": { @@ -7218,6 +7224,7 @@ "logs-volume": { "add-filters": "Adjon hozzá több címkét a lekérdezéshez a keresés szűkítése érdekében.", "decrease-timerange": "Csökkentse a lekérdezés időtartományát.", + "max-bytes-error": "", "much-data": "A lekérdezés túl sok adathoz próbál hozzáférni. Próbálkozzon a következőkkel:" }, "maximum-pinned-logs": "Elérte a rögzített naplók maximális számát ({{PINNED_LOGS_LIMIT}}). Egy másik hozzáadásához oldja fel egy napló rögzítését.", @@ -7267,12 +7274,15 @@ "tooltip-select-query-visualize-table": "Jelölje ki a táblázatban megjelenítendő lekérdezést" }, "logs-volume-panel": { - "content-streaming": "Adatfolyam" + "content-streaming": "Adatfolyam", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Naplómennyiség újratöltése", "label-reload-log-volume": "Naplómennyiség újratöltése", "loading": "Betöltés…", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "A lekérdezés naplómennyiségének betöltése nem sikerült", "title-no-logs-volume-available": "Nincs rendelkezésre álló naplómennyiség", "title-showing-partial-data": "Részadatok megjelenítése", @@ -9973,11 +9983,7 @@ "wrap-lines": "Sortörés" }, "logs-navigation": { - "newer-logs": "Újabb naplók", - "older-logs": "Régebbi naplók", - "scroll-bottom": "Görgetés az aljára", - "scroll-top": "Görgetés a tetejére", - "start-of-range": "Tartomány kezdete" + "scroll-top": "Görgetés a tetejére" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index b00d4be59e5..d9b58b75791 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -1976,9 +1976,11 @@ }, "notification-preview": { "alertmanager": "Alertmanager:", - "disabled-tooltip": "", "error": "Tidak dapat memuat pratinjau perutean untuk {{alertmanager}}", "initialized": "Berdasarkan label yang ditambahkan, instans peringatan dirutekan ke kebijakan pemberitahuan berikut. Perluas setiap kebijakan pemberitahuan di bawah ini untuk melihat detail selengkapnya.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Pratinjau perutean", "select-folder-tooltip": "", "text-loading-preview": "Memuat pratinjau...", @@ -5078,8 +5080,12 @@ "new-panel": { "configure-button": "Konfigurasikan", "configure-button-menu": "Alihkan tombol menu", + "empty-state-message": "", "menu-open-panel-editor": "Konfigurasikan", - "menu-use-library-panel": "Gunakan panel pustaka" + "menu-use-library-panel": "Gunakan panel pustaka", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Panel baru", "on-create-new-row": { @@ -7194,6 +7200,7 @@ "logs-volume": { "add-filters": "Tambahkan lebih banyak label ke kueri Anda untuk mempersempit pencarian Anda.", "decrease-timerange": "Kurangi rentang waktu kueri Anda.", + "max-bytes-error": "", "much-data": "Kueri mencoba mengakses terlalu banyak data. Coba salah satu atau beberapa hal berikut:" }, "maximum-pinned-logs": "Maksimum {{PINNED_LOGS_LIMIT}} log yang disematkan tercapai. Buka sematan log untuk menambahkan log lain.", @@ -7243,12 +7250,15 @@ "tooltip-select-query-visualize-table": "Pilih kueri untuk divisualisasikan pada tabel" }, "logs-volume-panel": { - "content-streaming": "Streaming" + "content-streaming": "Streaming", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Muat ulang volume log", "label-reload-log-volume": "Muat ulang volume log", "loading": "Memuat...", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Gagal memuat volume log untuk kueri ini", "title-no-logs-volume-available": "Tidak ada volume log yang tersedia", "title-showing-partial-data": "Menampilkan data parsial", @@ -9929,11 +9939,7 @@ "wrap-lines": "Terapkan wrap pada baris" }, "logs-navigation": { - "newer-logs": "Log baru", - "older-logs": "Log lama", - "scroll-bottom": "Gulir ke bawah", - "scroll-top": "Gulir ke atas", - "start-of-range": "Awal rentang" + "scroll-top": "Gulir ke atas" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index ab7d67dc580..7ada28c6693 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -1982,9 +1982,11 @@ }, "notification-preview": { "alertmanager": "Alertmanager:", - "disabled-tooltip": "", "error": "Impossibile caricare l'anteprima del routing per {{alertmanager}}", "initialized": "In base alle etichette aggiunte, le istanze di avviso vengono instradate alle seguenti politiche di notifica. Espandi ciascuna politica di notifica qui sotto per visualizzare ulteriori dettagli.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Anteprima routing", "select-folder-tooltip": "", "text-loading-preview": "Caricamento anteprima in corso...", @@ -5098,8 +5100,12 @@ "new-panel": { "configure-button": "Configura", "configure-button-menu": "Attiva/disattiva menu", + "empty-state-message": "", "menu-open-panel-editor": "Configura", - "menu-use-library-panel": "Usa il pannello della libreria" + "menu-use-library-panel": "Usa il pannello della libreria", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Nuovo pannello", "on-create-new-row": { @@ -7218,6 +7224,7 @@ "logs-volume": { "add-filters": "Aggiungi altre etichette alla query per restringere la ricerca.", "decrease-timerange": "Riduci l'intervallo di tempo della query.", + "max-bytes-error": "", "much-data": "La query sta tentando di accedere a troppi dati. Esegui una o più delle operazioni seguenti:" }, "maximum-pinned-logs": "Numero massimo di {{PINNED_LOGS_LIMIT}} registri bloccati raggiunto. Sblocca un registro per aggiungerne un altro.", @@ -7267,12 +7274,15 @@ "tooltip-select-query-visualize-table": "Seleziona una query da visualizzare nella tabella" }, "logs-volume-panel": { - "content-streaming": "Streaming" + "content-streaming": "Streaming", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Ricarica il volume del registro", "label-reload-log-volume": "Ricarica il volume del registro", "loading": "Caricamento in corso...", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Impossibile caricare il volume dei registri per questa query", "title-no-logs-volume-available": "Nessun volume di registri disponibile", "title-showing-partial-data": "Visualizzazione di dati parziali", @@ -9973,11 +9983,7 @@ "wrap-lines": "A capo" }, "logs-navigation": { - "newer-logs": "Registri più recenti", - "older-logs": "Registri meno recenti", - "scroll-bottom": "Scorri verso il basso", - "scroll-top": "Scorri verso l'alto", - "start-of-range": "Inizio dell'intervallo" + "scroll-top": "Scorri verso l'alto" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 7a09ed72d4c..5043d63283e 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -1976,9 +1976,11 @@ }, "notification-preview": { "alertmanager": "Alertmanager:", - "disabled-tooltip": "", "error": "{{alertmanager}}のルーティングプレビューを読み込めませんでした", "initialized": "追加されたラベルに基づいて、アラートインスタンスは次の通知ポリシーにルーティングされます。詳細を表示するには、以下の各通知ポリシーを展開します。", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "ルーティングのプレビュー", "select-folder-tooltip": "", "text-loading-preview": "プレビュー読み込み中...", @@ -5078,8 +5080,12 @@ "new-panel": { "configure-button": "構成", "configure-button-menu": "メニューの切り替え", + "empty-state-message": "", "menu-open-panel-editor": "構成", - "menu-use-library-panel": "ライブラリパネルを使用" + "menu-use-library-panel": "ライブラリパネルを使用", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "新規パネル", "on-create-new-row": { @@ -7194,6 +7200,7 @@ "logs-volume": { "add-filters": "クエリにラベルを追加して、検索範囲を絞り込んでください。", "decrease-timerange": "クエリの時間範囲を短縮してください。", + "max-bytes-error": "", "much-data": "クエリがアクセスしようとしているデータが多すぎます。次の1つ以上:" }, "maximum-pinned-logs": "ピン固定されたログが最大数、{{PINNED_LOGS_LIMIT}}に達しました。別のログを追加するには、ログのピン留めを解除してください。", @@ -7243,12 +7250,15 @@ "tooltip-select-query-visualize-table": "テーブルで表示するクエリを選択" }, "logs-volume-panel": { - "content-streaming": "ストリーミング" + "content-streaming": "ストリーミング", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "ログ量を再読み込み", "label-reload-log-volume": "ログ量を再読み込み", "loading": "読み込み中...", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "このクエリのログ量の読み込みに失敗しました", "title-no-logs-volume-available": "利用可能なログ量がありません", "title-showing-partial-data": "部分的なデータの表示", @@ -9929,11 +9939,7 @@ "wrap-lines": "行を折り返す" }, "logs-navigation": { - "newer-logs": "新しいログ", - "older-logs": "古いログ", - "scroll-bottom": "一番下までスクロール", - "scroll-top": "一番上までスクロール", - "start-of-range": "範囲の開始日" + "scroll-top": "一番上までスクロール" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 56dd9df87fb..0b205d6c624 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -1976,9 +1976,11 @@ }, "notification-preview": { "alertmanager": "Alertmanager:", - "disabled-tooltip": "", "error": "{{alertmanager}}의 라우팅 미리보기를 로딩하지 못했습니다.", "initialized": "추가된 라벨을 토대로 경고 인스턴스가 다음 알림 정책으로 라우팅됩니다. 자세한 내용을 보려면 아래의 각 알림 정책을 펼치세요.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "라우팅 미리보기", "select-folder-tooltip": "", "text-loading-preview": "미리 보기 로딩 중...", @@ -5078,8 +5080,12 @@ "new-panel": { "configure-button": "구성", "configure-button-menu": "토글 메뉴", + "empty-state-message": "", "menu-open-panel-editor": "구성", - "menu-use-library-panel": "라이브러리 패널 사용" + "menu-use-library-panel": "라이브러리 패널 사용", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "새 패널", "on-create-new-row": { @@ -7194,6 +7200,7 @@ "logs-volume": { "add-filters": "검색 결과를 좁히려면 쿼리에 라벨을 추가하세요.", "decrease-timerange": "쿼리의 시간 범위를 줄입니다.", + "max-bytes-error": "", "much-data": "쿼리가 너무 많은 데이터에 액세스하려고 합니다. 다음 중 하나 이상의 방법을 시도해 보세요." }, "maximum-pinned-logs": "고정할 수 있는 최대 로그 수({{PINNED_LOGS_LIMIT}})에 도달했습니다. 다른 로그를 추가하려면 기존 로그의 고정을 해제하세요.", @@ -7243,12 +7250,15 @@ "tooltip-select-query-visualize-table": "표에서 시각화할 쿼리 선택" }, "logs-volume-panel": { - "content-streaming": "스트리밍" + "content-streaming": "스트리밍", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "로그 볼륨 다시 로딩", "label-reload-log-volume": "로그 볼륨 다시 로딩", "loading": "로딩 중...", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "이 쿼리에 대한 로그 볼륨 로딩 실패", "title-no-logs-volume-available": "사용 가능한 로그 볼륨 없음", "title-showing-partial-data": "부분 데이터 표시 중", @@ -9929,11 +9939,7 @@ "wrap-lines": "줄 바꿈" }, "logs-navigation": { - "newer-logs": "최근 로그", - "older-logs": "이전 로그", - "scroll-bottom": "맨 아래로 스크롤", - "scroll-top": "맨 위로 스크롤", - "start-of-range": "범위의 시작" + "scroll-top": "맨 위로 스크롤" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 854505274f8..fae593379b6 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -1982,9 +1982,11 @@ }, "notification-preview": { "alertmanager": "Waarschuwingsmanager:", - "disabled-tooltip": "", "error": "Kon routeringsvoorbeeld niet laden voor {{alertmanager}}", "initialized": "Op basis van de toegevoegde labels worden waarschuwingsinstanties gerouteerd naar het volgende meldingsbeleid. Vouw elk meldingsbeleid hieronder uit voor meer informatie.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Voorbeeld routering", "select-folder-tooltip": "", "text-loading-preview": "Voorbeeldweergave laden...", @@ -5098,8 +5100,12 @@ "new-panel": { "configure-button": "Configureren", "configure-button-menu": "Navigatiemenu", + "empty-state-message": "", "menu-open-panel-editor": "Configureren", - "menu-use-library-panel": "Bibliotheekpaneel gebruiken" + "menu-use-library-panel": "Bibliotheekpaneel gebruiken", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Nieuw paneel", "on-create-new-row": { @@ -7218,6 +7224,7 @@ "logs-volume": { "add-filters": "Voeg meer labels toe aan je query om je zoekopdracht te verfijnen.", "decrease-timerange": "Verkort het tijdsbereik van je query.", + "max-bytes-error": "", "much-data": "De query probeert toegang te krijgen tot te veel gegevens. Probeer een of meer van de volgende oplossingen:" }, "maximum-pinned-logs": "Maximum aantal van {{PINNED_LOGS_LIMIT}} vastgezette logs bereikt. Maak een logboek los om een ander toe te voegen.", @@ -7267,12 +7274,15 @@ "tooltip-select-query-visualize-table": "Selecteer een query om te visualiseren in de tabel" }, "logs-volume-panel": { - "content-streaming": "Streamen" + "content-streaming": "Streamen", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Logvolume opnieuw laden", "label-reload-log-volume": "Logvolume opnieuw laden", "loading": "Laden...", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Kan het logvolume voor deze query niet laden", "title-no-logs-volume-available": "Geen logvolume beschikbaar", "title-showing-partial-data": "Gedeeltelijke gegevens weergeven", @@ -9973,11 +9983,7 @@ "wrap-lines": "Lijnen omsluiten" }, "logs-navigation": { - "newer-logs": "Nieuwere logboeken", - "older-logs": "Oudere logboeken", - "scroll-bottom": "Naar beneden scrollen", - "scroll-top": "Naar boven scrollen", - "start-of-range": "Begin van het bereik" + "scroll-top": "Naar boven scrollen" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index f3d06bb2edb..581e3de181b 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -1994,9 +1994,11 @@ }, "notification-preview": { "alertmanager": "Menedżer alertów:", - "disabled-tooltip": "", "error": "Nie udało się załadować podglądu przekierowania dla {{alertmanager}}", "initialized": "Instancje alertów są kierowane do następujących zasad powiadomień na podstawie dodanych etykiet. Rozwiń poniższe zasady powiadomień, aby wyświetlić więcej informacji.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Podgląd przekierowania", "select-folder-tooltip": "", "text-loading-preview": "Wczytywanie podglądu…", @@ -5138,8 +5140,12 @@ "new-panel": { "configure-button": "Konfiguruj", "configure-button-menu": "Przełącz menu", + "empty-state-message": "", "menu-open-panel-editor": "Konfiguruj", - "menu-use-library-panel": "Użyj panelu biblioteki" + "menu-use-library-panel": "Użyj panelu biblioteki", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Nowy panel", "on-create-new-row": { @@ -7266,6 +7272,7 @@ "logs-volume": { "add-filters": "Dodaj więcej etykiet do zapytania, aby zawęzić wyszukiwanie.", "decrease-timerange": "Zmniejsz zakres czasu zapytania.", + "max-bytes-error": "", "much-data": "Zapytanie próbuje uzyskać dostęp do zbyt dużej ilości danych. Spróbuj skorzystać z co najmniej jednego z podanych sposobów:" }, "maximum-pinned-logs": "Osiągnięto maksymalną liczbę przypiętych logów: {{PINNED_LOGS_LIMIT}}. Odepnij log, aby dodać kolejny.", @@ -7315,12 +7322,15 @@ "tooltip-select-query-visualize-table": "Wybierz zapytanie do wizualizacji w tabeli" }, "logs-volume-panel": { - "content-streaming": "Strumieniowanie" + "content-streaming": "Strumieniowanie", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Wczytaj ponownie wolumin logów", "label-reload-log-volume": "Wczytaj ponownie wolumin logów", "loading": "Ładowanie…", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Nie udało się załadować woluminu logów dla tego zapytania", "title-no-logs-volume-available": "Brak dostępnego woluminu logów", "title-showing-partial-data": "Wyświetlanie częściowych danych", @@ -10061,11 +10071,7 @@ "wrap-lines": "Zawijaj wiersze" }, "logs-navigation": { - "newer-logs": "Nowsze logi", - "older-logs": "Starsze logi", - "scroll-bottom": "Przewiń w dół", - "scroll-top": "Przewiń w górę", - "start-of-range": "Początek zakresu" + "scroll-top": "Przewiń w górę" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index a46882c87c8..9fb0c60ec94 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -1982,9 +1982,11 @@ }, "notification-preview": { "alertmanager": "Alertmanager:", - "disabled-tooltip": "", "error": "Não foi possível carregar a visualização de encaminhamento para {{alertmanager}}", "initialized": "Com base nas etiquetas adicionadas, as instâncias de alerta são encaminhadas para as seguintes políticas de notificação. Expanda cada política de notificação abaixo para ver mais detalhes.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Visualização do encaminhamento", "select-folder-tooltip": "", "text-loading-preview": "Carregando pré-visualização…", @@ -5098,8 +5100,12 @@ "new-panel": { "configure-button": "Configuração", "configure-button-menu": "Ativar ou desativar o menu", + "empty-state-message": "", "menu-open-panel-editor": "Configuração", - "menu-use-library-panel": "Usar painel da biblioteca" + "menu-use-library-panel": "Usar painel da biblioteca", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Novo painel", "on-create-new-row": { @@ -7218,6 +7224,7 @@ "logs-volume": { "add-filters": "Adicione mais etiquetas à sua consulta para tornar a pesquisa mais restritiva.", "decrease-timerange": "Diminua o intervalo de tempo da sua consulta.", + "max-bytes-error": "", "much-data": "A consulta está tentando acessar dados demais. Tente uma ou mais das seguintes soluções:" }, "maximum-pinned-logs": "Máximo de {{PINNED_LOGS_LIMIT}} logs fixados atingidos. Remova um dos logs fixados para adicionar outro.", @@ -7267,12 +7274,15 @@ "tooltip-select-query-visualize-table": "Selecione uma consulta para visualizar na tabela" }, "logs-volume-panel": { - "content-streaming": "Streaming" + "content-streaming": "Streaming", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Recarregar volume de log", "label-reload-log-volume": "Recarregar volume de log", "loading": "Carregando...", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Falha ao carregar o volume de log para esta consulta", "title-no-logs-volume-available": "Nenhum volume de logs disponível", "title-showing-partial-data": "Exibindo dados parciais", @@ -9973,11 +9983,7 @@ "wrap-lines": "Aplicar quebra de linha" }, "logs-navigation": { - "newer-logs": "Logs mais recentes", - "older-logs": "Logs mais antigos", - "scroll-bottom": "Rolar para baixo", - "scroll-top": "Rolar para cima", - "start-of-range": "Início do intervalo" + "scroll-top": "Rolar para cima" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 81009be83be..edb3fcc3a16 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -1982,9 +1982,11 @@ }, "notification-preview": { "alertmanager": "Alertmanager:", - "disabled-tooltip": "", "error": "Não foi possível carregar a pré-visualização de encaminhamento para {{alertmanager}}", "initialized": "Com base nas etiquetas adicionadas, as instâncias de alerta são encaminhadas para as seguintes políticas de notificação. Expanda cada política de notificação abaixo para ver mais detalhes.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Pré-visualizar encaminhamento", "select-folder-tooltip": "", "text-loading-preview": "A carregar a pré-visualização...", @@ -5098,8 +5100,12 @@ "new-panel": { "configure-button": "Configurar", "configure-button-menu": "Alternar menu", + "empty-state-message": "", "menu-open-panel-editor": "Configurar", - "menu-use-library-panel": "Utilizar o painel de biblioteca" + "menu-use-library-panel": "Utilizar o painel de biblioteca", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Novo painel", "on-create-new-row": { @@ -7218,6 +7224,7 @@ "logs-volume": { "add-filters": "Adicione mais etiquetas à sua consulta para restringir a sua pesquisa.", "decrease-timerange": "Diminua o intervalo de tempo da sua consulta.", + "max-bytes-error": "", "much-data": "A consulta está a tentar aceder a demasiados dados. Tente um ou mais dos seguintes procedimentos:" }, "maximum-pinned-logs": "Máximo de registos {{PINNED_LOGS_LIMIT}} fixados atingidos. Retire um registo para adicionar outro.", @@ -7267,12 +7274,15 @@ "tooltip-select-query-visualize-table": "Selecione uma consulta para visualizar na tabela" }, "logs-volume-panel": { - "content-streaming": "Transmissão" + "content-streaming": "Transmissão", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Recarregar volume de registo", "label-reload-log-volume": "Recarregar volume de registo", "loading": "A carregar...", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Falha ao carregar o volume de registos para esta consulta", "title-no-logs-volume-available": "Nenhum volume de registos disponível", "title-showing-partial-data": "A mostrar dados parciais", @@ -9973,11 +9983,7 @@ "wrap-lines": "Quebra de linhas" }, "logs-navigation": { - "newer-logs": "Registos mais recentes", - "older-logs": "Registos mais antigos", - "scroll-bottom": "Rolar para baixo", - "scroll-top": "Rolar para o topo", - "start-of-range": "Início do intervalo" + "scroll-top": "Rolar para o topo" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index a1c58056eea..a7a264ece15 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -1994,9 +1994,11 @@ }, "notification-preview": { "alertmanager": "Обработчик оповещений Alertmanager:", - "disabled-tooltip": "", "error": "Не удалось загрузить предварительный просмотр маршрутизации для {{alertmanager}}", "initialized": "На основе добавленных меток экземпляры оповещений направляются к следующим политикам уведомления. Для просмотра более подробной информации разверните каждую политику уведомления, приведенную ниже.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Предварительный просмотр маршрутизации", "select-folder-tooltip": "", "text-loading-preview": "Загрузка просмотра...", @@ -5138,8 +5140,12 @@ "new-panel": { "configure-button": "Настроить", "configure-button-menu": "Переключить меню", + "empty-state-message": "", "menu-open-panel-editor": "Настроить", - "menu-use-library-panel": "Использовать панель библиотеки" + "menu-use-library-panel": "Использовать панель библиотеки", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Новая панель", "on-create-new-row": { @@ -7266,6 +7272,7 @@ "logs-volume": { "add-filters": "Чтобы сузить область поиска, добавьте больше меток к запросу.", "decrease-timerange": "Уменьшите временной диапазон запроса.", + "max-bytes-error": "", "much-data": "Запрос пытается получить доступ к слишком большому количеству данных. Попробуйте воспользоваться одним из следующих способов." }, "maximum-pinned-logs": "Достигнуто максимальное количество закрепленных журналов ({{PINNED_LOGS_LIMIT}}). Открепите журнал, чтобы добавить другой.", @@ -7315,12 +7322,15 @@ "tooltip-select-query-visualize-table": "Выберите запрос для визуализации в таблице" }, "logs-volume-panel": { - "content-streaming": "Потоковая передача" + "content-streaming": "Потоковая передача", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Перезагрузить объем журналов", "label-reload-log-volume": "Перезагрузить объем журналов", "loading": "Загрузка…", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Не удалось загрузить объем журналов для этого запроса", "title-no-logs-volume-available": "Нет доступного объема журналов", "title-showing-partial-data": "Отображение частичных данных", @@ -10061,11 +10071,7 @@ "wrap-lines": "Переносить строки" }, "logs-navigation": { - "newer-logs": "Новые журналы", - "older-logs": "Старые журналы", - "scroll-bottom": "Прокрутить вниз", - "scroll-top": "Прокрутить вверх", - "start-of-range": "Начало диапазона" + "scroll-top": "Прокрутить вверх" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index ba91a108866..9a1940aa2b4 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -1982,9 +1982,11 @@ }, "notification-preview": { "alertmanager": "Varningshanterare:", - "disabled-tooltip": "", "error": "Kunde inte ladda förhandsgranskning av routing för {{alertmanager}}", "initialized": "Baserat på de etiketter som lagts till dirigeras varningsinstanser till följande aviseringspolicyer. Utöka varje aviseringspolicy nedan för att se mer information.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Förhandsgranska dirigering", "select-folder-tooltip": "", "text-loading-preview": "Laddar förhandsgranskning …", @@ -5098,8 +5100,12 @@ "new-panel": { "configure-button": "Konfigurera", "configure-button-menu": "Växla meny", + "empty-state-message": "", "menu-open-panel-editor": "Konfigurera", - "menu-use-library-panel": "Använd bibliotekspanel" + "menu-use-library-panel": "Använd bibliotekspanel", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Ny panel", "on-create-new-row": { @@ -7218,6 +7224,7 @@ "logs-volume": { "add-filters": "Lägg till fler etiketter för din fråga för att begränsa din sökning.", "decrease-timerange": "Minska tidsintervallet för din fråga.", + "max-bytes-error": "", "much-data": "Frågan försöker komma åt för mycket data. Prova ett eller flera av följande:" }, "maximum-pinned-logs": "Maximalt antal {{PINNED_LOGS_LIMIT}} fästa loggar har uppnåtts. Frigör en logg om du vill lägga till en annan.", @@ -7267,12 +7274,15 @@ "tooltip-select-query-visualize-table": "Välj en fråga att visualisera i tabellen" }, "logs-volume-panel": { - "content-streaming": "Streaming" + "content-streaming": "Streaming", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "Ladda om loggvolym", "label-reload-log-volume": "Ladda om loggvolym", "loading": "Laddar …", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Det gick inte att ladda loggvolym för denna fråga", "title-no-logs-volume-available": "Ingen loggvolym tillgänglig", "title-showing-partial-data": "Visar partiella data", @@ -9973,11 +9983,7 @@ "wrap-lines": "Radbryt linjer" }, "logs-navigation": { - "newer-logs": "Nyare loggar", - "older-logs": "Äldre loggar", - "scroll-bottom": "Skrolla längst ner", - "scroll-top": "Skrolla till toppen", - "start-of-range": "Start av intervall" + "scroll-top": "Skrolla till toppen" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 6515d038e6a..a081fdbd8d3 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -1982,9 +1982,11 @@ }, "notification-preview": { "alertmanager": "Uyarı yöneticisi:", - "disabled-tooltip": "", "error": "{{alertmanager}} için yönlendirme ön izlemesi yüklenemedi", "initialized": "Eklenen etiketlere bağlı olarak uyarı örnekleri aşağıdaki bildirim politikalarına yönlendirilir. Daha fazla ayrıntı görmek için her bir bildirim politikasını genişletin.", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "Yönlendirmeyi ön izle", "select-folder-tooltip": "", "text-loading-preview": "Ön izleme yükleniyor...", @@ -5098,8 +5100,12 @@ "new-panel": { "configure-button": "Yapılandır", "configure-button-menu": "Menüyü aç/kapat", + "empty-state-message": "", "menu-open-panel-editor": "Yapılandır", - "menu-use-library-panel": "Kütüphane panelini kullan" + "menu-use-library-panel": "Kütüphane panelini kullan", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "Yeni panel", "on-create-new-row": { @@ -7218,6 +7224,7 @@ "logs-volume": { "add-filters": "Sorgunuzu daraltmak için sorgunuza daha fazla etiket ekleyin.", "decrease-timerange": "Sorgunuzun zaman aralığını kısaltın.", + "max-bytes-error": "", "much-data": "Sorgu çok fazla veriye erişmeye çalışıyor. Aşağıdakilerden biri veya daha fazlasını yerine getirin:" }, "maximum-pinned-logs": "Maksimum {{PINNED_LOGS_LIMIT}} sabitlenmiş günlük ulaşıldı. Başka bir tane eklemek için bir günlüğün sabitlemesini kaldırın.", @@ -7267,12 +7274,15 @@ "tooltip-select-query-visualize-table": "Tabloda görselleştirmek için bir sorgu seçin" }, "logs-volume-panel": { - "content-streaming": "Akış" + "content-streaming": "Akış", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "", "label-reload-log-volume": "Günlük kaydı hacmini yeniden yükle", "loading": "Yükleniyor...", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "Bu sorgu için günlük kaydı birimi yüklenemedi", "title-no-logs-volume-available": "Günlük kaydı birimi yok", "title-showing-partial-data": "Kısmi veriler gösteriliyor", @@ -9973,11 +9983,7 @@ "wrap-lines": "Satırları kaydır" }, "logs-navigation": { - "newer-logs": "Yeni günlükler", - "older-logs": "Eski günlükler", - "scroll-bottom": "En alta kaydır", - "scroll-top": "En üste kaydır", - "start-of-range": "Aralık başlangıcı" + "scroll-top": "En üste kaydır" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 925a737732c..481399a0bcb 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -1976,9 +1976,11 @@ }, "notification-preview": { "alertmanager": "Alertmanager:", - "disabled-tooltip": "", "error": "无法加载 {{alertmanager}} 的路由预览", "initialized": "根据添加的标签,提醒实例会被路由到以下通知策略。展开下面的每个通知策略以查看更多详细信息。", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "预览路由", "select-folder-tooltip": "", "text-loading-preview": "正在加载预览...", @@ -5078,8 +5080,12 @@ "new-panel": { "configure-button": "配置", "configure-button-menu": "切换菜单", + "empty-state-message": "", "menu-open-panel-editor": "配置", - "menu-use-library-panel": "使用库面板" + "menu-use-library-panel": "使用库面板", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "新面板", "on-create-new-row": { @@ -7194,6 +7200,7 @@ "logs-volume": { "add-filters": "向您的查询添加更多标签以缩小搜索范围。", "decrease-timerange": "缩小查询的时间范围。", + "max-bytes-error": "", "much-data": "该查询正在尝试访问过多的数据。请尝试以下一种或多种方法:" }, "maximum-pinned-logs": "已达到固定日志的最大数量限制 {{PINNED_LOGS_LIMIT}}。取消固定某个日志,以添加另一个日志。", @@ -7243,12 +7250,15 @@ "tooltip-select-query-visualize-table": "选择要在表中可视化的查询" }, "logs-volume-panel": { - "content-streaming": "流媒体传输" + "content-streaming": "流媒体传输", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "重新加载日志卷", "label-reload-log-volume": "重新加载日志卷", "loading": "加载中...", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "加载此查询的日志卷失败", "title-no-logs-volume-available": "没有可用的日志卷", "title-showing-partial-data": "显示部分数据", @@ -9929,11 +9939,7 @@ "wrap-lines": "多行显示" }, "logs-navigation": { - "newer-logs": "新日志", - "older-logs": "旧日志", - "scroll-bottom": "滚动到底部", - "scroll-top": "滚动到顶部", - "start-of-range": "范围的开始" + "scroll-top": "滚动到顶部" }, "logs-panel": { "render-common-labels": { diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index c128f8b4360..061e2cb2f50 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -1976,9 +1976,11 @@ }, "notification-preview": { "alertmanager": "警報管理:", - "disabled-tooltip": "", "error": "無法載入路由預覽:{{alertmanager}}", "initialized": "根據新增的標籤,警報執行個體被路由到以下通知策略。展開下方的每個通知政策以檢視更多詳細資訊。", + "no-condition-tooltip": "", + "permission-warning": "", + "permission-warning-message": "", "preview-routing": "預覽路由", "select-folder-tooltip": "", "text-loading-preview": "正在載入預覽…", @@ -5078,8 +5080,12 @@ "new-panel": { "configure-button": "設定", "configure-button-menu": "切換選單", + "empty-state-message": "", "menu-open-panel-editor": "設定", - "menu-use-library-panel": "使用資料庫面板" + "menu-use-library-panel": "使用資料庫面板", + "suggestions": { + "empty-state-message": "" + } }, "new-panel-title": "新的面板", "on-create-new-row": { @@ -7194,6 +7200,7 @@ "logs-volume": { "add-filters": "在您的查詢中新增更多標籤,以縮小搜尋範圍。", "decrease-timerange": "縮小查詢的時間範圍。", + "max-bytes-error": "", "much-data": "查詢嘗試存取的資料過多。請嘗試以下一個或多個方法:" }, "maximum-pinned-logs": "已達到固定日誌的最大值 {{PINNED_LOGS_LIMIT}}。取消固定一個日誌以便新增另一個。", @@ -7243,12 +7250,15 @@ "tooltip-select-query-visualize-table": "選擇要在表格中視覺化呈現的查詢項目" }, "logs-volume-panel": { - "content-streaming": "串流" + "content-streaming": "串流", + "partial-data-tips": "" }, "logs-volume-panel-list": { "aria-label-reload-log-volume": "重新載入紀錄容量", "label-reload-log-volume": "重新載入紀錄容量", "loading": "正在載入…", + "message-showing-partial-data-max-bytes": "", + "message-showing-partial-data-timeout": "", "title-failed-volume-query": "無法載入此查詢的紀錄容量", "title-no-logs-volume-available": "沒有可用的紀錄容量", "title-showing-partial-data": "顯示部分資料", @@ -9929,11 +9939,7 @@ "wrap-lines": "換行" }, "logs-navigation": { - "newer-logs": "較新的日誌", - "older-logs": "較舊的日誌", - "scroll-bottom": "滾動到底部", - "scroll-top": "捲動回頂部", - "start-of-range": "範圍開始" + "scroll-top": "捲動回頂部" }, "logs-panel": { "render-common-labels": { From 39dc659ad8516ca933f29de6ed0a31c5151e61a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 24 Nov 2025 06:03:45 +0100 Subject: [PATCH 069/378] chore: reduce Azure barrel files (#114297) --- eslint-suppressions.json | 10 ---------- .../azure_log_analytics_datasource.test.ts | 3 ++- .../azure_log_analytics_datasource.ts | 3 ++- .../azure_monitor/azure_monitor_datasource.ts | 3 ++- .../azuremonitor/azure_monitor/url_builder.ts | 2 +- .../azure_resource_graph_datasource.test.ts | 2 +- .../azure_resource_graph_datasource.ts | 4 ++-- .../components/LogsQueryEditor/AzureCheatSheet.tsx | 3 ++- .../components/LogsQueryEditor/LogsQueryEditor.tsx | 4 ++-- .../components/LogsQueryEditor/setQueryValue.ts | 3 ++- .../MetricsQueryEditor/AdvancedResourcePicker.tsx | 2 +- .../MetricsQueryEditor/DimensionFields.tsx | 3 ++- .../MetricsQueryEditor/MetricsQueryEditor.tsx | 3 ++- .../MetricsQueryEditor/dataHooks.test.ts | 3 ++- .../components/MetricsQueryEditor/dataHooks.ts | 3 ++- .../components/MetricsQueryEditor/setQueryValue.ts | 3 ++- .../components/QueryEditor/QueryEditor.test.tsx | 2 +- .../components/QueryEditor/QueryEditor.tsx | 3 ++- .../components/QueryEditor/QueryHeader.tsx | 4 ++-- .../components/QueryEditor/usePreparedQuery.ts | 3 ++- .../components/ResourceField/ResourceField.tsx | 2 +- .../components/ResourcePicker/AdvancedMulti.tsx | 2 +- .../components/ResourcePicker/ResourcePicker.tsx | 2 +- .../components/ResourcePicker/utils.ts | 3 ++- .../components/TracesQueryEditor/Filters.tsx | 2 +- .../TracesQueryEditor/TracesQueryEditor.tsx | 3 ++- .../components/TracesQueryEditor/setQueryValue.ts | 3 ++- .../VariableEditor/GrafanaTemplateVariableFn.tsx | 3 ++- .../VariableEditor/VariableEditor.test.tsx | 3 ++- .../components/VariableEditor/VariableEditor.tsx | 3 ++- .../components/shared/FormatAsField.test.tsx | 3 ++- .../components/shared/FormatAsField.tsx | 2 +- .../datasource/azuremonitor/datasource.test.ts | 2 +- .../plugins/datasource/azuremonitor/datasource.ts | 3 ++- .../azuremonitor/grafanaTemplateVariableFns.ts | 7 ++++--- .../azuremonitor/grafanaTemplateVariables.test.ts | 3 ++- .../plugins/datasource/azuremonitor/mocks/query.ts | 3 ++- .../app/plugins/datasource/azuremonitor/module.ts | 3 ++- .../resourcePicker/resourcePickerData.ts | 3 ++- .../plugins/datasource/azuremonitor/types/query.ts | 12 ------------ .../azuremonitor/types/templateVariables.ts | 14 -------------- .../plugins/datasource/azuremonitor/types/types.ts | 3 ++- .../azuremonitor/utils/migrateAnnotation.test.ts | 3 ++- .../azuremonitor/utils/migrateAnnotation.ts | 3 ++- .../azuremonitor/utils/migrateQuery.test.ts | 3 ++- .../datasource/azuremonitor/utils/migrateQuery.ts | 3 ++- .../datasource/azuremonitor/variables.test.ts | 3 ++- .../plugins/datasource/azuremonitor/variables.ts | 4 ++-- 48 files changed, 82 insertions(+), 87 deletions(-) delete mode 100644 public/app/plugins/datasource/azuremonitor/types/templateVariables.ts diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 84e5d786552..b3eaa1feb52 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3706,16 +3706,6 @@ "count": 7 } }, - "public/app/plugins/datasource/azuremonitor/types/query.ts": { - "no-barrel-files/no-barrel-files": { - "count": 3 - } - }, - "public/app/plugins/datasource/azuremonitor/types/templateVariables.ts": { - "no-barrel-files/no-barrel-files": { - "count": 1 - } - }, "public/app/plugins/datasource/azuremonitor/utils/common.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 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 88043e66dc2..9d6b0a15d8f 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 @@ -1,9 +1,10 @@ import { CustomVariableModel } from '@grafana/data'; +import { AzureLogsQuery, AzureQueryType, AzureTracesQuery } from '../dataquery.gen'; import { Context, createContext } from '../mocks/datasource'; import createMockQuery from '../mocks/query'; import { singleVariable } from '../mocks/variables'; -import { AzureLogsQuery, AzureMonitorQuery, AzureQueryType, AzureTracesQuery } from '../types/query'; +import { AzureMonitorQuery } from '../types/query'; import AzureLogAnalyticsDatasource from './azure_log_analytics_datasource'; import FakeSchemaData from './mocks/schema'; 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 77733e2dfa3..97c121fecdd 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 @@ -6,7 +6,8 @@ import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/run import ResponseParser from '../azure_monitor/response_parser'; import { getCredentials } from '../credentials'; -import { AzureMonitorQuery, AzureQueryType } from '../types/query'; +import { AzureQueryType } from '../dataquery.gen'; +import { AzureMonitorQuery } from '../types/query'; import { AzureMonitorDataSourceJsonData, AzureMonitorDataSourceInstanceSettings, diff --git a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts index a4bb34dc5f9..7afc9b511f5 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts @@ -5,8 +5,9 @@ import { ScopedVars } from '@grafana/data'; import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { getCredentials } from '../credentials'; +import { AzureMetricQuery, AzureQueryType } from '../dataquery.gen'; import TimegrainConverter from '../time_grain_converter'; -import { AzureMetricQuery, AzureMonitorQuery, AzureQueryType } from '../types/query'; +import { AzureMonitorQuery } from '../types/query'; import { AzureAPIResponse, AzureMonitorDataSourceInstanceSettings, diff --git a/public/app/plugins/datasource/azuremonitor/azure_monitor/url_builder.ts b/public/app/plugins/datasource/azuremonitor/azure_monitor/url_builder.ts index 545c1648315..7fe6944d9f0 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_monitor/url_builder.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_monitor/url_builder.ts @@ -1,6 +1,6 @@ import { TemplateSrv } from '@grafana/runtime'; -import { AzureMonitorResource } from '../types/query'; +import { AzureMonitorResource } from '../dataquery.gen'; import { GetMetricNamespacesQuery, GetMetricNamesQuery } from '../types/types'; export default class UrlBuilder { diff --git a/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.test.ts b/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.test.ts index 8fbb4cbe3e6..a26da4563cb 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.test.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.test.ts @@ -2,12 +2,12 @@ import { get, set } from 'lodash'; import { CustomVariableModel } from '@grafana/data'; +import { AzureQueryType } from '../dataquery.gen'; import { Context, createContext } from '../mocks/datasource'; import { createMockInstanceSetttings } from '../mocks/instanceSettings'; import createMockQuery from '../mocks/query'; import { createTemplateVariables } from '../mocks/utils'; import { multiVariable, singleVariable, subscriptionsVariable } from '../mocks/variables'; -import { AzureQueryType } from '../types/query'; import AzureResourceGraphDatasource from './azure_resource_graph_datasource'; diff --git a/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.ts b/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.ts index c7b3877f429..1bc562ed391 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.ts @@ -4,9 +4,9 @@ import { ScopedVars } from '@grafana/data'; import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { resourceTypes } from '../azureMetadata/resourceTypes'; -import { ARGScope } from '../dataquery.gen'; +import { ARGScope, AzureQueryType } from '../dataquery.gen'; import { createFilter } from '../resourcePicker/resourcePickerData'; -import { AzureMonitorQuery, AzureQueryType } from '../types/query'; +import { AzureMonitorQuery } from '../types/query'; import { AzureGetResourceNamesQuery, AzureGraphResponse, diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx index 7ed4304b881..d61e8d03f5e 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx @@ -18,7 +18,8 @@ import { } from '@grafana/ui'; import AzureLogAnalyticsDatasource from '../../azure_log_analytics/azure_log_analytics_datasource'; -import { AzureMonitorQuery, AzureQueryType } from '../../types/query'; +import { AzureQueryType } from '../../dataquery.gen'; +import { AzureMonitorQuery } from '../../types/query'; import { Category, CheatsheetQueries, CheatsheetQuery, DropdownCategories } from '../../types/types'; import { RawQuery } from './RawQuery'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx index 00125f293b7..bec68fee1c7 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -6,11 +6,11 @@ import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/plugin-ui'; import { config, getTemplateSrv } from '@grafana/runtime'; import { Alert, LinkButton, Space, Text, TextLink } from '@grafana/ui'; -import { LogsEditorMode } from '../../dataquery.gen'; +import { LogsEditorMode, ResultFormat } from '../../dataquery.gen'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; import { AzureLogAnalyticsMetadataTable } from '../../types/logAnalyticsMetadata'; -import { AzureMonitorQuery, ResultFormat } from '../../types/query'; +import { AzureMonitorQuery } from '../../types/query'; import { AzureMonitorErrorish, AzureMonitorOption, EngineSchema } from '../../types/types'; import { LogsQueryBuilder } from '../LogsQueryBuilder/LogsQueryBuilder'; import ResourceField from '../ResourceField/ResourceField'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/setQueryValue.ts b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/setQueryValue.ts index 9662afd52af..1e4422cd4b5 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/setQueryValue.ts +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/setQueryValue.ts @@ -1,6 +1,7 @@ import { SelectableValue } from '@grafana/data'; -import { AzureMonitorQuery, ResultFormat } from '../../types/query'; +import { ResultFormat } from '../../dataquery.gen'; +import { AzureMonitorQuery } from '../../types/query'; export function setKustoQuery(query: AzureMonitorQuery, kustoQuery: string): AzureMonitorQuery { return { diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx index abf4d91de90..1be90aafd26 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx @@ -6,8 +6,8 @@ import { Trans, t } from '@grafana/i18n'; import { AccessoryButton } from '@grafana/plugin-ui'; import { Input, Label, InlineField, Button, useStyles2 } from '@grafana/ui'; +import { AzureMonitorResource } from '../../dataquery.gen'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorResource } from '../../types/query'; export interface ResourcePickerProps { resources: T[]; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx index 6a7d3c22d37..c5d9e1626f5 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx @@ -5,7 +5,8 @@ import { t } from '@grafana/i18n'; import { EditorList, AccessoryButton } from '@grafana/plugin-ui'; import { Select, Stack, MultiSelect } from '@grafana/ui'; -import { AzureMetricDimension, AzureMonitorQuery } from '../../types/query'; +import { AzureMetricDimension } from '../../dataquery.gen'; +import { AzureMonitorQuery } from '../../types/query'; import { AzureMonitorOption, AzureQueryEditorFieldProps } from '../../types/types'; import { Field } from '../shared/Field'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx index 3490908f4c5..52503350614 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -2,9 +2,10 @@ import { PanelData } from '@grafana/data'; import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/plugin-ui'; import { multiResourceCompatibleTypes } from '../../azureMetadata/resourceTypes'; +import { AzureMonitorResource } from '../../dataquery.gen'; import type Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorQuery, AzureMonitorResource } from '../../types/query'; +import { AzureMonitorQuery } from '../../types/query'; import { AzureMonitorOption, AzureMonitorErrorish } from '../../types/types'; import ResourceField from '../ResourceField/ResourceField'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.test.ts b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.test.ts index 47a6edf0a19..6ca4eaf27cb 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.test.ts +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.test.ts @@ -1,8 +1,9 @@ import { renderHook, waitFor } from '@testing-library/react'; +import { AzureMetricQuery, AzureQueryType } from '../../dataquery.gen'; import Datasource from '../../datasource'; import createMockDatasource from '../../mocks/datasource'; -import { AzureMetricQuery, AzureMonitorQuery, AzureQueryType } from '../../types/query'; +import { AzureMonitorQuery } from '../../types/query'; import { AzureMonitorOption } from '../../types/types'; import { diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.ts b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.ts index 82115c289c4..97a0a6579b7 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.ts +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.ts @@ -2,9 +2,10 @@ import { useEffect, useState } from 'react'; import { rangeUtil } from '@grafana/data'; +import { AzureMonitorResource } from '../../dataquery.gen'; import Datasource from '../../datasource'; import TimegrainConverter from '../../time_grain_converter'; -import { AzureMonitorQuery, AzureMonitorResource } from '../../types/query'; +import { AzureMonitorQuery } from '../../types/query'; import { AzureMonitorErrorish, AzureMonitorOption } from '../../types/types'; import { toOption } from '../../utils/common'; import { useAsyncState } from '../../utils/useAsyncState'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/setQueryValue.ts b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/setQueryValue.ts index 05b17493ca3..92671157dde 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/setQueryValue.ts +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/setQueryValue.ts @@ -1,4 +1,5 @@ -import { AzureMetricDimension, AzureMonitorQuery } from '../../types/query'; +import { AzureMetricDimension } from '../../dataquery.gen'; +import { AzureMonitorQuery } from '../../types/query'; export function setCustomNamespace(query: AzureMonitorQuery, selection: string | undefined): AzureMonitorQuery { if (query.azureMonitor?.customNamespace === selection) { diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx index 17ccab9ea64..f9df635473c 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx @@ -5,11 +5,11 @@ import { config } from '@grafana/runtime'; // eslint-disable-next-line no-restricted-imports import * as ui from '@grafana/ui'; +import { AzureQueryType, ResultFormat } from '../../dataquery.gen'; import { selectors } from '../../e2e/selectors'; import createMockDatasource from '../../mocks/datasource'; import { invalidNamespaceError } from '../../mocks/errors'; import createMockQuery from '../../mocks/query'; -import { AzureQueryType, ResultFormat } from '../../types/query'; import { selectOptionInTest } from '../../utils/testUtils'; import { createMockResourcePickerData } from '../LogsQueryEditor/mocks'; diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx index 39968c166b5..dce90eecc02 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx @@ -7,9 +7,10 @@ import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Alert, CodeEditor, Space, TextLink } from '@grafana/ui'; +import { AzureQueryType } from '../../dataquery.gen'; import AzureMonitorDatasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorQuery, AzureQueryType } from '../../types/query'; +import { AzureMonitorQuery } from '../../types/query'; import { AzureMonitorDataSourceJsonData, AzureMonitorErrorish, AzureMonitorOption } from '../../types/types'; import useLastError from '../../utils/useLastError'; import ArgQueryEditor from '../ArgQueryEditor/ArgQueryEditor'; diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx index 75d89a8fc47..ab7c817c935 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx @@ -6,9 +6,9 @@ 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 { AzureQueryType, LogsEditorMode } from '../../dataquery.gen'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorQuery, AzureQueryType } from '../../types/query'; +import { AzureMonitorQuery } from '../../types/query'; interface QueryTypeFieldProps { query: AzureMonitorQuery; diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/usePreparedQuery.ts b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/usePreparedQuery.ts index c2da635fcd0..210fe47087b 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/usePreparedQuery.ts +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/usePreparedQuery.ts @@ -2,7 +2,8 @@ import deepEqual from 'fast-deep-equal'; import { defaults } from 'lodash'; import { useEffect, useMemo } from 'react'; -import { AzureMonitorQuery, AzureQueryType, ResultFormat } from '../../types/query'; +import { AzureQueryType, ResultFormat } from '../../dataquery.gen'; +import { AzureMonitorQuery } from '../../types/query'; import migrateQuery from '../../utils/migrateQuery'; const DEFAULT_QUERY = { diff --git a/public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx b/public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx index 35af5ad31f6..85a1d69401a 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx @@ -5,10 +5,10 @@ import * as React from 'react'; import { Trans, t } from '@grafana/i18n'; import { Button, Icon, Modal, useStyles2, IconName } from '@grafana/ui'; +import { AzureMonitorResource } from '../../dataquery.gen'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; import { ResourcePickerQueryType } from '../../resourcePicker/resourcePickerData'; -import { AzureMonitorResource } from '../../types/query'; import { AzureQueryEditorFieldProps } from '../../types/types'; import ResourcePicker from '../ResourcePicker/ResourcePicker'; import getStyles from '../ResourcePicker/styles'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/AdvancedMulti.tsx b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/AdvancedMulti.tsx index f9d5cee1d55..6e8b6ed675e 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/AdvancedMulti.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/AdvancedMulti.tsx @@ -4,8 +4,8 @@ import * as React from 'react'; import { t } from '@grafana/i18n'; import { Collapse, Space } from '@grafana/ui'; +import { AzureMonitorResource } from '../../dataquery.gen'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorResource } from '../../types/query'; export interface ResourcePickerProps { resources: T[]; diff --git a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.tsx b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.tsx index 3bacbdc2a81..cbe2e8a3154 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.tsx @@ -24,10 +24,10 @@ import { } from '@grafana/ui'; import { resourceTypeDisplayNames } from '../../azureMetadata/resourceTypes'; +import { AzureMonitorResource } from '../../dataquery.gen'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; import ResourcePickerData, { ResourcePickerQueryType } from '../../resourcePicker/resourcePickerData'; -import { AzureMonitorResource } from '../../types/query'; import { ResourceGraphFilters } from '../../types/types'; import messageFromError from '../../utils/messageFromError'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/utils.ts b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/utils.ts index bcc9c9d2d03..37c82058ba3 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/utils.ts +++ b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/utils.ts @@ -3,8 +3,9 @@ import { produce } from 'immer'; import { getTemplateSrv } from '@grafana/runtime'; import UrlBuilder from '../../azure_monitor/url_builder'; +import { AzureMonitorResource } from '../../dataquery.gen'; import { ResourcePickerQueryType } from '../../resourcePicker/resourcePickerData'; -import { AzureMonitorQuery, AzureMonitorResource } from '../../types/query'; +import { AzureMonitorQuery } from '../../types/query'; import { ResourceRow, ResourceRowGroup } from './types'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx index 785c9fd4e12..e6e670cdc66 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx @@ -6,7 +6,7 @@ import { t } from '@grafana/i18n'; import { EditorList } from '@grafana/plugin-ui'; import { Field } from '@grafana/ui'; -import { AzureTracesFilter } from '../../types/query'; +import { AzureTracesFilter } from '../../dataquery.gen'; import { AzureQueryEditorFieldProps } from '../../types/types'; import { makeRenderItem } from './Filter'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx index 46729f22dda..d38a611ede1 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx @@ -7,9 +7,10 @@ import { t } from '@grafana/i18n'; import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/plugin-ui'; import { Input } from '@grafana/ui'; +import { AzureQueryType, ResultFormat } from '../../dataquery.gen'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorQuery, AzureQueryType, ResultFormat } from '../../types/query'; +import { AzureMonitorQuery } from '../../types/query'; import { AzureMonitorErrorish, AzureMonitorOption } from '../../types/types'; import AdvancedResourcePicker from '../LogsQueryEditor/AdvancedResourcePicker'; import ResourceField from '../ResourceField/ResourceField'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/setQueryValue.ts b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/setQueryValue.ts index b901c7b800d..33d98fc091c 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/setQueryValue.ts +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/setQueryValue.ts @@ -1,6 +1,7 @@ import { SelectableValue } from '@grafana/data'; -import { AzureMonitorQuery, AzureQueryType, AzureTracesFilter, ResultFormat } from '../../types/query'; +import { AzureQueryType, AzureTracesFilter, ResultFormat } from '../../dataquery.gen'; +import { AzureMonitorQuery } from '../../types/query'; // Used when switching from a traces exemplar query to a standard Azure Traces query export function setDefaultTracesQuery(query: AzureMonitorQuery): AzureMonitorQuery { diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/GrafanaTemplateVariableFn.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/GrafanaTemplateVariableFn.tsx index 1727cac59a7..b090561f716 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/GrafanaTemplateVariableFn.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/GrafanaTemplateVariableFn.tsx @@ -3,9 +3,10 @@ import { ChangeEvent, useCallback, useEffect, useState } from 'react'; import { t } from '@grafana/i18n'; import { InlineField, Input } from '@grafana/ui'; +import { AzureQueryType } from '../../dataquery.gen'; import DataSource from '../../datasource'; import { migrateStringQueriesToObjectQueries } from '../../grafanaTemplateVariableFns'; -import { AzureMonitorQuery, AzureQueryType } from '../../types/query'; +import { AzureMonitorQuery } from '../../types/query'; const GrafanaTemplateVariableFnInput = ({ query, diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx index c400eb35a7c..3c103976311 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx @@ -6,8 +6,9 @@ import { select, openMenu } from 'react-select-event'; // eslint-disable-next-line no-restricted-imports import * as ui from '@grafana/ui'; +import { AzureQueryType } from '../../dataquery.gen'; import createMockDatasource from '../../mocks/datasource'; -import { AzureMonitorQuery, AzureQueryType } from '../../types/query'; +import { AzureMonitorQuery } from '../../types/query'; import VariableEditor from './VariableEditor'; diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx index 75ced4bacd0..a20e6cdb904 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx @@ -8,10 +8,11 @@ import { getTemplateSrv } from '@grafana/runtime'; import { Alert, Field, Select, Space } from '@grafana/ui'; import UrlBuilder from '../../azure_monitor/url_builder'; +import { AzureQueryType } from '../../dataquery.gen'; import DataSource from '../../datasource'; import { selectors } from '../../e2e/selectors'; import { migrateQuery } from '../../grafanaTemplateVariableFns'; -import { AzureMonitorQuery, AzureQueryType } from '../../types/query'; +import { AzureMonitorQuery } from '../../types/query'; import { AzureMonitorOption } from '../../types/types'; import useLastError from '../../utils/useLastError'; import ArgQueryEditor from '../ArgQueryEditor/ArgQueryEditor'; diff --git a/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.test.tsx b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.test.tsx index ed02f1b9ce3..3131a12cf17 100644 --- a/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.test.tsx @@ -2,9 +2,10 @@ import { render, screen } from '@testing-library/react'; import { SelectableValue } from '@grafana/data'; +import { ResultFormat } from '../../dataquery.gen'; import createMockDatasource from '../../mocks/datasource'; import createMockQuery from '../../mocks/query'; -import { AzureMonitorQuery, ResultFormat } from '../../types/query'; +import { AzureMonitorQuery } from '../../types/query'; import FormatAsField from './FormatAsField'; diff --git a/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx index 240db475305..65ed6e4334c 100644 --- a/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx @@ -5,8 +5,8 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { Select } from '@grafana/ui'; +import { ResultFormat } from '../../dataquery.gen'; import { selectors } from '../../e2e/selectors'; -import { ResultFormat } from '../../types/query'; import { FormatAsFieldProps } from '../../types/types'; import { Field } from './Field'; diff --git a/public/app/plugins/datasource/azuremonitor/datasource.test.ts b/public/app/plugins/datasource/azuremonitor/datasource.test.ts index be090fa6254..62256b8e592 100644 --- a/public/app/plugins/datasource/azuremonitor/datasource.test.ts +++ b/public/app/plugins/datasource/azuremonitor/datasource.test.ts @@ -1,8 +1,8 @@ +import { AzureQueryType } from './dataquery.gen'; import Datasource from './datasource'; import { createMockInstanceSetttings } from './mocks/instanceSettings'; import createMockQuery from './mocks/query'; import { singleVariable } from './mocks/variables'; -import { AzureQueryType } from './types/query'; jest.mock('@grafana/runtime', () => { return { diff --git a/public/app/plugins/datasource/azuremonitor/datasource.ts b/public/app/plugins/datasource/azuremonitor/datasource.ts index 104d4c2de86..4ea7738fffd 100644 --- a/public/app/plugins/datasource/azuremonitor/datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/datasource.ts @@ -17,8 +17,9 @@ import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/run import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource'; import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource'; import AzureResourceGraphDatasource from './azure_resource_graph/azure_resource_graph_datasource'; +import { AzureQueryType } from './dataquery.gen'; import ResourcePickerData from './resourcePicker/resourcePickerData'; -import { AzureMonitorQuery, AzureQueryType } from './types/query'; +import { AzureMonitorQuery } from './types/query'; import { AzureMonitorDataSourceJsonData } from './types/types'; import migrateAnnotation from './utils/migrateAnnotation'; import migrateQuery from './utils/migrateQuery'; diff --git a/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariableFns.ts b/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariableFns.ts index 3e3945da4f2..6e0eaa3c966 100644 --- a/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariableFns.ts +++ b/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariableFns.ts @@ -1,9 +1,8 @@ import { isGUIDish } from './components/ResourcePicker/utils'; -import DataSource from './datasource'; -import { AzureMonitorQuery, AzureQueryType } from './types/query'; import { AppInsightsGroupByQuery, AppInsightsMetricNameQuery, + AzureQueryType, GrafanaTemplateVariableQuery, MetricNamespaceQuery, MetricNamesQuery, @@ -11,7 +10,9 @@ import { ResourceNamesQuery, SubscriptionsQuery, WorkspacesQuery, -} from './types/templateVariables'; +} from './dataquery.gen'; +import DataSource from './datasource'; +import { AzureMonitorQuery } from './types/query'; /* Grafana Template Variable Functions diff --git a/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariables.test.ts b/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariables.test.ts index d5db96929e2..f391e84e17b 100644 --- a/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariables.test.ts +++ b/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariables.test.ts @@ -1,6 +1,7 @@ +import { AzureQueryType } from './dataquery.gen'; import { migrateQuery, migrateStringQueriesToObjectQueries } from './grafanaTemplateVariableFns'; import createMockDatasource from './mocks/datasource'; -import { AzureMonitorQuery, AzureQueryType } from './types/query'; +import { AzureMonitorQuery } from './types/query'; describe('migrateStringQueriesToObjectQueries', () => { const expectedMigrations: Array<{ input: string; output: AzureMonitorQuery }> = [ diff --git a/public/app/plugins/datasource/azuremonitor/mocks/query.ts b/public/app/plugins/datasource/azuremonitor/mocks/query.ts index 6fbfb66b365..1e7662fd575 100644 --- a/public/app/plugins/datasource/azuremonitor/mocks/query.ts +++ b/public/app/plugins/datasource/azuremonitor/mocks/query.ts @@ -1,4 +1,5 @@ -import { AzureMonitorQuery, AzureQueryType, ResultFormat } from '../types/query'; +import { AzureQueryType, ResultFormat } from '../dataquery.gen'; +import { AzureMonitorQuery } from '../types/query'; export default function createMockQuery(overrides?: Partial): AzureMonitorQuery { return { diff --git a/public/app/plugins/datasource/azuremonitor/module.ts b/public/app/plugins/datasource/azuremonitor/module.ts index 5528db67d56..9c96f129cc2 100644 --- a/public/app/plugins/datasource/azuremonitor/module.ts +++ b/public/app/plugins/datasource/azuremonitor/module.ts @@ -4,10 +4,11 @@ import { getAppEvents } from '@grafana/runtime'; import { ConfigEditor } from './components/ConfigEditor/ConfigEditor'; import AzureMonitorQueryEditor from './components/QueryEditor/QueryEditor'; +import { AzureQueryType, ResultFormat } from './dataquery.gen'; import Datasource from './datasource'; import pluginJson from './plugin.json'; import { trackAzureMonitorDashboardLoaded } from './tracking'; -import { AzureMonitorQuery, AzureQueryType, ResultFormat } from './types/query'; +import { AzureMonitorQuery } from './types/query'; import { AzureMonitorDataSourceJsonData } from './types/types'; // don't load plugin translations in test environments diff --git a/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts b/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts index 64215cc2f59..749b6295e53 100644 --- a/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts +++ b/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts @@ -13,7 +13,8 @@ import { parseResourceURI, resourceToString, } from '../components/ResourcePicker/utils'; -import { AzureMonitorQuery, AzureMonitorResource } from '../types/query'; +import { AzureMonitorResource } from '../dataquery.gen'; +import { AzureMonitorQuery } from '../types/query'; import { AzureMonitorDataSourceInstanceSettings, AzureMonitorDataSourceJsonData, diff --git a/public/app/plugins/datasource/azuremonitor/types/query.ts b/public/app/plugins/datasource/azuremonitor/types/query.ts index c9098ef91c7..1078e4bcc5e 100644 --- a/public/app/plugins/datasource/azuremonitor/types/query.ts +++ b/public/app/plugins/datasource/azuremonitor/types/query.ts @@ -1,17 +1,5 @@ import { AzureMonitorQuery as AzureMonitorQueryBase, AzureQueryType } from '../dataquery.gen'; -export { AzureQueryType }; -export type { - AzureMetricQuery, - AzureLogsQuery, - AzureResourceGraphQuery, - AzureTracesQuery, - AzureMonitorResource, - AzureMetricDimension, - AzureTracesFilter, -} from '../dataquery.gen'; -export { ResultFormat } from '../dataquery.gen'; - /** * Represents the query as it moves through the frontend query editor and datasource files. * It can represent new queries that are still being edited, so all properties are optional diff --git a/public/app/plugins/datasource/azuremonitor/types/templateVariables.ts b/public/app/plugins/datasource/azuremonitor/types/templateVariables.ts deleted file mode 100644 index 467a8d3a3c9..00000000000 --- a/public/app/plugins/datasource/azuremonitor/types/templateVariables.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type { - GrafanaTemplateVariableQueryType, - UnknownQuery, - AppInsightsMetricNameQuery, - AppInsightsGroupByQuery, - SubscriptionsQuery, - ResourceGroupsQuery, - ResourceNamesQuery, - MetricNamespaceQuery, - MetricDefinitionsQuery, - MetricNamesQuery, - WorkspacesQuery, - GrafanaTemplateVariableQuery, -} from '../dataquery.gen'; diff --git a/public/app/plugins/datasource/azuremonitor/types/types.ts b/public/app/plugins/datasource/azuremonitor/types/types.ts index 99ab54f5b54..7514d3df103 100644 --- a/public/app/plugins/datasource/azuremonitor/types/types.ts +++ b/public/app/plugins/datasource/azuremonitor/types/types.ts @@ -3,10 +3,11 @@ import { EntityGroup, Function, ScalarParameter, TabularParameter } from '@kusto import { AzureDataSourceJsonData, AzureDataSourceSecureJsonData } from '@grafana/azure-sdk'; import { DataSourceInstanceSettings, DataSourceSettings, PanelData, SelectableValue, TimeRange } from '@grafana/data'; +import { ResultFormat } from '../dataquery.gen'; import Datasource from '../datasource'; import { AzureLogAnalyticsMetadataTable } from './logAnalyticsMetadata'; -import { AzureMonitorQuery, ResultFormat } from './query'; +import { AzureMonitorQuery } from './query'; export type AzureMonitorDataSourceSettings = DataSourceSettings< AzureMonitorDataSourceJsonData, diff --git a/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.test.ts b/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.test.ts index b1642f250e2..8c994cbbd88 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.test.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.test.ts @@ -1,6 +1,7 @@ import { AnnotationQuery } from '@grafana/data'; -import { AzureMonitorQuery, AzureQueryType } from '../types/query'; +import { AzureQueryType } from '../dataquery.gen'; +import { AzureMonitorQuery } from '../types/query'; import migrateAnnotation from './migrateAnnotation'; diff --git a/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.ts b/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.ts index ce37019e15f..fe8351153b8 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.ts @@ -1,6 +1,7 @@ import { AnnotationQuery } from '@grafana/data'; -import { AzureMonitorQuery, AzureQueryType } from '../types/query'; +import { AzureQueryType } from '../dataquery.gen'; +import { AzureMonitorQuery } from '../types/query'; // The old Angular annotations editor put some properties (rawQuery, workspace, subscription) // on the root annotation object, rather than down in the 'targets' query value diff --git a/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.test.ts b/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.test.ts index 7e5e1769660..2dd3863ba0b 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.test.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.test.ts @@ -1,4 +1,5 @@ -import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType, ResultFormat } from '../types/query'; +import { AzureMetricDimension, AzureQueryType, ResultFormat } from '../dataquery.gen'; +import { AzureMonitorQuery } from '../types/query'; import migrateQuery from './migrateQuery'; diff --git a/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.ts b/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.ts index 6ebf853d8fa..6a5d3115133 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.ts @@ -4,8 +4,9 @@ import { setTimeGrain as setMetricsTimeGrain, } from '../components/MetricsQueryEditor/setQueryValue'; import { parseResourceDetails } from '../components/ResourcePicker/utils'; +import { AzureMetricDimension, AzureQueryType } from '../dataquery.gen'; import TimegrainConverter from '../time_grain_converter'; -import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType } from '../types/query'; +import { AzureMonitorQuery } from '../types/query'; const OLD_DEFAULT_DROPDOWN_VALUE = 'select'; diff --git a/public/app/plugins/datasource/azuremonitor/variables.test.ts b/public/app/plugins/datasource/azuremonitor/variables.test.ts index 55102e3fcfa..fe21e13af55 100644 --- a/public/app/plugins/datasource/azuremonitor/variables.test.ts +++ b/public/app/plugins/datasource/azuremonitor/variables.test.ts @@ -2,9 +2,10 @@ import { from, lastValueFrom } from 'rxjs'; import { DataQueryRequest, toDataFrame } from '@grafana/data'; +import { AzureQueryType } from './dataquery.gen'; import createMockDatasource from './mocks/datasource'; import { invalidSubscriptionError } from './mocks/errors'; -import { AzureMonitorQuery, AzureQueryType } from './types/query'; +import { AzureMonitorQuery } from './types/query'; import { VariableSupport } from './variables'; jest.mock('@grafana/runtime', () => ({ diff --git a/public/app/plugins/datasource/azuremonitor/variables.ts b/public/app/plugins/datasource/azuremonitor/variables.ts index 5477def0e2d..eafef180265 100644 --- a/public/app/plugins/datasource/azuremonitor/variables.ts +++ b/public/app/plugins/datasource/azuremonitor/variables.ts @@ -13,10 +13,10 @@ import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import UrlBuilder from './azure_monitor/url_builder'; import { parseResourceURI } from './components/ResourcePicker/utils'; import VariableEditor from './components/VariableEditor/VariableEditor'; +import { AzureQueryType, GrafanaTemplateVariableQuery } from './dataquery.gen'; import DataSource from './datasource'; import { migrateQuery } from './grafanaTemplateVariableFns'; -import { AzureMonitorQuery, AzureQueryType } from './types/query'; -import { GrafanaTemplateVariableQuery } from './types/templateVariables'; +import { AzureMonitorQuery } from './types/query'; import { RawAzureResourceItem } from './types/types'; import messageFromError from './utils/messageFromError'; From 8d75d79313549120f87222bb56819a6179ade78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Bedi?= Date: Mon, 24 Nov 2025 10:26:41 +0100 Subject: [PATCH 070/378] PostgreSQL: Remove feature toggle `postgresDSUsePGX` (#113675) * PostgreSQL: Remove feature toggle `postgresDSUsePGX` * Fix tests and linting * Address review comments --- .../src/types/featureToggles.gen.ts | 4 - .../backendplugin/coreplugin/registry.go | 2 +- pkg/server/wire_gen.go | 4 +- pkg/services/featuremgmt/registry.go | 6 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - .../plugintest/plugins_test.go | 2 +- .../grafana-postgresql-datasource/locker.go | 85 - .../locker_test.go | 63 - .../pgx/handler_checkhealth.go | 117 -- .../pgx/handler_checkhealth_test.go | 61 - .../pgx/sql_engine.go | 836 --------- .../pgx/sql_engine_test.go | 681 ------- .../pgxtlsmanager.go | 151 -- .../pgxtlsmanager_test.go | 245 --- .../grafana-postgresql-datasource/postgres.go | 133 +- .../postgres_pgx_snapshot_test.go | 196 -- .../postgres_pgx_test.go | 1579 ----------------- .../postgres_service.go | 49 +- .../postgres_snapshot_test.go | 26 +- .../postgres_test.go | 327 +++- .../grafana-postgresql-datasource/proxy.go | 31 +- .../proxy_test.go | 15 +- .../sqleng/handler_checkhealth.go | 21 +- .../sqleng/handler_checkhealth_test.go | 10 - .../sqleng/sql_engine.go | 440 ++++- .../sqleng/sql_engine_test.go | 242 +++ .../standalone/main.go | 5 +- .../table/types_datetime.golden.jsonc | 64 +- .../table/types_datetime_pgx.golden.jsonc | 214 --- .../testdata/table/types_datetime_pgx.sql | 44 - .../tlsmanager.go | 274 +-- .../tlsmanager_test.go | 369 ++-- .../configuration/ConfigurationEditor.tsx | 40 +- 34 files changed, 1176 insertions(+), 5165 deletions(-) delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/locker.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/locker_test.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/pgx/handler_checkhealth.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/pgx/handler_checkhealth_test.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/pgx/sql_engine.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/pgx/sql_engine_test.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager_test.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_snapshot_test.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_test.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.golden.jsonc delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.sql diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b215e95220f..35d876518b9 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -922,10 +922,6 @@ export interface FeatureToggles { */ grafanaAssistantInProfilesDrilldown?: boolean; /** - * Enables using PGX instead of libpq for PostgreSQL datasource - */ - postgresDSUsePGX?: boolean; - /** * Enables creating alerts from Tempo data source */ tempoAlerting?: boolean; diff --git a/pkg/plugins/backendplugin/coreplugin/registry.go b/pkg/plugins/backendplugin/coreplugin/registry.go index 811d400c520..1e610b1ef1c 100644 --- a/pkg/plugins/backendplugin/coreplugin/registry.go +++ b/pkg/plugins/backendplugin/coreplugin/registry.go @@ -237,7 +237,7 @@ func NewPlugin(pluginID string, cfg *setting.Cfg, httpClientProvider *httpclient case Tempo: svc = tempo.ProvideService(httpClientProvider, tracer) case PostgreSQL: - svc = postgres.ProvideService(cfg) + svc = postgres.ProvideService() case MySQL: svc = mysql.ProvideService() case MSSQL: diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 716cd7a9c50..ce8345d2b09 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -400,7 +400,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api prometheusService := prometheus.ProvideService(httpclientProvider) tempoService := tempo.ProvideService(httpclientProvider, tracer) testdatasourceService := testdatasource.ProvideService() - postgresService := postgres.ProvideService(cfg) + postgresService := postgres.ProvideService() mysqlService := mysql.ProvideService() mssqlService := mssql.ProvideService(cfg) entityEventsService := store.ProvideEntityEventsService(cfg, sqlStore, featureToggles) @@ -1047,7 +1047,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac prometheusService := prometheus.ProvideService(httpclientProvider) tempoService := tempo.ProvideService(httpclientProvider, tracer) testdatasourceService := testdatasource.ProvideService() - postgresService := postgres.ProvideService(cfg) + postgresService := postgres.ProvideService() mysqlService := mysql.ProvideService() mssqlService := mssql.ProvideService(cfg) entityEventsService := store.ProvideEntityEventsService(cfg, sqlStore, featureToggles) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index e4afd8e5c11..ba7abae5a6d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1518,12 +1518,6 @@ var ( FrontendOnly: true, Expression: "true", }, - { - Name: "postgresDSUsePGX", - Description: "Enables using PGX instead of libpq for PostgreSQL datasource", - Stage: FeatureStageExperimental, - Owner: grafanaOSSBigTent, - }, { Name: "tempoAlerting", Description: "Enables creating alerts from Tempo data source", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 25e97404857..cd3222235fa 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -208,7 +208,6 @@ unifiedNavbars,GA,@grafana/plugins-platform-backend,false,false,true logsPanelControls,preview,@grafana/observability-logs,false,false,true metricsFromProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,true grafanaAssistantInProfilesDrilldown,GA,@grafana/observability-traces-and-profiling,false,false,true -postgresDSUsePGX,experimental,@grafana/oss-big-tent,false,false,false tempoAlerting,experimental,@grafana/observability-traces-and-profiling,false,false,false pluginsAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 059ed11e535..284ce16e739 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -606,10 +606,6 @@ const ( // use multi-tenant path for awsTempCredentials FlagMultiTenantTempCredentials = "multiTenantTempCredentials" - // FlagPostgresDSUsePGX - // Enables using PGX instead of libpq for PostgreSQL datasource - FlagPostgresDSUsePGX = "postgresDSUsePGX" - // FlagTempoAlerting // Enables creating alerts from Tempo data source FlagTempoAlerting = "tempoAlerting" diff --git a/pkg/services/pluginsintegration/plugintest/plugins_test.go b/pkg/services/pluginsintegration/plugintest/plugins_test.go index 25fff53a5fb..df4a0fd7b75 100644 --- a/pkg/services/pluginsintegration/plugintest/plugins_test.go +++ b/pkg/services/pluginsintegration/plugintest/plugins_test.go @@ -161,7 +161,7 @@ func TestIntegrationPluginManager(t *testing.T) { pr := prometheus.ProvideService(hcp) tmpo := tempo.ProvideService(hcp, tracer) td := testdatasource.ProvideService() - pg := postgres.ProvideService(cfg) + pg := postgres.ProvideService() my := mysql.ProvideService() ms := mssql.ProvideService(cfg) db := db.InitTestDB(t, sqlstore.InitTestDBOpt{Cfg: cfg}) diff --git a/pkg/tsdb/grafana-postgresql-datasource/locker.go b/pkg/tsdb/grafana-postgresql-datasource/locker.go deleted file mode 100644 index 796c37c7415..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/locker.go +++ /dev/null @@ -1,85 +0,0 @@ -package postgres - -import ( - "fmt" - "sync" -) - -// locker is a named reader/writer mutual exclusion lock. -// The lock for each particular key can be held by an arbitrary number of readers or a single writer. -type locker struct { - locks map[any]*sync.RWMutex - locksRW *sync.RWMutex -} - -func newLocker() *locker { - return &locker{ - locks: make(map[any]*sync.RWMutex), - locksRW: new(sync.RWMutex), - } -} - -// Lock locks named rw mutex with specified key for writing. -// If the lock with the same key is already locked for reading or writing, -// Lock blocks until the lock is available. -func (lkr *locker) Lock(key any) { - lk, ok := lkr.getLock(key) - if !ok { - lk = lkr.newLock(key) - } - lk.Lock() -} - -// Unlock unlocks named rw mutex with specified key for writing. It is a run-time error if rw is -// not locked for writing on entry to Unlock. -func (lkr *locker) Unlock(key any) { - lk, ok := lkr.getLock(key) - if !ok { - panic(fmt.Errorf("lock for key '%s' not initialized", key)) - } - lk.Unlock() -} - -// RLock locks named rw mutex with specified key for reading. -// -// It should not be used for recursive read locking for the same key; a blocked Lock -// call excludes new readers from acquiring the lock. See the -// documentation on the golang RWMutex type. -func (lkr *locker) RLock(key any) { - lk, ok := lkr.getLock(key) - if !ok { - lk = lkr.newLock(key) - } - lk.RLock() -} - -// RUnlock undoes a single RLock call for specified key; -// it does not affect other simultaneous readers of locker for specified key. -// It is a run-time error if locker for specified key is not locked for reading -func (lkr *locker) RUnlock(key any) { - lk, ok := lkr.getLock(key) - if !ok { - panic(fmt.Errorf("lock for key '%s' not initialized", key)) - } - lk.RUnlock() -} - -func (lkr *locker) newLock(key any) *sync.RWMutex { - lkr.locksRW.Lock() - defer lkr.locksRW.Unlock() - - if lk, ok := lkr.locks[key]; ok { - return lk - } - lk := new(sync.RWMutex) - lkr.locks[key] = lk - return lk -} - -func (lkr *locker) getLock(key any) (*sync.RWMutex, bool) { - lkr.locksRW.RLock() - defer lkr.locksRW.RUnlock() - - lock, ok := lkr.locks[key] - return lock, ok -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/locker_test.go b/pkg/tsdb/grafana-postgresql-datasource/locker_test.go deleted file mode 100644 index 9b0eca32f99..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/locker_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package postgres - -import ( - "sync" - "testing" - "time" - - "github.com/grafana/grafana/pkg/util/testutil" - "github.com/stretchr/testify/require" -) - -func TestIntegrationLocker(t *testing.T) { - testutil.SkipIntegrationTestInShortMode(t) - - const notUpdated = "not_updated" - const atThread1 = "at_thread_1" - const atThread2 = "at_thread_2" - t.Run("Should lock for same keys", func(t *testing.T) { - updated := notUpdated - locker := newLocker() - locker.Lock(1) - var wg sync.WaitGroup - wg.Add(1) - defer func() { - locker.Unlock(1) - wg.Wait() - }() - - go func() { - locker.RLock(1) - defer func() { - locker.RUnlock(1) - wg.Done() - }() - require.Equal(t, atThread1, updated, "Value should be updated in different thread") - updated = atThread2 - }() - time.Sleep(time.Millisecond * 10) - require.Equal(t, notUpdated, updated, "Value should not be updated in different thread") - updated = atThread1 - }) - - t.Run("Should not lock for different keys", func(t *testing.T) { - updated := notUpdated - locker := newLocker() - locker.Lock(1) - defer locker.Unlock(1) - var wg sync.WaitGroup - wg.Add(1) - go func() { - locker.RLock(2) - defer func() { - locker.RUnlock(2) - wg.Done() - }() - require.Equal(t, notUpdated, updated, "Value should not be updated in different thread") - updated = atThread2 - }() - wg.Wait() - require.Equal(t, atThread2, updated, "Value should be updated in different thread") - updated = atThread1 - }) -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/pgx/handler_checkhealth.go b/pkg/tsdb/grafana-postgresql-datasource/pgx/handler_checkhealth.go deleted file mode 100644 index 74bc6923eac..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/pgx/handler_checkhealth.go +++ /dev/null @@ -1,117 +0,0 @@ -package pgx - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net" - "strings" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" -) - -func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - err := e.Ping(ctx) - if err != nil { - logCheckHealthError(ctx, e.dsInfo, err) - if strings.EqualFold(req.PluginContext.User.Role, "Admin") { - return ErrToHealthCheckResult(err) - } - errResponse := &backend.CheckHealthResult{ - Status: backend.HealthStatusError, - Message: e.TransformQueryError(e.log, err).Error(), - } - return errResponse, nil - } - return &backend.CheckHealthResult{Status: backend.HealthStatusOk, Message: "Database Connection OK"}, nil -} - -// ErrToHealthCheckResult converts error into user friendly health check message -// This should be called with non nil error. If the err parameter is empty, we will send Internal Server Error -func ErrToHealthCheckResult(err error) (*backend.CheckHealthResult, error) { - if err == nil { - return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: "Internal Server Error"}, nil - } - res := &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: err.Error()} - details := map[string]string{ - "verboseMessage": err.Error(), - "errorDetailsLink": "https://grafana.com/docs/grafana/latest/datasources/postgres", - } - var opErr *net.OpError - if errors.As(err, &opErr) { - res.Message = "Network error: Failed to connect to the server" - if opErr != nil && opErr.Err != nil { - errMessage := opErr.Err.Error() - if strings.HasSuffix(opErr.Err.Error(), "no such host") { - errMessage = "no such host" - } - if strings.HasSuffix(opErr.Err.Error(), "unknown port") { - errMessage = "unknown port" - } - if strings.HasSuffix(opErr.Err.Error(), "invalid port") { - errMessage = "invalid port" - } - if strings.HasSuffix(opErr.Err.Error(), "missing port in address") { - errMessage = "missing port in address" - } - if strings.HasSuffix(opErr.Err.Error(), "invalid syntax") { - errMessage = "invalid syntax found in the address" - } - res.Message += fmt.Sprintf(". Error message: %s", errMessage) - } - } - - if errors.Is(err, sqleng.ErrParsingPostgresURL) { - res.Message = fmt.Sprintf("Connection string error: %s", sqleng.ErrParsingPostgresURL.Error()) - if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil { - details["verboseMessage"] = unwrappedErr.Error() - } - } - detailBytes, marshalErr := json.Marshal(details) - if marshalErr != nil { - return res, nil - } - res.JSONDetails = detailBytes - return res, nil -} - -func logCheckHealthError(ctx context.Context, dsInfo sqleng.DataSourceInfo, err error) { - logger := log.DefaultLogger.FromContext(ctx) - configSummary := map[string]any{ - "config_url_length": len(dsInfo.URL), - "config_user_length": len(dsInfo.User), - "config_database_length": len(dsInfo.Database), - "config_json_data_database_length": len(dsInfo.JsonData.Database), - "config_max_open_conns": dsInfo.JsonData.MaxOpenConns, - "config_max_idle_conns": dsInfo.JsonData.MaxIdleConns, - "config_conn_max_life_time": dsInfo.JsonData.ConnMaxLifetime, - "config_conn_timeout": dsInfo.JsonData.ConnectionTimeout, - "config_timescaledb": dsInfo.JsonData.Timescaledb, - "config_ssl_mode": dsInfo.JsonData.Mode, - "config_tls_configuration_method": dsInfo.JsonData.ConfigurationMethod, - "config_tls_skip_verify": dsInfo.JsonData.TlsSkipVerify, - "config_timezone": dsInfo.JsonData.Timezone, - "config_time_interval": dsInfo.JsonData.TimeInterval, - "config_enable_secure_proxy": dsInfo.JsonData.SecureDSProxy, - "config_allow_clear_text_passwords": dsInfo.JsonData.AllowCleartextPasswords, - "config_authentication_type": dsInfo.JsonData.AuthenticationType, - "config_ssl_root_cert_file_length": len(dsInfo.JsonData.RootCertFile), - "config_ssl_cert_file_length": len(dsInfo.JsonData.CertFile), - "config_ssl_key_file_length": len(dsInfo.JsonData.CertKeyFile), - "config_encrypt_length": len(dsInfo.JsonData.Encrypt), - "config_server_name_length": len(dsInfo.JsonData.Servername), - "config_password_length": len(dsInfo.DecryptedSecureJSONData["password"]), - "config_tls_ca_cert_length": len(dsInfo.DecryptedSecureJSONData["tlsCACert"]), - "config_tls_client_cert_length": len(dsInfo.DecryptedSecureJSONData["tlsClientCert"]), - "config_tls_client_key_length": len(dsInfo.DecryptedSecureJSONData["tlsClientKey"]), - } - configSummaryJSON, marshalError := json.Marshal(configSummary) - if marshalError != nil { - logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error") - return - } - logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error_detailed", "details", string(configSummaryJSON)) -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/pgx/handler_checkhealth_test.go b/pkg/tsdb/grafana-postgresql-datasource/pgx/handler_checkhealth_test.go deleted file mode 100644 index 16536368622..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/pgx/handler_checkhealth_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package pgx - -import ( - "errors" - "fmt" - "net" - "testing" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestErrToHealthCheckResult(t *testing.T) { - tests := []struct { - name string - err error - want *backend.CheckHealthResult - }{ - { - name: "without error", - want: &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: "Internal Server Error"}, - }, - { - name: "network error", - err: errors.Join(errors.New("foo"), &net.OpError{Op: "read", Net: "tcp", Err: errors.New("some op")}), - want: &backend.CheckHealthResult{ - Status: backend.HealthStatusError, - Message: "Network error: Failed to connect to the server. Error message: some op", - JSONDetails: []byte(`{"errorDetailsLink":"https://grafana.com/docs/grafana/latest/datasources/postgres","verboseMessage":"foo\nread tcp: some op"}`), - }, - }, - { - name: "regular error", - err: errors.New("internal server error"), - want: &backend.CheckHealthResult{ - Status: backend.HealthStatusError, - Message: "internal server error", - JSONDetails: []byte(`{"errorDetailsLink":"https://grafana.com/docs/grafana/latest/datasources/postgres","verboseMessage":"internal server error"}`), - }, - }, - { - name: "invalid port specifier error", - err: fmt.Errorf("%w %q: %w", sqleng.ErrParsingPostgresURL, `"foo.bar.co"`, errors.New(`strconv.Atoi: parsing "foo.bar.co": invalid syntax`)), - want: &backend.CheckHealthResult{ - Status: backend.HealthStatusError, - Message: "Connection string error: error parsing postgres url", - JSONDetails: []byte(`{"errorDetailsLink":"https://grafana.com/docs/grafana/latest/datasources/postgres","verboseMessage":"error parsing postgres url \"\\\"foo.bar.co\\\"\": strconv.Atoi: parsing \"foo.bar.co\": invalid syntax"}`), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ErrToHealthCheckResult(tt.err) - require.Nil(t, err) - assert.Equal(t, string(tt.want.JSONDetails), string(got.JSONDetails)) - require.Equal(t, tt.want, got) - }) - } -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/pgx/sql_engine.go b/pkg/tsdb/grafana-postgresql-datasource/pgx/sql_engine.go deleted file mode 100644 index 8f8af632f13..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/pgx/sql_engine.go +++ /dev/null @@ -1,836 +0,0 @@ -package pgx - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net" - "runtime/debug" - "strconv" - "strings" - "sync" - "time" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" - "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" - "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" - "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" - "github.com/jackc/pgx/v5/pgxpool" -) - -// MetaKeyExecutedQueryString is the key where the executed query should get stored -const MetaKeyExecutedQueryString = "executedQueryString" - -// SQLMacroEngine interpolates macros into sql. It takes in the Query to have access to query context and -// timeRange to be able to generate queries that use from and to. -type SQLMacroEngine interface { - Interpolate(query *backend.DataQuery, timeRange backend.TimeRange, sql string) (string, error) -} - -// SqlQueryResultTransformer transforms a query result row to RowValues with proper types. -type SqlQueryResultTransformer interface { - // TransformQueryError transforms a query error. - TransformQueryError(logger log.Logger, err error) error - GetConverterList() []sqlutil.StringConverter -} - -type JsonData struct { - MaxOpenConns int `json:"maxOpenConns"` - MaxIdleConns int `json:"maxIdleConns"` - ConnMaxLifetime int `json:"connMaxLifetime"` - ConnectionTimeout int `json:"connectionTimeout"` - Timescaledb bool `json:"timescaledb"` - Mode string `json:"sslmode"` - ConfigurationMethod string `json:"tlsConfigurationMethod"` - TlsSkipVerify bool `json:"tlsSkipVerify"` - RootCertFile string `json:"sslRootCertFile"` - CertFile string `json:"sslCertFile"` - CertKeyFile string `json:"sslKeyFile"` - Timezone string `json:"timezone"` - Encrypt string `json:"encrypt"` - Servername string `json:"servername"` - TimeInterval string `json:"timeInterval"` - Database string `json:"database"` - SecureDSProxy bool `json:"enableSecureSocksProxy"` - SecureDSProxyUsername string `json:"secureSocksProxyUsername"` - AllowCleartextPasswords bool `json:"allowCleartextPasswords"` - AuthenticationType string `json:"authenticationType"` -} - -type DataPluginConfiguration struct { - DSInfo sqleng.DataSourceInfo - TimeColumnNames []string - MetricColumnTypes []string - RowLimit int64 -} - -type DataSourceHandler struct { - macroEngine SQLMacroEngine - queryResultTransformer SqlQueryResultTransformer - timeColumnNames []string - metricColumnTypes []string - log log.Logger - dsInfo sqleng.DataSourceInfo - rowLimit int64 - userError string - pool *pgxpool.Pool -} - -type QueryJson struct { - RawSql string `json:"rawSql"` - Fill bool `json:"fill"` - FillInterval float64 `json:"fillInterval"` - FillMode string `json:"fillMode"` - FillValue float64 `json:"fillValue"` - Format string `json:"format"` -} - -func (e *DataSourceHandler) TransformQueryError(logger log.Logger, err error) error { - // OpError is the error type usually returned by functions in the net - // package. It describes the operation, network type, and address of - // an error. We log this error rather than return it to the client - // for security purposes. - var opErr *net.OpError - if errors.As(err, &opErr) { - return fmt.Errorf("failed to connect to server - %s", e.userError) - } - - return e.queryResultTransformer.TransformQueryError(logger, err) -} - -func NewQueryDataHandler(userFacingDefaultError string, p *pgxpool.Pool, config DataPluginConfiguration, queryResultTransformer SqlQueryResultTransformer, - macroEngine SQLMacroEngine, log log.Logger) (*DataSourceHandler, error) { - queryDataHandler := DataSourceHandler{ - queryResultTransformer: queryResultTransformer, - macroEngine: macroEngine, - timeColumnNames: []string{"time"}, - log: log, - dsInfo: config.DSInfo, - rowLimit: config.RowLimit, - userError: userFacingDefaultError, - } - - if len(config.TimeColumnNames) > 0 { - queryDataHandler.timeColumnNames = config.TimeColumnNames - } - - if len(config.MetricColumnTypes) > 0 { - queryDataHandler.metricColumnTypes = config.MetricColumnTypes - } - - queryDataHandler.pool = p - return &queryDataHandler, nil -} - -type DBDataResponse struct { - dataResponse backend.DataResponse - refID string -} - -func (e *DataSourceHandler) Dispose() { - e.log.Debug("Disposing DB...") - - if e.pool != nil { - e.pool.Close() - } - - e.log.Debug("DB disposed") -} - -func (e *DataSourceHandler) Ping(ctx context.Context) error { - return e.pool.Ping(ctx) -} - -func (e *DataSourceHandler) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - result := backend.NewQueryDataResponse() - ch := make(chan DBDataResponse, len(req.Queries)) - var wg sync.WaitGroup - // Execute each query in a goroutine and wait for them to finish afterwards - for _, query := range req.Queries { - queryjson := QueryJson{ - Fill: false, - Format: "time_series", - } - err := json.Unmarshal(query.JSON, &queryjson) - if err != nil { - return nil, backend.DownstreamErrorf("error unmarshal query json: %s", err.Error()) - } - - // the fill-params are only stored inside this function, during query-interpolation. we do not support - // sending them in "from the outside" - if queryjson.Fill || queryjson.FillInterval != 0.0 || queryjson.FillMode != "" || queryjson.FillValue != 0.0 { - return nil, backend.DownstreamErrorf("query fill-parameters not supported") - } - - if queryjson.RawSql == "" { - continue - } - - wg.Add(1) - go e.executeQuery(ctx, query, &wg, ch, queryjson) - } - - wg.Wait() - - // Read results from channels - close(ch) - result.Responses = make(map[string]backend.DataResponse) - for queryResult := range ch { - result.Responses[queryResult.refID] = queryResult.dataResponse - } - - return result, nil -} - -func (e *DataSourceHandler) handleQueryError(frameErr string, err error, query string, source backend.ErrorSource, ch chan DBDataResponse, queryResult DBDataResponse) { - var emptyFrame data.Frame - emptyFrame.SetMeta(&data.FrameMeta{ExecutedQueryString: query}) - if isDownstreamError(err) { - source = backend.ErrorSourceDownstream - } - queryResult.dataResponse.Error = fmt.Errorf("%s: %w", frameErr, err) - queryResult.dataResponse.ErrorSource = source - queryResult.dataResponse.Frames = data.Frames{&emptyFrame} - ch <- queryResult -} - -func (e *DataSourceHandler) handlePanic(logger log.Logger, queryResult *DBDataResponse, ch chan DBDataResponse) { - if r := recover(); r != nil { - logger.Error("ExecuteQuery panic", "error", r, "stack", string(debug.Stack())) - if theErr, ok := r.(error); ok { - queryResult.dataResponse.Error = theErr - queryResult.dataResponse.ErrorSource = backend.ErrorSourcePlugin - } else if theErrString, ok := r.(string); ok { - queryResult.dataResponse.Error = errors.New(theErrString) - queryResult.dataResponse.ErrorSource = backend.ErrorSourcePlugin - } else { - queryResult.dataResponse.Error = fmt.Errorf("unexpected error - %s", e.userError) - queryResult.dataResponse.ErrorSource = backend.ErrorSourceDownstream - } - ch <- *queryResult - } -} - -// Interpolate provides global macros/substitutions for all sql datasources. -var Interpolate = func(query backend.DataQuery, timeRange backend.TimeRange, timeInterval string, sql string) string { - interval := query.Interval - - sql = strings.ReplaceAll(sql, "$__interval_ms", strconv.FormatInt(interval.Milliseconds(), 10)) - sql = strings.ReplaceAll(sql, "$__interval", gtime.FormatInterval(interval)) - sql = strings.ReplaceAll(sql, "$__unixEpochFrom()", fmt.Sprintf("%d", timeRange.From.UTC().Unix())) - sql = strings.ReplaceAll(sql, "$__unixEpochTo()", fmt.Sprintf("%d", timeRange.To.UTC().Unix())) - - return sql -} - -func (e *DataSourceHandler) execQuery(ctx context.Context, query string) ([]*pgconn.Result, error) { - c, err := e.pool.Acquire(ctx) - if err != nil { - return nil, backend.DownstreamErrorf("failed to acquire connection: %w", err) - } - defer c.Release() - - mrr := c.Conn().PgConn().Exec(ctx, query) - // Close returns the first error that occurred during the MultiResultReader's use. We will log that later. - defer mrr.Close() //nolint:errcheck - return mrr.ReadAll() -} - -func (e *DataSourceHandler) executeQuery(queryContext context.Context, query backend.DataQuery, wg *sync.WaitGroup, - ch chan DBDataResponse, queryJSON QueryJson) { - defer wg.Done() - queryResult := DBDataResponse{ - dataResponse: backend.DataResponse{}, - refID: query.RefID, - } - - logger := e.log.FromContext(queryContext) - defer e.handlePanic(logger, &queryResult, ch) - - if queryJSON.RawSql == "" { - panic("Query model property rawSql should not be empty at this point") - } - - // global substitutions - interpolatedQuery := Interpolate(query, query.TimeRange, e.dsInfo.JsonData.TimeInterval, queryJSON.RawSql) - - // data source specific substitutions - interpolatedQuery, err := e.macroEngine.Interpolate(&query, query.TimeRange, interpolatedQuery) - if err != nil { - e.handleQueryError("interpolation failed", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) - return - } - - results, err := e.execQuery(queryContext, interpolatedQuery) - if err != nil { - e.handleQueryError("db query error", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) - return - } - - qm, err := e.newProcessCfg(queryContext, query, results, interpolatedQuery) - if err != nil { - e.handleQueryError("failed to get configurations", err, interpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) - return - } - - frame, err := convertResultsToFrame(results, e.rowLimit) - if err != nil { - e.handleQueryError("convert frame from rows error", err, interpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) - return - } - - e.processFrame(frame, qm, queryResult, ch, logger) -} - -// dataQueryFormat is the type of query. -type dataQueryFormat string - -const ( - // dataQueryFormatTable identifies a table query (default). - dataQueryFormatTable dataQueryFormat = "table" - // dataQueryFormatSeries identifies a time series query. - dataQueryFormatSeries dataQueryFormat = "time_series" -) - -type dataQueryModel struct { - InterpolatedQuery string // property not set until after Interpolate() - Format dataQueryFormat - TimeRange backend.TimeRange - FillMissing *data.FillMissing // property not set until after Interpolate() - Interval time.Duration - columnNames []string - columnTypes []string - timeIndex int - timeEndIndex int - metricIndex int - metricPrefix bool - queryContext context.Context -} - -func convertSQLTimeColumnsToEpochMS(frame *data.Frame, qm *dataQueryModel) error { - if qm.timeIndex != -1 { - if err := convertSQLTimeColumnToEpochMS(frame, qm.timeIndex); err != nil { - return fmt.Errorf("%v: %w", "failed to convert time column", err) - } - } - - if qm.timeEndIndex != -1 { - if err := convertSQLTimeColumnToEpochMS(frame, qm.timeEndIndex); err != nil { - return fmt.Errorf("%v: %w", "failed to convert timeend column", err) - } - } - - return nil -} - -func (e *DataSourceHandler) processFrame(frame *data.Frame, qm *dataQueryModel, queryResult DBDataResponse, ch chan DBDataResponse, logger log.Logger) { - if frame.Meta == nil { - frame.Meta = &data.FrameMeta{} - } - frame.Meta.ExecutedQueryString = qm.InterpolatedQuery - - // If no rows were returned, clear any previously set `Fields` with a single empty `data.Field` slice. - // Then assign `queryResult.dataResponse.Frames` the current single frame with that single empty Field. - // This assures 1) our visualization doesn't display unwanted empty fields, and also that 2) - // additionally-needed frame data stays intact and is correctly passed to our visulization. - if frame.Rows() == 0 { - frame.Fields = []*data.Field{} - queryResult.dataResponse.Frames = data.Frames{frame} - ch <- queryResult - return - } - - if err := convertSQLTimeColumnsToEpochMS(frame, qm); err != nil { - e.handleQueryError("converting time columns failed", err, qm.InterpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) - return - } - - if qm.Format == dataQueryFormatSeries { - // time series has to have time column - if qm.timeIndex == -1 { - e.handleQueryError("db has no time column", errors.New("time column is missing; make sure your data includes a time column for time series format or switch to a table format that doesn't require it"), qm.InterpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) - return - } - - // Make sure to name the time field 'Time' to be backward compatible with Grafana pre-v8. - frame.Fields[qm.timeIndex].Name = data.TimeSeriesTimeFieldName - - for i := range qm.columnNames { - if i == qm.timeIndex || i == qm.metricIndex { - continue - } - - if t := frame.Fields[i].Type(); t == data.FieldTypeString || t == data.FieldTypeNullableString { - continue - } - - var err error - if frame, err = convertSQLValueColumnToFloat(frame, i); err != nil { - e.handleQueryError("convert value to float failed", err, qm.InterpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) - return - } - } - - tsSchema := frame.TimeSeriesSchema() - if tsSchema.Type == data.TimeSeriesTypeLong { - var err error - originalData := frame - frame, err = data.LongToWide(frame, qm.FillMissing) - if err != nil { - e.handleQueryError("failed to convert long to wide series when converting from dataframe", err, qm.InterpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) - return - } - - // Before 8x, a special metric column was used to name time series. The LongToWide transforms that into a metric label on the value field. - // But that makes series name have both the value column name AND the metric name. So here we are removing the metric label here and moving it to the - // field name to get the same naming for the series as pre v8 - if len(originalData.Fields) == 3 { - for _, field := range frame.Fields { - if len(field.Labels) == 1 { // 7x only supported one label - name, ok := field.Labels["metric"] - if ok { - field.Name = name - field.Labels = nil - } - } - } - } - } - if qm.FillMissing != nil { - // we align the start-time - startUnixTime := qm.TimeRange.From.Unix() / int64(qm.Interval.Seconds()) * int64(qm.Interval.Seconds()) - alignedTimeRange := backend.TimeRange{ - From: time.Unix(startUnixTime, 0), - To: qm.TimeRange.To, - } - - var err error - frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) - if err != nil { - logger.Error("Failed to resample dataframe", "err", err) - frame.AppendNotices(data.Notice{Text: "Failed to resample dataframe", Severity: data.NoticeSeverityWarning}) - return - } - } - } - - queryResult.dataResponse.Frames = data.Frames{frame} - ch <- queryResult -} - -func (e *DataSourceHandler) newProcessCfg(queryContext context.Context, query backend.DataQuery, - results []*pgconn.Result, interpolatedQuery string) (*dataQueryModel, error) { - columnNames := []string{} - columnTypes := []string{} - - // The results will contain column information in the metadata - for _, result := range results { - // Get column names from the result metadata - for _, field := range result.FieldDescriptions { - columnNames = append(columnNames, field.Name) - pqtype, ok := pgtype.NewMap().TypeForOID(field.DataTypeOID) - if !ok { - // Handle special cases for field types - switch field.DataTypeOID { - case pgtype.TimetzOID: - columnTypes = append(columnTypes, "timetz") - case 790: - columnTypes = append(columnTypes, "money") - default: - columnTypes = append(columnTypes, "unknown") - } - } else { - columnTypes = append(columnTypes, pqtype.Name) - } - } - } - - qm := &dataQueryModel{ - columnTypes: columnTypes, - columnNames: columnNames, - timeIndex: -1, - timeEndIndex: -1, - metricIndex: -1, - metricPrefix: false, - queryContext: queryContext, - } - - queryJSON := QueryJson{} - err := json.Unmarshal(query.JSON, &queryJSON) - if err != nil { - return nil, err - } - - if queryJSON.Fill { - qm.FillMissing = &data.FillMissing{} - qm.Interval = time.Duration(queryJSON.FillInterval * float64(time.Second)) - switch strings.ToLower(queryJSON.FillMode) { - case "null": - qm.FillMissing.Mode = data.FillModeNull - case "previous": - qm.FillMissing.Mode = data.FillModePrevious - case "value": - qm.FillMissing.Mode = data.FillModeValue - qm.FillMissing.Value = queryJSON.FillValue - default: - } - } - - qm.TimeRange.From = query.TimeRange.From.UTC() - qm.TimeRange.To = query.TimeRange.To.UTC() - - // Default to time_series if no format is provided - switch queryJSON.Format { - case "table": - qm.Format = dataQueryFormatTable - case "time_series": - fallthrough - default: - qm.Format = dataQueryFormatSeries - } - - for i, col := range qm.columnNames { - for _, tc := range e.timeColumnNames { - if col == tc { - qm.timeIndex = i - break - } - } - - if qm.Format == dataQueryFormatTable && strings.EqualFold(col, "timeend") { - qm.timeEndIndex = i - continue - } - - switch col { - case "metric": - qm.metricIndex = i - default: - if qm.metricIndex == -1 { - columnType := qm.columnTypes[i] - for _, mct := range e.metricColumnTypes { - if columnType == mct { - qm.metricIndex = i - continue - } - } - } - } - } - qm.InterpolatedQuery = interpolatedQuery - return qm, nil -} - -func convertResultsToFrame(results []*pgconn.Result, rowLimit int64) (*data.Frame, error) { - m := pgtype.NewMap() - - // Find the first SELECT result to establish the frame structure - var firstSelectResult *pgconn.Result - for _, result := range results { - if result.CommandTag.Select() { - firstSelectResult = result - break - } - } - - // If no SELECT results found, return empty frame - if firstSelectResult == nil { - return data.NewFrame(""), nil - } - - // Create frame structure based on the first SELECT result - fields := make(data.Fields, len(firstSelectResult.FieldDescriptions)) - fieldTypes, err := getFieldTypesFromDescriptions(firstSelectResult.FieldDescriptions, m) - if err != nil { - return nil, err - } - - for i, v := range firstSelectResult.FieldDescriptions { - fields[i] = data.NewFieldFromFieldType(fieldTypes[i], 0) - fields[i].Name = v.Name - } - frame := *data.NewFrame("", fields...) - - // Process all SELECT results, but validate column compatibility - for _, result := range results { - // Skip non-select statements - if !result.CommandTag.Select() { - continue - } - - // Validate that this result has the same structure as the frame - if len(result.FieldDescriptions) != len(frame.Fields) { - return nil, fmt.Errorf("incompatible result structure: expected %d columns, got %d columns", - len(frame.Fields), len(result.FieldDescriptions)) - } - - // Validate column names and types match - for i, fd := range result.FieldDescriptions { - if fd.Name != frame.Fields[i].Name { - return nil, fmt.Errorf("column name mismatch at position %d: expected %q, got %q", - i, frame.Fields[i].Name, fd.Name) - } - } - - fieldDescriptions := result.FieldDescriptions - for rowIdx := range result.Rows { - if rowIdx == int(rowLimit) { - frame.AppendNotices(data.Notice{ - Severity: data.NoticeSeverityWarning, - Text: fmt.Sprintf("Results have been limited to %v because the SQL row limit was reached", rowLimit), - }) - break - } - row := make([]any, len(fieldDescriptions)) - for colIdx, fd := range fieldDescriptions { - rawValue := result.Rows[rowIdx][colIdx] - - if rawValue == nil { - row[colIdx] = nil - continue - } - - convertedValue, err := convertPostgresValue(rawValue, fd, m) - if err != nil { - return nil, err - } - row[colIdx] = convertedValue - } - - // Validate row length matches frame field count before appending - if len(row) != len(frame.Fields) { - return nil, fmt.Errorf("row data length mismatch: expected %d values, got %d values", - len(frame.Fields), len(row)) - } - - frame.AppendRow(row...) - } - } - - return &frame, nil -} - -// convertPostgresValue converts a raw PostgreSQL value to the appropriate Go type -func convertPostgresValue(rawValue []byte, fd pgconn.FieldDescription, m *pgtype.Map) (interface{}, error) { - dataTypeOID := fd.DataTypeOID - format := fd.Format - - // Convert based on type - switch fd.DataTypeOID { - case pgtype.Int2OID: - var d *int16 - scanPlan := m.PlanScan(dataTypeOID, format, &d) - err := scanPlan.Scan(rawValue, &d) - if err != nil { - return nil, err - } - return d, nil - case pgtype.Int4OID: - var d *int32 - scanPlan := m.PlanScan(dataTypeOID, format, &d) - err := scanPlan.Scan(rawValue, &d) - if err != nil { - return nil, err - } - return d, nil - case pgtype.Int8OID: - var d *int64 - scanPlan := m.PlanScan(dataTypeOID, format, &d) - err := scanPlan.Scan(rawValue, &d) - if err != nil { - return nil, err - } - return d, nil - case pgtype.NumericOID, pgtype.Float8OID, pgtype.Float4OID: - var d *float64 - scanPlan := m.PlanScan(dataTypeOID, format, &d) - err := scanPlan.Scan(rawValue, &d) - if err != nil { - return nil, err - } - return d, nil - case pgtype.BoolOID: - var d *bool - scanPlan := m.PlanScan(dataTypeOID, format, &d) - err := scanPlan.Scan(rawValue, &d) - if err != nil { - return nil, err - } - return d, nil - case pgtype.ByteaOID: - d, err := pgtype.ByteaCodec.DecodeValue(pgtype.ByteaCodec{}, m, dataTypeOID, format, rawValue) - if err != nil { - return nil, err - } - str := string(d.([]byte)) - return &str, nil - case pgtype.TimestampOID, pgtype.TimestamptzOID, pgtype.DateOID: - var d *time.Time - scanPlan := m.PlanScan(dataTypeOID, format, &d) - err := scanPlan.Scan(rawValue, &d) - if err != nil { - return nil, err - } - return d, nil - case pgtype.TimeOID, pgtype.TimetzOID: - var d *string - scanPlan := m.PlanScan(dataTypeOID, format, &d) - err := scanPlan.Scan(rawValue, &d) - if err != nil { - return nil, err - } - return d, nil - case pgtype.JSONOID, pgtype.JSONBOID: - var d *string - scanPlan := m.PlanScan(dataTypeOID, format, &d) - err := scanPlan.Scan(rawValue, &d) - if err != nil { - return nil, err - } - // Handle null JSON values - if d == nil { - return nil, nil - } - j := json.RawMessage(*d) - return &j, nil - default: - var d *string - scanPlan := m.PlanScan(dataTypeOID, format, &d) - err := scanPlan.Scan(rawValue, &d) - if err != nil { - return nil, err - } - return d, nil - } -} - -func getFieldTypesFromDescriptions(fieldDescriptions []pgconn.FieldDescription, m *pgtype.Map) ([]data.FieldType, error) { - fieldTypes := make([]data.FieldType, len(fieldDescriptions)) - for i, v := range fieldDescriptions { - typeName, ok := m.TypeForOID(v.DataTypeOID) - if !ok { - fieldTypes[i] = data.FieldTypeNullableString - } else { - switch typeName.Name { - case "int2": - fieldTypes[i] = data.FieldTypeNullableInt16 - case "int4": - fieldTypes[i] = data.FieldTypeNullableInt32 - case "int8": - fieldTypes[i] = data.FieldTypeNullableInt64 - case "float4", "float8", "numeric": - fieldTypes[i] = data.FieldTypeNullableFloat64 - case "bool": - fieldTypes[i] = data.FieldTypeNullableBool - case "timestamptz", "timestamp", "date": - fieldTypes[i] = data.FieldTypeNullableTime - case "json", "jsonb": - fieldTypes[i] = data.FieldTypeNullableJSON - default: - fieldTypes[i] = data.FieldTypeNullableString - } - } - } - return fieldTypes, nil -} - -// convertSQLTimeColumnToEpochMS converts column named time to unix timestamp in milliseconds -// to make native datetime types and epoch dates work in annotation and table queries. -func convertSQLTimeColumnToEpochMS(frame *data.Frame, timeIndex int) error { - if timeIndex < 0 || timeIndex >= len(frame.Fields) { - return fmt.Errorf("timeIndex %d is out of range", timeIndex) - } - - origin := frame.Fields[timeIndex] - valueType := origin.Type() - if valueType == data.FieldTypeTime || valueType == data.FieldTypeNullableTime { - return nil - } - - newField := data.NewFieldFromFieldType(data.FieldTypeNullableTime, 0) - newField.Name = origin.Name - newField.Labels = origin.Labels - - valueLength := origin.Len() - for i := 0; i < valueLength; i++ { - v, err := origin.NullableFloatAt(i) - if err != nil { - return fmt.Errorf("unable to convert data to a time field") - } - if v == nil { - newField.Append(nil) - } else { - timestamp := time.Unix(0, int64(epochPrecisionToMS(*v))*int64(time.Millisecond)) - newField.Append(×tamp) - } - } - frame.Fields[timeIndex] = newField - - return nil -} - -// convertSQLValueColumnToFloat converts timeseries value column to float. -func convertSQLValueColumnToFloat(frame *data.Frame, Index int) (*data.Frame, error) { - if Index < 0 || Index >= len(frame.Fields) { - return frame, fmt.Errorf("metricIndex %d is out of range", Index) - } - - origin := frame.Fields[Index] - valueType := origin.Type() - if valueType == data.FieldTypeFloat64 || valueType == data.FieldTypeNullableFloat64 { - return frame, nil - } - - newField := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, origin.Len()) - newField.Name = origin.Name - newField.Labels = origin.Labels - - for i := 0; i < origin.Len(); i++ { - v, err := origin.NullableFloatAt(i) - if err != nil { - return frame, err - } - newField.Set(i, v) - } - - frame.Fields[Index] = newField - - return frame, nil -} - -// epochPrecisionToMS converts epoch precision to millisecond, if needed. -// Only seconds to milliseconds supported right now -func epochPrecisionToMS(value float64) float64 { - s := strconv.FormatFloat(value, 'e', -1, 64) - if strings.HasSuffix(s, "e+09") { - return value * float64(1e3) - } - - if strings.HasSuffix(s, "e+18") { - return value / float64(time.Millisecond) - } - - return value -} - -func isDownstreamError(err error) bool { - if backend.IsDownstreamError(err) { - return true - } - resultProcessingDownstreamErrors := []error{ - data.ErrorInputFieldsWithoutRows, - data.ErrorSeriesUnsorted, - data.ErrorNullTimeValues, - } - for _, e := range resultProcessingDownstreamErrors { - if errors.Is(err, e) { - return true - } - } - return false -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/pgx/sql_engine_test.go b/pkg/tsdb/grafana-postgresql-datasource/pgx/sql_engine_test.go deleted file mode 100644 index 2f161badb7d..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/pgx/sql_engine_test.go +++ /dev/null @@ -1,681 +0,0 @@ -package pgx - -import ( - "fmt" - "net" - "testing" - "time" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" - "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana-plugin-sdk-go/backend/log" -) - -func Pointer[T any](v T) *T { return &v } - -func TestSQLEngine(t *testing.T) { - dt := time.Date(2018, 3, 14, 21, 20, 6, int(527345*time.Microsecond), time.UTC) - - t.Run("Handle interpolating $__interval and $__interval_ms", func(t *testing.T) { - from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC) - to := from.Add(5 * time.Minute) - timeRange := backend.TimeRange{From: from, To: to} - - text := "$__interval $__timeGroupAlias(time,$__interval) $__interval_ms" - - t.Run("interpolate 10 minutes $__interval", func(t *testing.T) { - query := backend.DataQuery{JSON: []byte("{}"), MaxDataPoints: 1500, Interval: time.Minute * 10} - sql := Interpolate(query, timeRange, "", text) - require.Equal(t, "10m $__timeGroupAlias(time,10m) 600000", sql) - }) - - t.Run("interpolate 4seconds $__interval", func(t *testing.T) { - query := backend.DataQuery{JSON: []byte("{}"), MaxDataPoints: 1500, Interval: time.Second * 4} - sql := Interpolate(query, timeRange, "", text) - require.Equal(t, "4s $__timeGroupAlias(time,4s) 4000", sql) - }) - - t.Run("interpolate 200 milliseconds $__interval", func(t *testing.T) { - query := backend.DataQuery{JSON: []byte("{}"), MaxDataPoints: 1500, Interval: time.Millisecond * 200} - sql := Interpolate(query, timeRange, "", text) - require.Equal(t, "200ms $__timeGroupAlias(time,200ms) 200", sql) - }) - }) - - t.Run("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func(t *testing.T) { - from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC) - to := from.Add(5 * time.Minute) - timeRange := backend.TimeRange{From: from, To: to} - query := backend.DataQuery{JSON: []byte("{}"), MaxDataPoints: 1500, Interval: time.Second * 60} - - t.Run("interpolate __unixEpochFrom function", func(t *testing.T) { - sql := Interpolate(query, timeRange, "", "select $__unixEpochFrom()") - require.Equal(t, fmt.Sprintf("select %d", from.Unix()), sql) - }) - - t.Run("interpolate __unixEpochTo function", func(t *testing.T) { - sql := Interpolate(query, timeRange, "", "select $__unixEpochTo()") - require.Equal(t, fmt.Sprintf("select %d", to.Unix()), sql) - }) - }) - - t.Run("Given row values with int64 as time columns", func(t *testing.T) { - tSeconds := dt.Unix() - tMilliseconds := dt.UnixNano() / 1e6 - tNanoSeconds := dt.UnixNano() - var nilPointer *int64 - - originFrame := data.NewFrame("", - data.NewField("time1", nil, []int64{ - tSeconds, - }), - data.NewField("time2", nil, []*int64{ - Pointer(tSeconds), - }), - data.NewField("time3", nil, []int64{ - tMilliseconds, - }), - data.NewField("time4", nil, []*int64{ - Pointer(tMilliseconds), - }), - data.NewField("time5", nil, []int64{ - tNanoSeconds, - }), - data.NewField("time6", nil, []*int64{ - Pointer(tNanoSeconds), - }), - data.NewField("time7", nil, []*int64{ - nilPointer, - }), - ) - - for i := 0; i < len(originFrame.Fields); i++ { - err := convertSQLTimeColumnToEpochMS(originFrame, i) - require.NoError(t, err) - } - - require.Equal(t, dt.Unix(), (*originFrame.Fields[0].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[1].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[2].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[3].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[4].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[5].At(0).(*time.Time)).Unix()) - require.Nil(t, originFrame.Fields[6].At(0)) - }) - - t.Run("Given row values with uint64 as time columns", func(t *testing.T) { - tSeconds := uint64(dt.Unix()) - tMilliseconds := uint64(dt.UnixNano() / 1e6) - tNanoSeconds := uint64(dt.UnixNano()) - var nilPointer *uint64 - - originFrame := data.NewFrame("", - data.NewField("time1", nil, []uint64{ - tSeconds, - }), - data.NewField("time2", nil, []*uint64{ - Pointer(tSeconds), - }), - data.NewField("time3", nil, []uint64{ - tMilliseconds, - }), - data.NewField("time4", nil, []*uint64{ - Pointer(tMilliseconds), - }), - data.NewField("time5", nil, []uint64{ - tNanoSeconds, - }), - data.NewField("time6", nil, []*uint64{ - Pointer(tNanoSeconds), - }), - data.NewField("time7", nil, []*uint64{ - nilPointer, - }), - ) - - for i := 0; i < len(originFrame.Fields); i++ { - err := convertSQLTimeColumnToEpochMS(originFrame, i) - require.NoError(t, err) - } - - require.Equal(t, dt.Unix(), (*originFrame.Fields[0].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[1].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[2].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[3].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[4].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[5].At(0).(*time.Time)).Unix()) - require.Nil(t, originFrame.Fields[6].At(0)) - }) - - t.Run("Given row values with int32 as time columns", func(t *testing.T) { - tSeconds := int32(dt.Unix()) - var nilInt *int32 - - originFrame := data.NewFrame("", - data.NewField("time1", nil, []int32{ - tSeconds, - }), - data.NewField("time2", nil, []*int32{ - Pointer(tSeconds), - }), - data.NewField("time7", nil, []*int32{ - nilInt, - }), - ) - for i := 0; i < 3; i++ { - err := convertSQLTimeColumnToEpochMS(originFrame, i) - require.NoError(t, err) - } - - require.Equal(t, dt.Unix(), (*originFrame.Fields[0].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[1].At(0).(*time.Time)).Unix()) - require.Nil(t, originFrame.Fields[2].At(0)) - }) - - t.Run("Given row values with uint32 as time columns", func(t *testing.T) { - tSeconds := uint32(dt.Unix()) - var nilInt *uint32 - - originFrame := data.NewFrame("", - data.NewField("time1", nil, []uint32{ - tSeconds, - }), - data.NewField("time2", nil, []*uint32{ - Pointer(tSeconds), - }), - data.NewField("time7", nil, []*uint32{ - nilInt, - }), - ) - for i := 0; i < len(originFrame.Fields); i++ { - err := convertSQLTimeColumnToEpochMS(originFrame, i) - require.NoError(t, err) - } - require.Equal(t, dt.Unix(), (*originFrame.Fields[0].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[1].At(0).(*time.Time)).Unix()) - require.Nil(t, originFrame.Fields[2].At(0)) - }) - - t.Run("Given row values with float64 as time columns", func(t *testing.T) { - tSeconds := float64(dt.UnixNano()) / float64(time.Second) - tMilliseconds := float64(dt.UnixNano()) / float64(time.Millisecond) - tNanoSeconds := float64(dt.UnixNano()) - var nilPointer *float64 - - originFrame := data.NewFrame("", - data.NewField("time1", nil, []float64{ - tSeconds, - }), - data.NewField("time2", nil, []*float64{ - Pointer(tSeconds), - }), - data.NewField("time3", nil, []float64{ - tMilliseconds, - }), - data.NewField("time4", nil, []*float64{ - Pointer(tMilliseconds), - }), - data.NewField("time5", nil, []float64{ - tNanoSeconds, - }), - data.NewField("time6", nil, []*float64{ - Pointer(tNanoSeconds), - }), - data.NewField("time7", nil, []*float64{ - nilPointer, - }), - ) - - for i := 0; i < len(originFrame.Fields); i++ { - err := convertSQLTimeColumnToEpochMS(originFrame, i) - require.NoError(t, err) - } - - require.Equal(t, dt.Unix(), (*originFrame.Fields[0].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[1].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[2].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[3].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[4].At(0).(*time.Time)).Unix()) - require.Equal(t, dt.Unix(), (*originFrame.Fields[5].At(0).(*time.Time)).Unix()) - require.Nil(t, originFrame.Fields[6].At(0)) - }) - - t.Run("Given row values with float32 as time columns", func(t *testing.T) { - tSeconds := float32(dt.Unix()) - var nilInt *float32 - - originFrame := data.NewFrame("", - data.NewField("time1", nil, []float32{ - tSeconds, - }), - data.NewField("time2", nil, []*float32{ - Pointer(tSeconds), - }), - data.NewField("time7", nil, []*float32{ - nilInt, - }), - ) - for i := 0; i < len(originFrame.Fields); i++ { - err := convertSQLTimeColumnToEpochMS(originFrame, i) - require.NoError(t, err) - } - require.Equal(t, int64(tSeconds), (*originFrame.Fields[0].At(0).(*time.Time)).Unix()) - require.Equal(t, int64(tSeconds), (*originFrame.Fields[1].At(0).(*time.Time)).Unix()) - require.Nil(t, originFrame.Fields[2].At(0)) - }) - - t.Run("Given row with value columns, would be converted to float64", func(t *testing.T) { - originFrame := data.NewFrame("", - data.NewField("value1", nil, []int64{ - int64(1), - }), - data.NewField("value2", nil, []*int64{ - Pointer(int64(1)), - }), - data.NewField("value3", nil, []int32{ - int32(1), - }), - data.NewField("value4", nil, []*int32{ - Pointer(int32(1)), - }), - data.NewField("value5", nil, []int16{ - int16(1), - }), - data.NewField("value6", nil, []*int16{ - Pointer(int16(1)), - }), - data.NewField("value7", nil, []int8{ - int8(1), - }), - data.NewField("value8", nil, []*int8{ - Pointer(int8(1)), - }), - data.NewField("value9", nil, []float64{ - float64(1), - }), - data.NewField("value10", nil, []*float64{ - Pointer(1.0), - }), - data.NewField("value11", nil, []float32{ - float32(1), - }), - data.NewField("value12", nil, []*float32{ - Pointer(float32(1)), - }), - data.NewField("value13", nil, []uint64{ - uint64(1), - }), - data.NewField("value14", nil, []*uint64{ - Pointer(uint64(1)), - }), - data.NewField("value15", nil, []uint32{ - uint32(1), - }), - data.NewField("value16", nil, []*uint32{ - Pointer(uint32(1)), - }), - data.NewField("value17", nil, []uint16{ - uint16(1), - }), - data.NewField("value18", nil, []*uint16{ - Pointer(uint16(1)), - }), - data.NewField("value19", nil, []uint8{ - uint8(1), - }), - data.NewField("value20", nil, []*uint8{ - Pointer(uint8(1)), - }), - ) - for i := 0; i < len(originFrame.Fields); i++ { - _, err := convertSQLValueColumnToFloat(originFrame, i) - require.NoError(t, err) - if i == 8 { - require.Equal(t, float64(1), originFrame.Fields[i].At(0).(float64)) - } else { - require.NotNil(t, originFrame.Fields[i].At(0).(*float64)) - require.Equal(t, float64(1), *originFrame.Fields[i].At(0).(*float64)) - } - } - }) - - t.Run("Given row with nil value columns", func(t *testing.T) { - var int64NilPointer *int64 - var int32NilPointer *int32 - var int16NilPointer *int16 - var int8NilPointer *int8 - var float64NilPointer *float64 - var float32NilPointer *float32 - var uint64NilPointer *uint64 - var uint32NilPointer *uint32 - var uint16NilPointer *uint16 - var uint8NilPointer *uint8 - - originFrame := data.NewFrame("", - data.NewField("value1", nil, []*int64{ - int64NilPointer, - }), - data.NewField("value2", nil, []*int32{ - int32NilPointer, - }), - data.NewField("value3", nil, []*int16{ - int16NilPointer, - }), - data.NewField("value4", nil, []*int8{ - int8NilPointer, - }), - data.NewField("value5", nil, []*float64{ - float64NilPointer, - }), - data.NewField("value6", nil, []*float32{ - float32NilPointer, - }), - data.NewField("value7", nil, []*uint64{ - uint64NilPointer, - }), - data.NewField("value8", nil, []*uint32{ - uint32NilPointer, - }), - data.NewField("value9", nil, []*uint16{ - uint16NilPointer, - }), - data.NewField("value10", nil, []*uint8{ - uint8NilPointer, - }), - ) - for i := 0; i < len(originFrame.Fields); i++ { - t.Run("", func(t *testing.T) { - _, err := convertSQLValueColumnToFloat(originFrame, i) - require.NoError(t, err) - require.Nil(t, originFrame.Fields[i].At(0)) - }) - } - }) - - t.Run("Should not return raw connection errors", func(t *testing.T) { - err := net.OpError{Op: "Dial", Err: fmt.Errorf("inner-error")} - transformer := &testQueryResultTransformer{} - dp := DataSourceHandler{ - log: backend.NewLoggerWith("logger", "test"), - queryResultTransformer: transformer, - } - resultErr := dp.TransformQueryError(dp.log, &err) - assert.False(t, transformer.transformQueryErrorWasCalled) - errorText := resultErr.Error() - assert.NotEqual(t, err, resultErr) - assert.NotContains(t, errorText, "inner-error") - assert.Contains(t, errorText, "failed to connect to server") - }) - - t.Run("Should return non-connection errors unmodified", func(t *testing.T) { - err := fmt.Errorf("normal error") - transformer := &testQueryResultTransformer{} - dp := DataSourceHandler{ - log: backend.NewLoggerWith("logger", "test"), - queryResultTransformer: transformer, - } - resultErr := dp.TransformQueryError(dp.log, err) - assert.True(t, transformer.transformQueryErrorWasCalled) - assert.Equal(t, err, resultErr) - assert.ErrorIs(t, err, resultErr) - }) -} - -func TestConvertResultsToFrame(t *testing.T) { - // Import the pgx packages needed for testing - // These imports are included in the main file but need to be accessible for tests - t.Run("convertResultsToFrame with single result", func(t *testing.T) { - // Create mock field descriptions - fieldDescs := []pgconn.FieldDescription{ - {Name: "id", DataTypeOID: pgtype.Int4OID}, - {Name: "name", DataTypeOID: pgtype.TextOID}, - {Name: "value", DataTypeOID: pgtype.Float8OID}, - } - - // Create mock result data - mockRows := [][][]byte{ - {[]byte("1"), []byte("test1"), []byte("10.5")}, - {[]byte("2"), []byte("test2"), []byte("20.7")}, - } - - // Create mock result - result := &pgconn.Result{ - FieldDescriptions: fieldDescs, - Rows: mockRows, - } - result.CommandTag = pgconn.NewCommandTag("SELECT 2") - - results := []*pgconn.Result{result} - - frame, err := convertResultsToFrame(results, 1000) - require.NoError(t, err) - require.NotNil(t, frame) - require.Equal(t, 3, len(frame.Fields)) - require.Equal(t, 2, frame.Rows()) - - // Verify field names - require.Equal(t, "id", frame.Fields[0].Name) - require.Equal(t, "name", frame.Fields[1].Name) - require.Equal(t, "value", frame.Fields[2].Name) - }) - - t.Run("convertResultsToFrame with multiple compatible results", func(t *testing.T) { - // Create mock field descriptions (same structure for both results) - fieldDescs := []pgconn.FieldDescription{ - {Name: "id", DataTypeOID: pgtype.Int4OID}, - {Name: "name", DataTypeOID: pgtype.TextOID}, - } - - // Create first result - mockRows1 := [][][]byte{ - {[]byte("1"), []byte("test1")}, - {[]byte("2"), []byte("test2")}, - } - result1 := &pgconn.Result{ - FieldDescriptions: fieldDescs, - Rows: mockRows1, - } - result1.CommandTag = pgconn.NewCommandTag("SELECT 2") - - // Create second result with same structure - mockRows2 := [][][]byte{ - {[]byte("3"), []byte("test3")}, - {[]byte("4"), []byte("test4")}, - } - result2 := &pgconn.Result{ - FieldDescriptions: fieldDescs, - Rows: mockRows2, - } - result2.CommandTag = pgconn.NewCommandTag("SELECT 2") - - results := []*pgconn.Result{result1, result2} - - frame, err := convertResultsToFrame(results, 1000) - require.NoError(t, err) - require.NotNil(t, frame) - require.Equal(t, 2, len(frame.Fields)) - require.Equal(t, 4, frame.Rows()) // Should have rows from both results - - // Verify field names - require.Equal(t, "id", frame.Fields[0].Name) - require.Equal(t, "name", frame.Fields[1].Name) - }) - - t.Run("convertResultsToFrame with row limit", func(t *testing.T) { - // Create mock field descriptions - fieldDescs := []pgconn.FieldDescription{ - {Name: "id", DataTypeOID: pgtype.Int4OID}, - } - - // Create mock result data with 3 rows - mockRows := [][][]byte{ - {[]byte("1")}, - {[]byte("2")}, - {[]byte("3")}, - } - - result := &pgconn.Result{ - FieldDescriptions: fieldDescs, - Rows: mockRows, - } - result.CommandTag = pgconn.NewCommandTag("SELECT 3") - - results := []*pgconn.Result{result} - - // Set row limit to 2 - frame, err := convertResultsToFrame(results, 2) - require.NoError(t, err) - require.NotNil(t, frame) - require.Equal(t, 1, len(frame.Fields)) - require.Equal(t, 2, frame.Rows()) // Should be limited to 2 rows - - // Should have a notice about the limit - require.NotNil(t, frame.Meta) - require.Len(t, frame.Meta.Notices, 1) - require.Contains(t, frame.Meta.Notices[0].Text, "Results have been limited to 2") - }) - - t.Run("convertResultsToFrame with mixed SELECT and non-SELECT results", func(t *testing.T) { - // Create a non-SELECT result (should be skipped) - nonSelectResult := &pgconn.Result{} - nonSelectResult.CommandTag = pgconn.NewCommandTag("UPDATE 1") - - // Create a SELECT result - fieldDescs := []pgconn.FieldDescription{ - {Name: "id", DataTypeOID: pgtype.Int4OID}, - } - mockRows := [][][]byte{ - {[]byte("1")}, - } - selectResult := &pgconn.Result{ - FieldDescriptions: fieldDescs, - Rows: mockRows, - } - selectResult.CommandTag = pgconn.NewCommandTag("SELECT 1") - - results := []*pgconn.Result{nonSelectResult, selectResult} - - frame, err := convertResultsToFrame(results, 1000) - require.NoError(t, err) - require.NotNil(t, frame) - require.Equal(t, 1, len(frame.Fields)) - require.Equal(t, 1, frame.Rows()) - }) - - t.Run("convertResultsToFrame with no SELECT results", func(t *testing.T) { - // Create only non-SELECT results - result1 := &pgconn.Result{} - result1.CommandTag = pgconn.NewCommandTag("UPDATE 1") - - result2 := &pgconn.Result{} - result2.CommandTag = pgconn.NewCommandTag("INSERT 1") - - results := []*pgconn.Result{result1, result2} - - frame, err := convertResultsToFrame(results, 1000) - require.NoError(t, err) - require.NotNil(t, frame) - require.Equal(t, 0, len(frame.Fields)) - require.Equal(t, 0, frame.Rows()) - }) - - t.Run("convertResultsToFrame with multiple results and row limit per result", func(t *testing.T) { - // Create mock field descriptions (same structure for both results) - fieldDescs := []pgconn.FieldDescription{ - {Name: "id", DataTypeOID: pgtype.Int4OID}, - } - - // Create first result with 3 rows - mockRows1 := [][][]byte{ - {[]byte("1")}, - {[]byte("2")}, - {[]byte("3")}, - } - result1 := &pgconn.Result{ - FieldDescriptions: fieldDescs, - Rows: mockRows1, - } - result1.CommandTag = pgconn.NewCommandTag("SELECT 3") - - // Create second result with 3 rows - mockRows2 := [][][]byte{ - {[]byte("4")}, - {[]byte("5")}, - {[]byte("6")}, - } - result2 := &pgconn.Result{ - FieldDescriptions: fieldDescs, - Rows: mockRows2, - } - result2.CommandTag = pgconn.NewCommandTag("SELECT 3") - - results := []*pgconn.Result{result1, result2} - - // Set row limit to 2 (should limit each result to 2 rows) - frame, err := convertResultsToFrame(results, 2) - require.NoError(t, err) - require.NotNil(t, frame) - require.Equal(t, 1, len(frame.Fields)) - require.Equal(t, 4, frame.Rows()) // 2 rows from each result - - // Should have notices about the limit from both results - require.NotNil(t, frame.Meta) - require.Len(t, frame.Meta.Notices, 2) - require.Contains(t, frame.Meta.Notices[0].Text, "Results have been limited to 2") - require.Contains(t, frame.Meta.Notices[1].Text, "Results have been limited to 2") - }) - - t.Run("convertResultsToFrame handles null values correctly", func(t *testing.T) { - // Create mock field descriptions - fieldDescs := []pgconn.FieldDescription{ - {Name: "id", DataTypeOID: pgtype.Int4OID}, - {Name: "name", DataTypeOID: pgtype.TextOID}, - } - - // Create mock result data with null values - mockRows := [][][]byte{ - {[]byte("1"), nil}, // null name - {nil, []byte("test2")}, // null id - } - - result := &pgconn.Result{ - FieldDescriptions: fieldDescs, - Rows: mockRows, - } - result.CommandTag = pgconn.NewCommandTag("SELECT 2") - - results := []*pgconn.Result{result} - - frame, err := convertResultsToFrame(results, 1000) - require.NoError(t, err) - require.NotNil(t, frame) - require.Equal(t, 2, len(frame.Fields)) - require.Equal(t, 2, frame.Rows()) - - // Check that null values are handled correctly - // The exact representation depends on the field type, but should not panic - require.NotPanics(t, func() { - frame.Fields[0].At(1) // null id - frame.Fields[1].At(0) // null name - }) - }) -} - -type testQueryResultTransformer struct { - transformQueryErrorWasCalled bool -} - -func (t *testQueryResultTransformer) TransformQueryError(_ log.Logger, err error) error { - t.transformQueryErrorWasCalled = true - return err -} - -func (t *testQueryResultTransformer) GetConverterList() []sqlutil.StringConverter { - return nil -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager.go b/pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager.go deleted file mode 100644 index 89b43263aec..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager.go +++ /dev/null @@ -1,151 +0,0 @@ -package postgres - -import ( - "fmt" - "os" - - "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" -) - -var validateCertFuncPgx = validateCertFilePathsPgx - -type pgxTlsManager struct { - logger log.Logger -} - -func newPgxTlsManager(logger log.Logger) *pgxTlsManager { - return &pgxTlsManager{ - logger: logger, - } -} - -// getTLSSettings retrieves TLS settings and handles certificate file creation if needed. -func (m *pgxTlsManager) getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) { - tlsConfig := tlsSettings{ - Mode: dsInfo.JsonData.Mode, - } - - if tlsConfig.Mode == "disable" { - m.logger.Debug("Postgres TLS/SSL is disabled") - return tlsConfig, nil - } - - tlsConfig.ConfigurationMethod = dsInfo.JsonData.ConfigurationMethod - tlsConfig.RootCertFile = dsInfo.JsonData.RootCertFile - tlsConfig.CertFile = dsInfo.JsonData.CertFile - tlsConfig.CertKeyFile = dsInfo.JsonData.CertKeyFile - - if tlsConfig.ConfigurationMethod == "file-content" { - if err := m.createCertFiles(dsInfo, &tlsConfig); err != nil { - return tlsConfig, fmt.Errorf("failed to create TLS certificate files: %w", err) - } - } else { - if err := validateCertFuncPgx(tlsConfig.RootCertFile, tlsConfig.CertFile, tlsConfig.CertKeyFile); err != nil { - return tlsConfig, fmt.Errorf("invalid TLS certificate file paths: %w", err) - } - } - - return tlsConfig, nil -} - -// createCertFiles writes certificate files to temporary locations. -func (m *pgxTlsManager) createCertFiles(dsInfo sqleng.DataSourceInfo, tlsConfig *tlsSettings) error { - m.logger.Debug("Writing TLS certificate files to temporary locations") - - var err error - if tlsConfig.RootCertFile, err = m.writeCertFile("root-*.crt", dsInfo.DecryptedSecureJSONData["tlsCACert"]); err != nil { - return err - } - if tlsConfig.CertFile, err = m.writeCertFile("client-*.crt", dsInfo.DecryptedSecureJSONData["tlsClientCert"]); err != nil { - return err - } - if tlsConfig.CertKeyFile, err = m.writeCertFile("client-*.key", dsInfo.DecryptedSecureJSONData["tlsClientKey"]); err != nil { - return err - } - - return nil -} - -// writeCertFile writes a single certificate file to a temporary location. -func (m *pgxTlsManager) writeCertFile(pattern, content string) (string, error) { - if content == "" { - return "", nil - } - - m.logger.Debug("Writing certificate file", "pattern", pattern) - file, err := os.CreateTemp("", pattern) - if err != nil { - return "", fmt.Errorf("failed to create temporary file: %w", err) - } - defer func() { - if err := file.Close(); err != nil { - m.logger.Error("Failed to close file", "error", err) - } - }() - - if _, err := file.WriteString(content); err != nil { - return "", fmt.Errorf("failed to write to temporary file: %w", err) - } - - return file.Name(), nil -} - -// cleanupCertFiles removes temporary certificate files. -func (m *pgxTlsManager) cleanupCertFiles(tlsConfig tlsSettings) { - // Only clean up if the configuration method is "file-content" - if tlsConfig.ConfigurationMethod != "file-content" { - m.logger.Debug("Skipping cleanup of TLS certificate files") - return - } - m.logger.Debug("Cleaning up TLS certificate files") - - files := []struct { - path string - name string - }{ - {tlsConfig.RootCertFile, "root certificate"}, - {tlsConfig.CertFile, "client certificate"}, - {tlsConfig.CertKeyFile, "client key"}, - } - - for _, file := range files { - if file.path == "" { - continue - } - if err := os.Remove(file.path); err != nil { - m.logger.Error("Failed to remove file", "type", file.name, "path", file.path, "error", err) - } else { - m.logger.Debug("Successfully removed file", "type", file.name, "path", file.path) - } - } -} - -// validateCertFilePaths validates the existence of configured certificate file paths. -func validateCertFilePathsPgx(rootCert, clientCert, clientKey string) error { - for _, path := range []string{rootCert, clientCert, clientKey} { - if path == "" { - continue - } - exists, err := fileExistsPgx(path) - if err != nil { - return fmt.Errorf("error checking file existence: %w", err) - } - if !exists { - return sqleng.ErrCertFileNotExist - } - } - return nil -} - -// fileExists checks if a file exists at the given path. -func fileExistsPgx(path string) (bool, error) { - _, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - return true, nil -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager_test.go b/pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager_test.go deleted file mode 100644 index 4717469348f..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/pgxtlsmanager_test.go +++ /dev/null @@ -1,245 +0,0 @@ -package postgres - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// Test getTLSSettings. -func TestPgxGetTLSSettings(t *testing.T) { - mockValidateCertFilePathsPgx() - t.Cleanup(resetValidateCertFilePathsPgx) - - updatedTime := time.Now() - - testCases := []struct { - desc string - expErr string - jsonData sqleng.JsonData - secureJSONData map[string]string - uid string - tlsSettings tlsSettings - updated time.Time - }{ - { - desc: "Custom TLS authentication disabled", - updated: updatedTime, - jsonData: sqleng.JsonData{ - Mode: "disable", - RootCertFile: "i/am/coding/ca.crt", - CertFile: "i/am/coding/client.crt", - CertKeyFile: "i/am/coding/client.key", - ConfigurationMethod: "file-path", - }, - tlsSettings: tlsSettings{Mode: "disable"}, - }, - { - desc: "Custom TLS authentication with file path", - updated: updatedTime.Add(time.Minute), - jsonData: sqleng.JsonData{ - Mode: "verify-full", - ConfigurationMethod: "file-path", - RootCertFile: "i/am/coding/ca.crt", - CertFile: "i/am/coding/client.crt", - CertKeyFile: "i/am/coding/client.key", - }, - tlsSettings: tlsSettings{ - Mode: "verify-full", - ConfigurationMethod: "file-path", - RootCertFile: "i/am/coding/ca.crt", - CertFile: "i/am/coding/client.crt", - CertKeyFile: "i/am/coding/client.key", - }, - }, - } - for _, tt := range testCases { - t.Run(tt.desc, func(t *testing.T) { - var settings tlsSettings - var err error - mng := pgxTlsManager{ - logger: backend.NewLoggerWith("logger", "tsdb.postgres"), - } - - ds := sqleng.DataSourceInfo{ - JsonData: tt.jsonData, - DecryptedSecureJSONData: tt.secureJSONData, - UID: tt.uid, - Updated: tt.updated, - } - - settings, err = mng.getTLSSettings(ds) - - if tt.expErr == "" { - require.NoError(t, err, tt.desc) - assert.Equal(t, tt.tlsSettings, settings) - } else { - require.Error(t, err, tt.desc) - assert.True(t, strings.HasPrefix(err.Error(), tt.expErr), - fmt.Sprintf("%s: %q doesn't start with %q", tt.desc, err, tt.expErr)) - } - }) - } -} - -func mockValidateCertFilePathsPgx() { - validateCertFuncPgx = func(rootCert, clientCert, clientKey string) error { - return nil - } -} - -func resetValidateCertFilePathsPgx() { - validateCertFuncPgx = validateCertFilePathsPgx -} - -func TestTLSManager_GetTLSSettings(t *testing.T) { - logger := log.New() - tlsManager := newPgxTlsManager(logger) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "require", - ConfigurationMethod: "file-content", - }, - DecryptedSecureJSONData: map[string]string{ - "tlsCACert": "root-cert-content", - "tlsClientCert": "client-cert-content", - "tlsClientKey": "client-key-content", - }, - } - - tlsConfig, err := tlsManager.getTLSSettings(dsInfo) - require.NoError(t, err) - assert.Equal(t, "require", tlsConfig.Mode) - assert.NotEmpty(t, tlsConfig.RootCertFile) - assert.NotEmpty(t, tlsConfig.CertFile) - assert.NotEmpty(t, tlsConfig.CertKeyFile) - - // Cleanup temporary files - tlsManager.cleanupCertFiles(tlsConfig) - assert.NoFileExists(t, tlsConfig.RootCertFile) - assert.NoFileExists(t, tlsConfig.CertFile) - assert.NoFileExists(t, tlsConfig.CertKeyFile) -} - -func TestTLSManager_CleanupCertFiles_FilePath(t *testing.T) { - logger := log.New() - tlsManager := newPgxTlsManager(logger) - - // Create temporary files for testing - rootCertFile, err := tlsManager.writeCertFile("root-*.crt", "root-cert-content") - require.NoError(t, err) - clientCertFile, err := tlsManager.writeCertFile("client-*.crt", "client-cert-content") - require.NoError(t, err) - clientKeyFile, err := tlsManager.writeCertFile("client-*.key", "client-key-content") - require.NoError(t, err) - - // Simulate a configuration where the method is "file-path" - tlsConfig := tlsSettings{ - ConfigurationMethod: "file-path", - RootCertFile: rootCertFile, - CertFile: clientCertFile, - CertKeyFile: clientKeyFile, - } - - // Call cleanupCertFiles - tlsManager.cleanupCertFiles(tlsConfig) - - // Verify the files are NOT deleted - assert.FileExists(t, rootCertFile, "Root certificate file should not be deleted") - assert.FileExists(t, clientCertFile, "Client certificate file should not be deleted") - assert.FileExists(t, clientKeyFile, "Client key file should not be deleted") - - // Cleanup the files manually - err = os.Remove(rootCertFile) - require.NoError(t, err) - err = os.Remove(clientCertFile) - require.NoError(t, err) - err = os.Remove(clientKeyFile) - require.NoError(t, err) -} - -func TestTLSManager_CreateCertFiles(t *testing.T) { - logger := log.New() - tlsManager := newPgxTlsManager(logger) - - dsInfo := sqleng.DataSourceInfo{ - DecryptedSecureJSONData: map[string]string{ - "tlsCACert": "root-cert-content", - "tlsClientCert": "client-cert-content", - "tlsClientKey": "client-key-content", - }, - } - - tlsConfig := tlsSettings{ - ConfigurationMethod: "file-content", - } - err := tlsManager.createCertFiles(dsInfo, &tlsConfig) - require.NoError(t, err) - - assert.FileExists(t, tlsConfig.RootCertFile) - assert.FileExists(t, tlsConfig.CertFile) - assert.FileExists(t, tlsConfig.CertKeyFile) - - // Cleanup temporary files - tlsManager.cleanupCertFiles(tlsConfig) - assert.NoFileExists(t, tlsConfig.RootCertFile) - assert.NoFileExists(t, tlsConfig.CertFile) - assert.NoFileExists(t, tlsConfig.CertKeyFile) -} - -func TestTLSManager_WriteCertFile(t *testing.T) { - logger := log.New() - tlsManager := newPgxTlsManager(logger) - - // Test writing a valid certificate file - filePath, err := tlsManager.writeCertFile("test-*.crt", "test-cert-content") - require.NoError(t, err) - assert.FileExists(t, filePath) - - content, err := os.ReadFile(filepath.Clean(filePath)) - require.NoError(t, err) - assert.Equal(t, "test-cert-content", string(content)) - - // Cleanup the file - err = os.Remove(filePath) - require.NoError(t, err) - assert.NoFileExists(t, filePath) -} - -func TestTLSManager_CleanupCertFiles(t *testing.T) { - logger := log.New() - tlsManager := newPgxTlsManager(logger) - - // Create temporary files for testing - rootCertFile, err := tlsManager.writeCertFile("root-*.crt", "root-cert-content") - require.NoError(t, err) - clientCertFile, err := tlsManager.writeCertFile("client-*.crt", "client-cert-content") - require.NoError(t, err) - clientKeyFile, err := tlsManager.writeCertFile("client-*.key", "client-key-content") - require.NoError(t, err) - - tlsConfig := tlsSettings{ - ConfigurationMethod: "file-content", - RootCertFile: rootCertFile, - CertFile: clientCertFile, - CertKeyFile: clientKeyFile, - } - - // Cleanup the files - tlsManager.cleanupCertFiles(tlsConfig) - - // Verify the files are deleted - assert.NoFileExists(t, rootCertFile) - assert.NoFileExists(t, clientCertFile) - assert.NoFileExists(t, clientKeyFile) -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres.go b/pkg/tsdb/grafana-postgresql-datasource/postgres.go index 379741a18e9..6c7ea9552fb 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres.go @@ -2,7 +2,6 @@ package postgres import ( "context" - "database/sql" "encoding/json" "fmt" "reflect" @@ -16,63 +15,12 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" "github.com/jackc/pgx/v5/pgxpool" - "github.com/lib/pq" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - sqlengpgx "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/pgx" "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" ) -func newPostgres(ctx context.Context, userFacingDefaultError string, rowLimit int64, dsInfo sqleng.DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*sql.DB, *sqleng.DataSourceHandler, error) { - connector, err := pq.NewConnector(cnnstr) - if err != nil { - logger.Error("postgres connector creation failed", "error", err) - return nil, nil, fmt.Errorf("postgres connector creation failed") - } - - proxyClient, err := settings.ProxyClient(ctx) - if err != nil { - logger.Error("postgres proxy creation failed", "error", err) - return nil, nil, fmt.Errorf("postgres proxy creation failed") - } - - if proxyClient.SecureSocksProxyEnabled() { - dialer, err := proxyClient.NewSecureSocksProxyContextDialer() - if err != nil { - logger.Error("postgres proxy creation failed", "error", err) - return nil, nil, fmt.Errorf("postgres proxy creation failed") - } - postgresDialer := newPostgresProxyDialer(dialer) - // update the postgres dialer with the proxy dialer - connector.Dialer(postgresDialer) - } - - config := sqleng.DataPluginConfiguration{ - DSInfo: dsInfo, - MetricColumnTypes: []string{"UNKNOWN", "TEXT", "VARCHAR", "CHAR"}, - RowLimit: rowLimit, - } - - queryResultTransformer := postgresQueryResultTransformer{} - - db := sql.OpenDB(connector) - - db.SetMaxOpenConns(config.DSInfo.JsonData.MaxOpenConns) - db.SetMaxIdleConns(config.DSInfo.JsonData.MaxIdleConns) - db.SetConnMaxLifetime(time.Duration(config.DSInfo.JsonData.ConnMaxLifetime) * time.Second) - - handler, err := sqleng.NewQueryDataHandler(userFacingDefaultError, db, config, &queryResultTransformer, newPostgresMacroEngine(dsInfo.JsonData.Timescaledb), - logger) - if err != nil { - logger.Error("Failed connecting to Postgres", "err", err) - return nil, nil, err - } - - logger.Debug("Successfully connected to Postgres") - return db, handler, nil -} - -func newPostgresPGX(ctx context.Context, userFacingDefaultError string, rowLimit int64, dsInfo sqleng.DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*pgxpool.Pool, *sqlengpgx.DataSourceHandler, error) { +func newPostgres(ctx context.Context, userFacingDefaultError string, rowLimit int64, dsInfo sqleng.DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*pgxpool.Pool, *sqleng.DataSourceHandler, error) { pgxConf, err := pgxpool.ParseConfig(cnnstr) if err != nil { logger.Error("postgres config creation failed", "error", err) @@ -92,7 +40,7 @@ func newPostgresPGX(ctx context.Context, userFacingDefaultError string, rowLimit return nil, nil, fmt.Errorf("postgres proxy creation failed") } - pgxConf.ConnConfig.DialFunc = newPgxDialFunc(dialer) + pgxConf.ConnConfig.DialFunc = newDialFunc(dialer) } // by default pgx resolves hostnames to ip addresses. we must avoid this. @@ -101,7 +49,7 @@ func newPostgresPGX(ctx context.Context, userFacingDefaultError string, rowLimit return []string{host}, nil } - config := sqlengpgx.DataPluginConfiguration{ + config := sqleng.DataPluginConfiguration{ DSInfo: dsInfo, MetricColumnTypes: []string{"unknown", "text", "varchar", "char", "bpchar"}, RowLimit: rowLimit, @@ -117,7 +65,7 @@ func newPostgresPGX(ctx context.Context, userFacingDefaultError string, rowLimit return nil, nil, err } - handler, err := sqlengpgx.NewQueryDataHandler(userFacingDefaultError, p, config, &queryResultTransformer, newPostgresMacroEngine(dsInfo.JsonData.Timescaledb), + handler, err := sqleng.NewQueryDataHandler(userFacingDefaultError, p, config, &queryResultTransformer, newPostgresMacroEngine(dsInfo.JsonData.Timescaledb), logger) if err != nil { logger.Error("Failed connecting to Postgres", "err", err) @@ -128,7 +76,7 @@ func newPostgresPGX(ctx context.Context, userFacingDefaultError string, rowLimit return p, handler, nil } -func NewInstanceSettings(logger log.Logger, dataPath string) datasource.InstanceFactoryFunc { +func NewInstanceSettings(logger log.Logger) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { cfg := backend.GrafanaConfigFromContext(ctx) sqlCfg, err := cfg.SQL() @@ -171,48 +119,26 @@ func NewInstanceSettings(logger log.Logger, dataPath string) datasource.Instance return nil, err } - usePGX := cfg.FeatureToggles().IsEnabled("postgresDSUsePGX") - - if usePGX { - pgxlogger := logger.FromContext(ctx).With("driver", "pgx") - pgxTlsManager := newPgxTlsManager(pgxlogger) - pgxTlsSettings, err := pgxTlsManager.getTLSSettings(dsInfo) - if err != nil { - return "", err - } - - // Ensure cleanupCertFiles is called after the connection is opened - defer pgxTlsManager.cleanupCertFiles(pgxTlsSettings) - cnnstr, err := generateConnectionString(dsInfo, pgxTlsSettings, usePGX, pgxlogger) - if err != nil { - return "", err - } - _, handler, err := newPostgresPGX(ctx, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, cnnstr, pgxlogger, settings) - if err != nil { - pgxlogger.Error("Failed connecting to Postgres", "err", err) - return nil, err - } - pgxlogger.Debug("Successfully connected to Postgres") - return handler, nil - } else { - pqlogger := logger.FromContext(ctx).With("driver", "libpq") - tlsManager := newTLSManager(pqlogger, dataPath) - tlsSettings, err := tlsManager.getTLSSettings(dsInfo) - if err != nil { - return "", err - } - cnnstr, err := generateConnectionString(dsInfo, tlsSettings, usePGX, pqlogger) - if err != nil { - return nil, err - } - _, handler, err := newPostgres(ctx, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, cnnstr, pqlogger, settings) - if err != nil { - pqlogger.Error("Failed connecting to Postgres", "err", err) - return nil, err - } - pqlogger.Debug("Successfully connected to Postgres") - return handler, nil + pgxlogger := logger.FromContext(ctx).With("driver", "pgx") + tlsManager := newTLSManager(pgxlogger) + tlsSettings, err := tlsManager.getTLSSettings(dsInfo) + if err != nil { + return "", err } + + // Ensure cleanupCertFiles is called after the connection is opened + defer tlsManager.cleanupCertFiles(tlsSettings) + cnnstr, err := generateConnectionString(dsInfo, tlsSettings, pgxlogger) + if err != nil { + return "", err + } + _, handler, err := newPostgres(ctx, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, cnnstr, pgxlogger, settings) + if err != nil { + pgxlogger.Error("Failed connecting to Postgres", "err", err) + return nil, err + } + pgxlogger.Debug("Successfully connected to Postgres") + return handler, nil } } @@ -302,7 +228,7 @@ func buildBaseConnectionString(params connectionParams) string { return connStr } -func generateConnectionString(dsInfo sqleng.DataSourceInfo, tlsSettings tlsSettings, isPGX bool, logger log.Logger) (string, error) { +func generateConnectionString(dsInfo sqleng.DataSourceInfo, tlsSettings tlsSettings, logger log.Logger) (string, error) { params, err := parseConnectionParams(dsInfo, logger) if err != nil { return "", err @@ -312,15 +238,6 @@ func generateConnectionString(dsInfo sqleng.DataSourceInfo, tlsSettings tlsSetti connStr += fmt.Sprintf(" sslmode='%s'", escape(tlsSettings.Mode)) - // there is an issue with the lib/pq module, the `verify-ca` tls mode - // does not work correctly. ( see https://github.com/lib/pq/issues/1106 ) - // to workaround the problem, if the `verify-ca` mode is chosen, - // we disable sslsni. - if tlsSettings.Mode == "verify-ca" && !isPGX { - logger.Debug("Disabling sslsni for verify-ca mode") - connStr += " sslsni=0" - } - // Attach root certificate if provided if tlsSettings.RootCertFile != "" { logger.Debug("Setting server root certificate", "tlsRootCert", tlsSettings.RootCertFile) diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_snapshot_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_snapshot_test.go deleted file mode 100644 index e5a5decf3ae..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_snapshot_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package postgres - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - "testing" - "time" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana-plugin-sdk-go/experimental" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" - "github.com/grafana/grafana/pkg/util/testutil" -) - -// These tests require a real postgres database: -// - make devenv sources=postgres_tests -// - either set the env variable GRAFANA_TEST_DB = postgres -// - or set `forceRun := true` below -// -// The tests require a PostgreSQL db named grafanadstest and a user/password grafanatest/grafanatest! -// Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a -// preconfigured Postgres server suitable for running these tests. -func TestIntegrationPostgresPGXSnapshots(t *testing.T) { - testutil.SkipIntegrationTestInShortMode(t) - - // the logic in this function is copied from postgres_tests.go - shouldRunTest := func() bool { - testDbName, present := os.LookupEnv("GRAFANA_TEST_DB") - - if present && testDbName == "postgres" { - return true - } - - return false - } - - if !shouldRunTest() { - t.Skip() - } - - getCnnStr := func() string { - host := os.Getenv("POSTGRES_HOST") - if host == "" { - host = "localhost" - } - port := os.Getenv("POSTGRES_PORT") - if port == "" { - port = "5432" - } - - return fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanadstest sslmode=disable", - host, port) - } - - sqlQueryCommentRe := regexp.MustCompile(`^-- (.+)\n`) - - readSqlFile := func(path string) (string, string) { - // the file-path is not coming from the outside, - // it is hardcoded in this file. - //nolint:gosec - sqlBytes, err := os.ReadFile(path) - require.NoError(t, err) - - sql := string(sqlBytes) - - // first line of the file contains the sql query to run, commented out - match := sqlQueryCommentRe.FindStringSubmatch(sql) - require.Len(t, match, 2) - - rawSQL := strings.TrimSpace(match[1]) - - return rawSQL, sql - } - - makeQuery := func(rawSQL string, format string) backend.QueryDataRequest { - queryData := map[string]string{ - "rawSql": rawSQL, - "format": format, - } - - queryBytes, err := json.Marshal(queryData) - require.NoError(t, err) - - return backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: queryBytes, - RefID: "A", - TimeRange: backend.TimeRange{ - From: time.Date(2023, 12, 24, 14, 15, 22, 123456, time.UTC), - To: time.Date(2023, 12, 24, 14, 45, 13, 876543, time.UTC), - }, - }, - }, - } - } - - tt := []struct { - name string - format string - }{ - {format: "time_series", name: "simple"}, - {format: "time_series", name: "no_rows_long"}, - {format: "time_series", name: "no_rows_wide"}, - {format: "time_series", name: "7x_compat_metric_label"}, - {format: "time_series", name: "convert_to_float64"}, - {format: "time_series", name: "convert_to_float64_not"}, - {format: "time_series", name: "fill_null"}, - {format: "time_series", name: "fill_previous"}, - {format: "time_series", name: "fill_value"}, - {format: "time_series", name: "fill_value_wide"}, - {format: "table", name: "simple"}, - {format: "table", name: "multi_stat1"}, - {format: "table", name: "multi_stat2"}, - {format: "table", name: "no_rows"}, - {format: "table", name: "types_numeric"}, - {format: "table", name: "types_char"}, - {format: "table", name: "types_datetime_pgx"}, - {format: "table", name: "types_other"}, - {format: "table", name: "types_enum"}, - {format: "table", name: "types_jsonb"}, - {format: "table", name: "timestamp_convert_bigint"}, - {format: "table", name: "timestamp_convert_integer"}, - {format: "table", name: "timestamp_convert_real"}, - {format: "table", name: "timestamp_convert_double"}, - {format: "table", name: "time_group_compat_case1"}, - {format: "table", name: "time_group_compat_case2"}, - } - - for _, test := range tt { - require.True(t, test.format == "table" || test.format == "time_series") - t.Run(test.name, func(t *testing.T) { - origInterpolate := sqleng.Interpolate - t.Cleanup(func() { - sqleng.Interpolate = origInterpolate - }) - - sqleng.Interpolate = func(query backend.DataQuery, timeRange backend.TimeRange, timeInterval string, sql string) string { - return sql - } - - jsonData := sqleng.JsonData{ - MaxOpenConns: 10, - MaxIdleConns: 2, - ConnMaxLifetime: 14400, - Timescaledb: false, - Mode: "disable", - ConfigurationMethod: "file-path", - } - - dsInfo := sqleng.DataSourceInfo{ - JsonData: jsonData, - DecryptedSecureJSONData: map[string]string{}, - } - - logger := log.New() - - cnnstr := getCnnStr() - - p, handler, err := newPostgresPGX(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) - - t.Cleanup((func() { - _, err := p.Exec(context.Background(), "DROP TABLE tbl") - require.NoError(t, err) - p.Close() - })) - - require.NoError(t, err) - - sqlFilePath := filepath.Join("testdata", test.format, test.name+".sql") - goldenFileName := filepath.Join(test.format, test.name+".golden") - - rawSQL, sql := readSqlFile(sqlFilePath) - - _, err = p.Exec(context.Background(), sql) - require.NoError(t, err) - - query := makeQuery(rawSQL, test.format) - - result, err := handler.QueryData(context.Background(), &query) - require.Len(t, result.Responses, 1) - response, found := result.Responses["A"] - require.True(t, found) - require.NoError(t, err) - experimental.CheckGoldenJSONResponse(t, "testdata", goldenFileName, &response, updateGoldenFiles) - }) - } -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_test.go deleted file mode 100644 index 7c66bde7580..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_test.go +++ /dev/null @@ -1,1579 +0,0 @@ -package postgres - -import ( - "encoding/json" - "fmt" - "math/rand" - "os" - "strings" - "testing" - "time" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" - - "github.com/grafana/grafana/pkg/util/testutil" - _ "github.com/lib/pq" -) - -// Test generateConnectionString. -func TestIntegrationGenerateConnectionStringPGX(t *testing.T) { - testutil.SkipIntegrationTestInShortMode(t) - - testCases := []struct { - desc string - host string - user string - password string - database string - tlsSettings tlsSettings - expConnStr string - expErr string - uid string - }{ - { - desc: "Unix socket host", - host: "/var/run/postgresql", - user: "user", - password: "password", - database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' host='/var/run/postgresql' dbname='database' password='password' sslmode='verify-full'", - }, - { - desc: "TCP host", - host: "host", - user: "user", - password: "password", - database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' host='host' dbname='database' password='password' sslmode='verify-full'", - }, - { - desc: "verify-ca automatically adds disable-sni", - host: "host:1234", - user: "user", - password: "password", - database: "database", - tlsSettings: tlsSettings{Mode: "verify-ca"}, - expConnStr: "user='user' host='host' dbname='database' password='password' port=1234 sslmode='verify-ca' sslsni=0", - }, - { - desc: "TCP/port host", - host: "host:1234", - user: "user", - password: "password", - database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' host='host' dbname='database' password='password' port=1234 sslmode='verify-full'", - }, - { - desc: "Ipv6 host", - host: "[::1]", - user: "user", - password: "password", - database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' host='::1' dbname='database' password='password' sslmode='verify-full'", - }, - { - desc: "Ipv6/port host", - host: "[::1]:1234", - user: "user", - password: "password", - database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' host='::1' dbname='database' password='password' port=1234 sslmode='verify-full'", - }, - { - desc: "Invalid port", - host: "host:invalid", - user: "user", - database: "database", - tlsSettings: tlsSettings{}, - expErr: "error parsing postgres url", - }, - { - desc: "Password with single quote and backslash", - host: "host", - user: "user", - password: `p'\assword`, - database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: `user='user' host='host' dbname='database' password='p\'\\assword' sslmode='verify-full'`, - }, - { - desc: "User/DB with single quote and backslash", - host: "host", - user: `u'\ser`, - password: `password`, - database: `d'\atabase`, - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: `user='u\'\\ser' host='host' dbname='d\'\\atabase' password='password' sslmode='verify-full'`, - }, - { - desc: "Custom TLS mode disabled", - host: "host", - user: "user", - password: "password", - database: "database", - tlsSettings: tlsSettings{Mode: "disable"}, - expConnStr: "user='user' host='host' dbname='database' password='password' sslmode='disable'", - }, - { - desc: "Custom TLS mode verify-full with certificate files", - host: "host", - user: "user", - password: "password", - database: "database", - tlsSettings: tlsSettings{ - Mode: "verify-full", - RootCertFile: "i/am/coding/ca.crt", - CertFile: "i/am/coding/client.crt", - CertKeyFile: "i/am/coding/client.key", - }, - expConnStr: "user='user' host='host' dbname='database' password='password' sslmode='verify-full' " + - "sslrootcert='i/am/coding/ca.crt' sslcert='i/am/coding/client.crt' sslkey='i/am/coding/client.key'", - }, - { - desc: "No password", - host: "host", - user: "user", - password: "", - database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' host='host' dbname='database' sslmode='verify-full'", - }, - } - for _, tt := range testCases { - t.Run(tt.desc, func(t *testing.T) { - ds := sqleng.DataSourceInfo{ - URL: tt.host, - User: tt.user, - DecryptedSecureJSONData: map[string]string{"password": tt.password}, - Database: tt.database, - UID: tt.uid, - } - logger := backend.NewLoggerWith("logger", "tsdb.postgres") - - connStr, err := generateConnectionString(ds, tt.tlsSettings, false, logger) - - if tt.expErr == "" { - require.NoError(t, err, tt.desc) - assert.Equal(t, tt.expConnStr, connStr) - } else { - require.Error(t, err, tt.desc) - assert.True(t, strings.HasPrefix(err.Error(), tt.expErr), - fmt.Sprintf("%s: %q doesn't start with %q", tt.desc, err, tt.expErr)) - } - }) - } -} - -// To run this test, set runPostgresTests=true -// Or from the commandline: GRAFANA_TEST_DB=postgres go test -tags=integration -v ./pkg/tsdb/grafana-postgresql-datasource -// The tests require a PostgreSQL db named grafanadstest and a user/password grafanatest/grafanatest! -// Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a -// preconfigured Postgres server suitable for running these tests. -// There is also a datasource and dashboard provisioned by devenv scripts that you can -// use to verify that the generated data are visualized as expected, see -// devenv/README.md for setup instructions. -func TestIntegrationPostgresPGX(t *testing.T) { - testutil.SkipIntegrationTestInShortMode(t) - - // change to true to run the PostgreSQL tests - const runPostgresTests = false - - if !isTestDbPostgres() && !runPostgresTests { - t.Skip() - } - - origInterpolate := sqleng.Interpolate - t.Cleanup(func() { - sqleng.Interpolate = origInterpolate - }) - sqleng.Interpolate = func(query backend.DataQuery, timeRange backend.TimeRange, timeInterval string, sql string) string { - return sql - } - - jsonData := sqleng.JsonData{ - MaxOpenConns: 10, - MaxIdleConns: 2, - ConnMaxLifetime: 14400, - Timescaledb: false, - Mode: "disable", - ConfigurationMethod: "file-path", - } - - dsInfo := sqleng.DataSourceInfo{ - JsonData: jsonData, - DecryptedSecureJSONData: map[string]string{}, - } - - logger := backend.NewLoggerWith("logger", "postgres.test") - - cnnstr := postgresTestDBConnString() - - p, exe, err := newPostgresPGX(t.Context(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) - - require.NoError(t, err) - - fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) - - t.Run("Given a table with different native data types", func(t *testing.T) { - sql := ` - DROP TABLE IF EXISTS postgres_types; - CREATE TABLE postgres_types( - c00_smallint smallint, - c01_integer integer, - c02_bigint bigint, - - c03_real real, - c04_double double precision, - c05_decimal decimal(10,2), - c06_numeric numeric(10,2), - - c07_char char(10), - c08_varchar varchar(10), - c09_text text, - - c10_timestamp timestamp without time zone, - c11_timestamptz timestamp with time zone, - c12_date date, - c13_time time without time zone, - c14_timetz time with time zone, - time date, - c15_interval interval, - c16_smallint smallint, - - c17_json json, - c18_jsonb jsonb - ); - ` - _, err := p.Exec(t.Context(), sql) - require.NoError(t, err) - - sql = ` - INSERT INTO postgres_types VALUES( - 1,2,3, - 4.5,6.7,1.1,1.2, - 'char10','varchar10','text', - now(),now(),now(),now(),now(),now(),'15m'::interval,null, - '{"key1": "value1"}'::json, '{"key2": "value2"}'::jsonb - ); - ` - _, err = p.Exec(t.Context(), sql) - require.NoError(t, err) - - t.Run("When doing a table query should map Postgres column types to Go types", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT * FROM postgres_types", - "format": "table" - }`), - RefID: "A", - }, - }, - } - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Len(t, frames, 1) - require.Len(t, frames[0].Fields, 20) - - require.Equal(t, int16(1), *frames[0].Fields[0].At(0).(*int16)) - require.Equal(t, int32(2), *frames[0].Fields[1].At(0).(*int32)) - require.Equal(t, int64(3), *frames[0].Fields[2].At(0).(*int64)) - - require.Equal(t, float64(4.5), *frames[0].Fields[3].At(0).(*float64)) - require.Equal(t, float64(6.7), *frames[0].Fields[4].At(0).(*float64)) - require.Equal(t, float64(1.1), *frames[0].Fields[5].At(0).(*float64)) - require.Equal(t, float64(1.2), *frames[0].Fields[6].At(0).(*float64)) - - require.Equal(t, "char10 ", *frames[0].Fields[7].At(0).(*string)) - require.Equal(t, "varchar10", *frames[0].Fields[8].At(0).(*string)) - require.Equal(t, "text", *frames[0].Fields[9].At(0).(*string)) - - _, ok := frames[0].Fields[10].At(0).(*time.Time) - require.True(t, ok) - _, ok = frames[0].Fields[11].At(0).(*time.Time) - require.True(t, ok) - _, ok = frames[0].Fields[12].At(0).(*time.Time) - require.True(t, ok) - _, ok = frames[0].Fields[13].At(0).(*string) - require.True(t, ok) - _, ok = frames[0].Fields[14].At(0).(*string) - require.True(t, ok) - _, ok = frames[0].Fields[15].At(0).(*time.Time) - require.True(t, ok) - require.Equal(t, "00:15:00", *frames[0].Fields[16].At(0).(*string)) - require.Nil(t, frames[0].Fields[17].At(0)) - - _, ok = frames[0].Fields[18].At(0).(*json.RawMessage) - require.True(t, ok) - require.Equal(t, json.RawMessage(`{"key1": "value1"}`), *frames[0].Fields[18].At(0).(*json.RawMessage)) - _, ok = frames[0].Fields[19].At(0).(*json.RawMessage) - require.True(t, ok) - require.Equal(t, json.RawMessage(`{"key2": "value2"}`), *frames[0].Fields[19].At(0).(*json.RawMessage)) - }) - }) - - t.Run("Given a table with metrics that lacks data for some series ", func(t *testing.T) { - sql := ` - DROP TABLE IF EXISTS metric; - CREATE TABLE metric ( - time timestamp, - value integer - ) - ` - - _, err := p.Exec(t.Context(), sql) - require.NoError(t, err) - - type metric struct { - Time time.Time - Value int64 - } - - series := []*metric{} - firstRange := genTimeRangeByInterval(fromStart, 10*time.Minute, 10*time.Second) - secondRange := genTimeRangeByInterval(fromStart.Add(20*time.Minute), 10*time.Minute, 10*time.Second) - - for _, t := range firstRange { - series = append(series, &metric{ - Time: t, - Value: 15, - }) - } - - for _, t := range secondRange { - series = append(series, &metric{ - Time: t, - Value: 20, - }) - } - - for _, m := range series { - _, err := p.Exec(t.Context(), `INSERT INTO metric ("time", value) VALUES ($1, $2)`, m.Time.UTC(), m.Value) - require.NoError(t, err) - } - - t.Run("When doing a metric query using timeGroup", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", - "format": "time_series" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Len(t, frames, 1) - require.Equal(t, 4, frames[0].Fields[0].Len()) - - // without fill this should result in 4 buckets - - dt := fromStart - - for i := 0; i < 2; i++ { - aValue := *frames[0].Fields[1].At(i).(*float64) - aTime := *frames[0].Fields[0].At(i).(*time.Time) - require.Equal(t, float64(15), aValue) - require.Equal(t, dt, aTime) - dt = dt.Add(5 * time.Minute) - } - - // adjust for 10 minute gap between first and second set of points - dt = dt.Add(10 * time.Minute) - for i := 2; i < 4; i++ { - aValue := *frames[0].Fields[1].At(i).(*float64) - aTime := *frames[0].Fields[0].At(i).(*time.Time) - require.Equal(t, float64(20), aValue) - require.Equal(t, dt, aTime) - dt = dt.Add(5 * time.Minute) - } - }) - - t.Run("When doing a query without a format should default to time_series", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1 " - }`), - RefID: "A", - }, - }, - } - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Len(t, frames, 1) - require.Len(t, frames[0].Fields, 2) - }) - - t.Run("When doing a metric query using timeGroup and $__interval", func(t *testing.T) { - mockInterpolate := sqleng.Interpolate - sqleng.Interpolate = origInterpolate - t.Cleanup(func() { - sqleng.Interpolate = mockInterpolate - }) - - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT $__timeGroup(time, $__interval) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", - "format": "time_series" - }`), - RefID: "A", - Interval: time.Second * 60, - TimeRange: backend.TimeRange{ - From: fromStart, - To: fromStart.Add(30 * time.Minute), - }, - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - frames := queryResult.Frames - - require.NoError(t, queryResult.Error) - require.Equal(t, - "SELECT floor(extract(epoch from time)/60)*60 AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", - frames[0].Meta.ExecutedQueryString) - }) - - t.Run("When doing a metric query using timeGroup with NULL fill enabled", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", - "format": "time_series" - }`), - RefID: "A", - TimeRange: backend.TimeRange{ - From: fromStart, - To: fromStart.Add(34 * time.Minute), - }, - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, 7, frames[0].Fields[0].Len()) - - dt := fromStart - - for i := 0; i < 2; i++ { - aValue := *frames[0].Fields[1].At(i).(*float64) - aTime := *frames[0].Fields[0].At(i).(*time.Time) - require.Equal(t, float64(15), aValue) - require.True(t, aTime.Equal(dt)) - dt = dt.Add(5 * time.Minute) - } - - // check for NULL values inserted by fill - require.Nil(t, frames[0].Fields[1].At(2)) - require.Nil(t, frames[0].Fields[1].At(3)) - - // adjust for 10 minute gap between first and second set of points - dt = dt.Add(10 * time.Minute) - for i := 4; i < 6; i++ { - aValue := *frames[0].Fields[1].At(i).(*float64) - aTime := *frames[0].Fields[0].At(i).(*time.Time) - require.Equal(t, float64(20), aValue) - require.True(t, aTime.Equal(dt)) - dt = dt.Add(5 * time.Minute) - } - - // check for NULL values inserted by fill - require.Nil(t, frames[0].Fields[1].At(6)) - }) - - t.Run("When doing a metric query using timeGroup with value fill enabled", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT $__timeGroup(time, '5m', 1.5) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", - "format": "time_series" - }`), - RefID: "A", - TimeRange: backend.TimeRange{ - From: fromStart, - To: fromStart.Add(34 * time.Minute), - }, - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, 1.5, *frames[0].Fields[1].At(3).(*float64)) - }) - }) - - t.Run("Given a table with one data point", func(t *testing.T) { - type metric struct { - Time time.Time - Value int64 - } - - startTime := time.Now().UTC().Add(-time.Minute * 5) - series := []*metric{ - { - Time: startTime, - Value: 33, - }, - } - - for _, m := range series { - _, err := p.Exec(t.Context(), `INSERT INTO metric ("time", value) VALUES ($1, $2)`, m.Time.UTC(), m.Value) - require.NoError(t, err) - } - - t.Run("querying with time group with default value", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "WITH data AS (SELECT now()-'3m'::interval AS ts, 42 AS n) SELECT $__timeGroup(ts, '1m', 0), n FROM data", - "format": "time_series" - }`), - RefID: "A", - TimeRange: backend.TimeRange{ - From: startTime, - To: startTime.Add(5 * time.Minute), - }, - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, "Time", frames[0].Fields[0].Name) - require.Equal(t, "n", frames[0].Fields[1].Name) - require.Equal(t, float64(0), *frames[0].Fields[1].At(0).(*float64)) - require.Equal(t, float64(0), *frames[0].Fields[1].At(1).(*float64)) - require.Equal(t, float64(42), *frames[0].Fields[1].At(2).(*float64)) - require.Equal(t, float64(0), *frames[0].Fields[1].At(3).(*float64)) - require.Equal(t, float64(0), *frames[0].Fields[1].At(4).(*float64)) - require.Equal(t, float64(0), *frames[0].Fields[1].At(5).(*float64)) - }) - }) - - t.Run("When doing a metric query using timeGroup with previous fill enabled", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT $__timeGroup(time, '5m', previous), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", - "format": "time_series" - }`), - RefID: "A", - TimeRange: backend.TimeRange{ - From: fromStart, - To: fromStart.Add(34 * time.Minute), - }, - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, float64(15.0), *frames[0].Fields[1].At(2).(*float64)) - require.Equal(t, float64(15.0), *frames[0].Fields[1].At(3).(*float64)) - require.Equal(t, float64(20.0), *frames[0].Fields[1].At(6).(*float64)) - }) - - t.Run("Given a table with metrics having multiple values and measurements", func(t *testing.T) { - type metric_values struct { - Time time.Time - TimeInt64 int64 - TimeInt64Nullable *int64 - TimeFloat64 float64 - TimeFloat64Nullable *float64 - TimeInt32 int32 - TimeInt32Nullable *int32 - TimeFloat32 float32 - TimeFloat32Nullable *float32 - Measurement string - ValueOne int64 - ValueTwo int64 - } - - _, err := p.Exec(t.Context(), "DROP TABLE IF EXISTS metric_values") - require.NoError(t, err) - - _, err = p.Exec(t.Context(), `CREATE TABLE metric_values ( - "time" TIMESTAMP NULL, - "timeInt64" BIGINT NOT NULL, "timeInt64Nullable" BIGINT NULL, - "timeFloat64" DOUBLE PRECISION NOT NULL, "timeFloat64Nullable" DOUBLE PRECISION NULL, - "timeInt32" INTEGER NOT NULL, "timeInt32Nullable" INTEGER NULL, - "timeFloat32" DOUBLE PRECISION NOT NULL, "timeFloat32Nullable" DOUBLE PRECISION NULL, - measurement VARCHAR(255) NULL, - "valueOne" INTEGER NULL, "valueTwo" INTEGER NULL - )`) - require.NoError(t, err) - - rng := rand.New(rand.NewSource(time.Now().Unix())) - rnd := func(min, max int64) int64 { - return rng.Int63n(max-min) + min - } - - var tInitial time.Time - - series := []*metric_values{} - for i, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) { - if i == 0 { - tInitial = t - } - tSeconds := t.Unix() - tSecondsInt32 := int32(tSeconds) - tSecondsFloat32 := float32(tSeconds) - tMilliseconds := tSeconds * 1e3 - tMillisecondsFloat := float64(tMilliseconds) - first := metric_values{ - Time: t, - TimeInt64: tMilliseconds, - TimeInt64Nullable: &(tMilliseconds), - TimeFloat64: tMillisecondsFloat, - TimeFloat64Nullable: &tMillisecondsFloat, - TimeInt32: tSecondsInt32, - TimeInt32Nullable: &tSecondsInt32, - TimeFloat32: tSecondsFloat32, - TimeFloat32Nullable: &tSecondsFloat32, - Measurement: "Metric A", - ValueOne: rnd(0, 100), - ValueTwo: rnd(0, 100), - } - second := first - second.Measurement = "Metric B" - second.ValueOne = rnd(0, 100) - second.ValueTwo = rnd(0, 100) - - series = append(series, &first) - series = append(series, &second) - } - - // _, err = session.InsertMulti(series) - for _, m := range series { - _, err := p.Exec(t.Context(), `INSERT INTO "metric_values" ( - time, - "timeInt64", "timeInt64Nullable", - "timeFloat64", "timeFloat64Nullable", - "timeInt32", "timeInt32Nullable", - "timeFloat32", "timeFloat32Nullable", - measurement, "valueOne", "valueTwo" - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, - m.Time, - m.TimeInt64, m.TimeInt64Nullable, - m.TimeFloat64, m.TimeFloat64Nullable, - m.TimeInt32, m.TimeInt32Nullable, - m.TimeFloat32, m.TimeFloat32Nullable, - m.Measurement, m.ValueOne, m.ValueTwo, - ) - require.NoError(t, err) - } - - t.Run( - "When doing a metric query using epoch (int64) as time column and value column (int64) should return metric with time in time.Time", - func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT \"timeInt64\" as time, \"timeInt64\" FROM metric_values ORDER BY time LIMIT 1", - "format": "time_series" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Len(t, frames, 1) - require.True(t, tInitial.Equal(*frames[0].Fields[0].At(0).(*time.Time))) - }) - - t.Run("When doing a metric query using epoch (int64 nullable) as time column and value column (int64 nullable,) should return metric with time in time.Time", - func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT \"timeInt64Nullable\" as time, \"timeInt64Nullable\" FROM metric_values ORDER BY time LIMIT 1", - "format": "time_series" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Len(t, frames, 1) - require.True(t, tInitial.Equal(*frames[0].Fields[0].At(0).(*time.Time))) - }) - - t.Run("When doing a metric query using epoch (float64) as time column and value column (float64), should return metric with time in time.Time", - func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT \"timeFloat64\" as time, \"timeFloat64\" FROM metric_values ORDER BY time LIMIT 1", - "format": "time_series" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Len(t, frames, 1) - require.True(t, tInitial.Equal(*frames[0].Fields[0].At(0).(*time.Time))) - }) - - t.Run("When doing a metric query using epoch (float64 nullable) as time column and value column (float64 nullable), should return metric with time in time.Time", - func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT \"timeFloat64Nullable\" as time, \"timeFloat64Nullable\" FROM metric_values ORDER BY time LIMIT 1", - "format": "time_series" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.True(t, tInitial.Equal(*frames[0].Fields[0].At(0).(*time.Time))) - }) - - t.Run("When doing a metric query using epoch (int32) as time column and value column (int32), should return metric with time in time.Time", - func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT \"timeInt32\" as time, \"timeInt32\" FROM metric_values ORDER BY time LIMIT 1", - "format": "time_series" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.True(t, tInitial.Equal(*frames[0].Fields[0].At(0).(*time.Time))) - }) - - t.Run("When doing a metric query using epoch (int32 nullable) as time column and value column (int32 nullable), should return metric with time in time.Time", - func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT \"timeInt32Nullable\" as time, \"timeInt32Nullable\" FROM metric_values ORDER BY time LIMIT 1", - "format": "time_series" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.True(t, tInitial.Equal(*frames[0].Fields[0].At(0).(*time.Time))) - }) - - t.Run("When doing a metric query using epoch (float32) as time column and value column (float32), should return metric with time in time.Time", - func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT \"timeFloat32\" as time, \"timeFloat32\" FROM metric_values ORDER BY time LIMIT 1", - "format": "time_series" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - aTime := time.Unix(0, int64(float64(float32(tInitial.Unix()))*1e3)*int64(time.Millisecond)) - require.True(t, aTime.Equal(*frames[0].Fields[0].At(0).(*time.Time))) - }) - - t.Run("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable), should return metric with time in time.Time", - func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT \"timeFloat32Nullable\" as time, \"timeFloat32Nullable\" FROM metric_values ORDER BY time LIMIT 1", - "format": "time_series" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - aTime := time.Unix(0, int64(float64(float32(tInitial.Unix()))*1e3)*int64(time.Millisecond)) - require.True(t, aTime.Equal(*frames[0].Fields[0].At(0).(*time.Time))) - }) - - t.Run("When doing a metric query grouping by time and select metric column should return correct series", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values ORDER BY 1", - "format": "time_series" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, 3, len(frames[0].Fields)) - require.Equal(t, "Metric A - value one", frames[0].Fields[1].Name) - require.Equal(t, "Metric B - value one", frames[0].Fields[2].Name) - }) - - t.Run("When doing a metric query with metric column and multiple value columns", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT $__timeEpoch(time), measurement as metric, \"valueOne\", \"valueTwo\" FROM metric_values ORDER BY 1", - "format": "time_series" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.NoError(t, err) - require.Equal(t, 1, len(frames)) - require.Equal(t, 5, len(frames[0].Fields)) - require.Equal(t, "valueOne", frames[0].Fields[1].Name) - require.Equal(t, data.Labels{"metric": "Metric A"}, frames[0].Fields[1].Labels) - require.Equal(t, "valueOne", frames[0].Fields[2].Name) - require.Equal(t, data.Labels{"metric": "Metric B"}, frames[0].Fields[2].Labels) - require.Equal(t, "valueTwo", frames[0].Fields[3].Name) - require.Equal(t, data.Labels{"metric": "Metric A"}, frames[0].Fields[3].Labels) - require.Equal(t, "valueTwo", frames[0].Fields[4].Name) - require.Equal(t, data.Labels{"metric": "Metric B"}, frames[0].Fields[4].Labels) - }) - - t.Run("When doing a metric query grouping by time should return correct series", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values ORDER BY 1", - "format": "time_series" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, 3, len(frames[0].Fields)) - require.Equal(t, "valueOne", frames[0].Fields[1].Name) - require.Equal(t, "valueTwo", frames[0].Fields[2].Name) - }) - - t.Run("When doing a query with timeFrom,timeTo,unixEpochFrom,unixEpochTo macros", func(t *testing.T) { - fakeInterpolate := sqleng.Interpolate - t.Cleanup(func() { - sqleng.Interpolate = fakeInterpolate - }) - sqleng.Interpolate = origInterpolate - - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT time FROM metric_values WHERE time > $__timeFrom() OR time < $__timeFrom() OR 1 < $__unixEpochFrom() OR $__unixEpochTo() > 1 ORDER BY 1", - "format": "time_series" - }`), - RefID: "A", - TimeRange: backend.TimeRange{ - From: fromStart.Add(-5 * time.Minute), - To: fromStart, - }, - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - frames := queryResult.Frames - require.Len(t, frames, 1) - require.Equal(t, - "SELECT time FROM metric_values WHERE time > '2018-03-15T12:55:00Z' OR time < '2018-03-15T12:55:00Z' OR 1 < 1521118500 OR 1521118800 > 1 ORDER BY 1", - frames[0].Meta.ExecutedQueryString) - }) - }) - - t.Run("Given a table with event data", func(t *testing.T) { - type event struct { - TimeSec int64 - Description string - Tags string - } - - _, err := p.Exec(t.Context(), "DROP TABLE IF EXISTS event") - require.NoError(t, err) - _, err = p.Exec(t.Context(), `CREATE TABLE event (time_sec BIGINT NULL, description VARCHAR(255) NULL, tags VARCHAR(255) NULL)`) - require.NoError(t, err) - - events := []*event{} - for _, t := range genTimeRangeByInterval(fromStart.Add(-20*time.Minute), time.Hour, 25*time.Minute) { - events = append(events, &event{ - TimeSec: t.Unix(), - Description: "Someone deployed something", - Tags: "deploy", - }) - events = append(events, &event{ - TimeSec: t.Add(5 * time.Minute).Unix(), - Description: "New support ticket registered", - Tags: "ticket", - }) - } - - for _, e := range events { - _, err := p.Exec(t.Context(), "INSERT INTO event (time_sec, description, tags) VALUES ($1, $2, $3)", e.TimeSec, e.Description, e.Tags) - require.NoError(t, err) - } - - t.Run("When doing an annotation query of deploy events should return expected result", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT \"time_sec\" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC", - "format": "table" - }`), - RefID: "Deploys", - TimeRange: backend.TimeRange{ - From: fromStart.Add(-20 * time.Minute), - To: fromStart.Add(40 * time.Minute), - }, - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - - queryResult := resp.Responses["Deploys"] - - frames := queryResult.Frames - require.Len(t, frames, 1) - require.Len(t, frames[0].Fields, 3) - }) - - t.Run("When doing an annotation query of ticket events should return expected result", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT \"time_sec\" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC", - "format": "table" - }`), - RefID: "Tickets", - TimeRange: backend.TimeRange{ - From: fromStart.Add(-20 * time.Minute), - To: fromStart.Add(40 * time.Minute), - }, - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - - queryResult := resp.Responses["Tickets"] - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, 3, len(frames[0].Fields)) - }) - - t.Run("When doing an annotation query with a time column in datetime format", func(t *testing.T) { - dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) - dtFormat := "2006-01-02 15:04:05.999999999" - - queryjson := fmt.Sprintf("{\"rawSql\": \"SELECT CAST('%s' AS TIMESTAMP) as time, 'message' as text, 'tag1,tag2' as tags\", \"format\": \"table\" }", dt.Format(dtFormat)) - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(queryjson), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, 3, len(frames[0].Fields)) - - // Should be in time.Time - require.Equal(t, dt.Unix(), (*frames[0].Fields[0].At(0).(*time.Time)).Unix()) - }) - - t.Run("When doing an annotation query with a time column in epoch second format should return time.Time", func(t *testing.T) { - dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) - - queryjson := fmt.Sprintf("{\"rawSql\": \"SELECT %d as time, 'message' as text, 'tag1,tag2' as tags\", \"format\": \"table\"}", dt.Unix()) - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(queryjson), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, 3, len(frames[0].Fields)) - - // Should be in time.Time - require.Equal(t, dt.Unix(), (*frames[0].Fields[0].At(0).(*time.Time)).Unix()) - }) - - t.Run("When doing an annotation query with a time column in epoch second format (t *testing.Tint) should return time.Time", func(t *testing.T) { - dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) - queryjson := fmt.Sprintf("{\"rawSql\": \"SELECT cast(%d as bigint) as time, 'message' as text, 'tag1,tag2' as tags\", \"format\": \"table\"}", dt.Unix()) - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(queryjson), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, 3, len(frames[0].Fields)) - - // Should be in time.Time - require.Equal(t, dt.Unix(), (*frames[0].Fields[0].At(0).(*time.Time)).Unix()) - }) - - t.Run("When doing an annotation query with a time column in epoch millisecond format should return time.Time", func(t *testing.T) { - dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) - - queryjson := fmt.Sprintf("{\"rawSql\":\"SELECT %d as time, 'message' as text, 'tag1,tag2' as tags\", \"format\": \"table\"}", dt.Unix()*1000) - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(queryjson), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, 3, len(frames[0].Fields)) - - // Should be in time.Time - require.Equal(t, dt.Unix(), (*frames[0].Fields[0].At(0).(*time.Time)).Unix()) - }) - - t.Run("When doing an annotation query with a time column holding a bigint null value should return nil", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT cast(null as bigint) as time, 'message' as text, 'tag1,tag2' as tags", - "format": "table" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, 3, len(frames[0].Fields)) - - // Should be in time.Time - require.Nil(t, frames[0].Fields[0].At(0)) - }) - - t.Run("When doing an annotation query with a time column holding a timestamp null value should return nil", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT cast(null as timestamp) as time, 'message' as text, 'tag1,tag2' as tags", - "format": "table" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, 3, len(frames[0].Fields)) - - // Should be in time.Time - assert.Nil(t, frames[0].Fields[0].At(0)) - }) - - t.Run("When doing an annotation query with a time and timeend column should return two fields of type time", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT 1631053772276 as time, 1631054012276 as timeend, '' as text, '' as tags", - "format": "table" - }`), - RefID: "A", - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Equal(t, 1, len(frames)) - require.Equal(t, 4, len(frames[0].Fields)) - - require.Equal(t, data.FieldTypeNullableTime, frames[0].Fields[0].Type()) - require.Equal(t, data.FieldTypeNullableTime, frames[0].Fields[1].Type()) - }) - - t.Run("When row limit set to 1", func(t *testing.T) { - jsonData := sqleng.JsonData{ - MaxOpenConns: 10, - MaxIdleConns: 2, - ConnMaxLifetime: 14400, - Timescaledb: false, - Mode: "disable", - ConfigurationMethod: "file-path", - } - - dsInfo := sqleng.DataSourceInfo{ - JsonData: jsonData, - DecryptedSecureJSONData: map[string]string{}, - } - _, handler, err := newPostgresPGX(t.Context(), "error", 1, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) - - require.NoError(t, err) - - t.Run("When doing a table query that returns 2 rows should limit the result to 1 row", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT 1 as value UNION ALL select 2 as value", - "format": "table" - }`), - RefID: "A", - TimeRange: backend.TimeRange{ - From: time.Now(), - To: time.Now(), - }, - }, - }, - } - - resp, err := handler.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - frames := queryResult.Frames - require.NoError(t, err) - require.Equal(t, 1, len(frames)) - require.Equal(t, 1, len(frames[0].Fields)) - require.Equal(t, 1, frames[0].Rows()) - require.Len(t, frames[0].Meta.Notices, 1) - require.Equal(t, data.NoticeSeverityWarning, frames[0].Meta.Notices[0].Severity) - }) - - t.Run("When doing a time series query that returns 2 rows should limit the result to 1 row", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT 1 as time, 1 as value UNION ALL select 2 as time, 2 as value", - "format": "time_series" - }`), - RefID: "A", - TimeRange: backend.TimeRange{ - From: time.Now(), - To: time.Now(), - }, - }, - }, - } - - resp, err := handler.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - frames := queryResult.Frames - require.NoError(t, err) - require.Equal(t, 1, len(frames)) - require.Equal(t, 2, len(frames[0].Fields)) - require.Equal(t, 1, frames[0].Rows()) - require.Len(t, frames[0].Meta.Notices, 1) - require.Equal(t, data.NoticeSeverityWarning, frames[0].Meta.Notices[0].Severity) - }) - }) - }) - - t.Run("Given an empty table", func(t *testing.T) { - _, err := p.Exec(t.Context(), "DROP TABLE IF EXISTS empty_obj") - require.NoError(t, err) - _, err = p.Exec(t.Context(), "CREATE TABLE empty_obj (empty_key VARCHAR(255) NULL, empty_val BIGINT NULL)") - require.NoError(t, err) - - t.Run("When no rows are returned, should return an empty frame", func(t *testing.T) { - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - JSON: []byte(`{ - "rawSql": "SELECT empty_key, empty_val FROM empty_obj", - "format": "table" - }`), - RefID: "A", - TimeRange: backend.TimeRange{ - From: time.Now(), - To: time.Now().Add(1 * time.Minute), - }, - }, - }, - } - - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - - frames := queryResult.Frames - require.Len(t, frames, 1) - require.Equal(t, 0, frames[0].Rows()) - require.NotNil(t, frames[0].Fields) - require.Empty(t, frames[0].Fields) - }) - - t.Run("Should handle multiple result sets without panicking", func(t *testing.T) { - // Create a test table for the panic scenario test - sql := ` - DROP TABLE IF EXISTS test_multi_results; - CREATE TABLE test_multi_results( - id integer, - name text, - value numeric - ); - INSERT INTO test_multi_results VALUES - (1, 'test1', 10.5), - (2, 'test2', 20.7), - (3, 'test3', 30.2); - ` - _, err := p.Exec(t.Context(), sql) - require.NoError(t, err) - - t.Run("Should handle compatible multiple result sets", func(t *testing.T) { - // This query returns multiple result sets with the same structure - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - RefID: "A", - JSON: []byte(`{ - "rawSql": "SELECT id, name FROM test_multi_results WHERE id <= 2; SELECT id, name FROM test_multi_results WHERE id >= 2;", - "format": "table" - }`), - TimeRange: backend.TimeRange{ - From: fromStart, - To: fromStart.Add(1 * time.Hour), - }, - }, - }, - } - - // This should not panic and should work correctly - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Len(t, frames, 1) - - // The frame should be properly constructed from both SELECT results - frame := frames[0] - require.Equal(t, 2, len(frame.Fields)) // id, name from both queries - require.Equal(t, "id", frame.Fields[0].Name) - require.Equal(t, "name", frame.Fields[1].Name) - require.Equal(t, 4, frame.Rows()) // 2 rows from first result + 2 rows from second result - }) - - t.Run("Should return error for incompatible multiple result sets", func(t *testing.T) { - // This query returns multiple result sets with different structures - the kind that used to cause panic - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - RefID: "A", - JSON: []byte(`{ - "rawSql": "SELECT id, name FROM test_multi_results WHERE id <= 2; SELECT id, value FROM test_multi_results WHERE id >= 2;", - "format": "table" - }`), - TimeRange: backend.TimeRange{ - From: fromStart, - To: fromStart.Add(1 * time.Hour), - }, - }, - }, - } - - // This should not panic anymore, but should return an error instead - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - - // We expect an error about column mismatch, not a panic - require.Error(t, queryResult.Error) - require.Contains(t, queryResult.Error.Error(), "column name mismatch") - }) - - t.Run("Should return error for incompatible number of columns", func(t *testing.T) { - // This query returns multiple result sets with different number of columns - // This should fix the error "runtime error: index out of range [1] with length 1" - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - RefID: "A", - JSON: []byte(`{ - "rawSql": "SELECT id, name FROM test_multi_results WHERE id = 1; SELECT id FROM test_multi_results WHERE id = 1;", - "format": "table" - }`), - TimeRange: backend.TimeRange{ - From: fromStart, - To: fromStart.Add(1 * time.Hour), - }, - }, - }, - } - - // This should not panic anymore, but should return an error instead - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - - // We expect an error about incompatible result structure, not a panic - require.Error(t, queryResult.Error) - require.Contains(t, queryResult.Error.Error(), "incompatible result structure: expected 2 columns, got 1 columns") - }) - }) - - t.Run("Should handle queries with mixed statement types", func(t *testing.T) { - // This tests a scenario with UPDATE + SELECT that could cause the original panic - query := &backend.QueryDataRequest{ - Queries: []backend.DataQuery{ - { - RefID: "A", - JSON: []byte(`{ - "rawSql": "UPDATE test_multi_results SET name = 'updated' WHERE id = 1; SELECT id, name FROM test_multi_results WHERE id = 1;", - "format": "table" - }`), - TimeRange: backend.TimeRange{ - From: fromStart, - To: fromStart.Add(1 * time.Hour), - }, - }, - }, - } - - // This should not panic - resp, err := exe.QueryData(t.Context(), query) - require.NoError(t, err) - queryResult := resp.Responses["A"] - require.NoError(t, queryResult.Error) - - frames := queryResult.Frames - require.Len(t, frames, 1) - - // Should only contain data from the SELECT part - frame := frames[0] - require.Equal(t, 2, len(frame.Fields)) // id, name - require.Equal(t, 1, frame.Rows()) // 1 row - - // Verify the update worked - nameField := frame.Fields[1] - nameValue := nameField.At(0).(*string) - require.Equal(t, "updated", *nameValue) - }) - }) - - t.Run("Test Postgres connection with pgpass file", func(t *testing.T) { - require.NoError(t, preparePgpassFile(t)) - require.FileExists(t, os.Getenv("PGPASSFILE"), "Make sure that PGPASSFILE is set and file exists") - - cnnstr := postgresTestDBConnString() - require.NotContains(t, cnnstr, "password=", "Make sure that password is not in the connection string") - - pgpassPool, _, err := newPostgresPGX(t.Context(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) - require.NoError(t, err) - - _, err = pgpassPool.Query(t.Context(), "SELECT 1") // Test connection - require.NoError(t, err) - }) -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_service.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_service.go index 1face10ab2d..5eabc0a3f71 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_service.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_service.go @@ -7,8 +7,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" - "github.com/grafana/grafana/pkg/setting" - sqlengpgx "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/pgx" "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" ) @@ -16,48 +14,30 @@ type Service struct { im instancemgmt.InstanceManager } -func ProvideService(cfg *setting.Cfg) *Service { +func ProvideService() *Service { logger := backend.NewLoggerWith("logger", "tsdb.postgres") s := &Service{ - im: datasource.NewInstanceManager(NewInstanceSettings(logger, cfg.DataPath)), + im: datasource.NewInstanceManager(NewInstanceSettings(logger)), } return s } // NOTE: do not put any business logic into this method. it's whole job is to forward the call "inside" func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - cfg := backend.GrafanaConfigFromContext(ctx) - if cfg.FeatureToggles().IsEnabled("postgresDSUsePGX") { - dsHandler, err := s.getDSInfoPGX(ctx, req.PluginContext) - if err != nil { - return sqlengpgx.ErrToHealthCheckResult(err) - } - return dsHandler.CheckHealth(ctx, req) - } else { - dsHandler, err := s.getDSInfo(ctx, req.PluginContext) - if err != nil { - return sqleng.ErrToHealthCheckResult(err) - } - return dsHandler.CheckHealth(ctx, req) + dsHandler, err := s.getDSInfo(ctx, req.PluginContext) + if err != nil { + return sqleng.ErrToHealthCheckResult(err) } + return dsHandler.CheckHealth(ctx, req) } // NOTE: do not put any business logic into this method. it's whole job is to forward the call "inside" func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - cfg := backend.GrafanaConfigFromContext(ctx) - if cfg.FeatureToggles().IsEnabled("postgresDSUsePGX") { - dsInfo, err := s.getDSInfoPGX(ctx, req.PluginContext) - if err != nil { - return nil, err - } - return dsInfo.QueryData(ctx, req) - } else { - dsInfo, err := s.getDSInfo(ctx, req.PluginContext) - if err != nil { - return nil, err - } - return dsInfo.QueryData(ctx, req) + dsInfo, err := s.getDSInfo(ctx, req.PluginContext) + if err != nil { + return nil, err } + return dsInfo.QueryData(ctx, req) } func (s *Service) getDSInfo(ctx context.Context, pluginCtx backend.PluginContext) (*sqleng.DataSourceHandler, error) { @@ -68,12 +48,3 @@ func (s *Service) getDSInfo(ctx context.Context, pluginCtx backend.PluginContext instance := i.(*sqleng.DataSourceHandler) return instance, nil } - -func (s *Service) getDSInfoPGX(ctx context.Context, pluginCtx backend.PluginContext) (*sqlengpgx.DataSourceHandler, error) { - i, err := s.im.Get(ctx, pluginCtx) - if err != nil { - return nil, err - } - instance := i.(*sqlengpgx.DataSourceHandler) - return instance, nil -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go index c0017825b82..6aa7a22e7f5 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go @@ -1,7 +1,6 @@ package postgres import ( - "context" "encoding/json" "fmt" "os" @@ -23,7 +22,7 @@ import ( var updateGoldenFiles = false // These tests require a real postgres database: -// - make devenv sources=potgres_tests +// - make devenv sources=postgres_tests // - either set the env variable GRAFANA_TEST_DB = postgres // - or set `forceRun := true` below // @@ -127,6 +126,8 @@ func TestIntegrationPostgresSnapshots(t *testing.T) { {format: "table", name: "types_char"}, {format: "table", name: "types_datetime"}, {format: "table", name: "types_other"}, + {format: "table", name: "types_enum"}, + {format: "table", name: "types_jsonb"}, {format: "table", name: "timestamp_convert_bigint"}, {format: "table", name: "timestamp_convert_integer"}, {format: "table", name: "timestamp_convert_real"}, @@ -148,10 +149,11 @@ func TestIntegrationPostgresSnapshots(t *testing.T) { } jsonData := sqleng.JsonData{ - MaxOpenConns: 0, + MaxOpenConns: 10, MaxIdleConns: 2, ConnMaxLifetime: 14400, Timescaledb: false, + Mode: "disable", ConfigurationMethod: "file-path", } @@ -164,28 +166,24 @@ func TestIntegrationPostgresSnapshots(t *testing.T) { cnnstr := getCnnStr() - db, handler, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) - - t.Cleanup((func() { - _, err := db.Exec("DROP TABLE tbl") - require.NoError(t, err) - err = db.Close() - require.NoError(t, err) - })) - + p, handler, err := newPostgres(t.Context(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) require.NoError(t, err) + t.Cleanup(func() { + p.Close() + }) + sqlFilePath := filepath.Join("testdata", test.format, test.name+".sql") goldenFileName := filepath.Join(test.format, test.name+".golden") rawSQL, sql := readSqlFile(sqlFilePath) - _, err = db.Exec(sql) + _, err = p.Exec(t.Context(), sql) require.NoError(t, err) query := makeQuery(rawSQL, test.format) - result, err := handler.QueryData(context.Background(), &query) + result, err := handler.QueryData(t.Context(), &query) require.Len(t, result.Responses, 1) response, found := result.Responses["A"] require.True(t, found) diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go index 7d6e99fcb92..2de6a6bc6f0 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go @@ -1,7 +1,7 @@ package postgres import ( - "context" + "encoding/json" "fmt" "math/rand" "os" @@ -15,20 +15,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" - "github.com/grafana/grafana/pkg/util/testutil" - _ "github.com/lib/pq" ) // Test generateConnectionString. func TestIntegrationGenerateConnectionString(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) - cfg := setting.NewCfg() - cfg.DataPath = t.TempDir() - testCases := []struct { desc string host string @@ -58,15 +52,6 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { tlsSettings: tlsSettings{Mode: "verify-full"}, expConnStr: "user='user' host='host' dbname='database' password='password' sslmode='verify-full'", }, - { - desc: "verify-ca automatically adds disable-sni", - host: "host:1234", - user: "user", - password: "password", - database: "database", - tlsSettings: tlsSettings{Mode: "verify-ca"}, - expConnStr: "user='user' host='host' dbname='database' password='password' port=1234 sslmode='verify-ca' sslsni=0", - }, { desc: "TCP/port host", host: "host:1234", @@ -156,8 +141,6 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { } for _, tt := range testCases { t.Run(tt.desc, func(t *testing.T) { - logger := backend.NewLoggerWith("logger", "tsdb.postgres") - ds := sqleng.DataSourceInfo{ URL: tt.host, User: tt.user, @@ -165,8 +148,9 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { Database: tt.database, UID: tt.uid, } + logger := backend.NewLoggerWith("logger", "tsdb.postgres") - connStr, err := generateConnectionString(ds, tt.tlsSettings, false, logger) + connStr, err := generateConnectionString(ds, tt.tlsSettings, logger) if tt.expErr == "" { require.NoError(t, err, tt.desc) @@ -207,10 +191,11 @@ func TestIntegrationPostgres(t *testing.T) { } jsonData := sqleng.JsonData{ - MaxOpenConns: 0, + MaxOpenConns: 10, MaxIdleConns: 2, ConnMaxLifetime: 14400, Timescaledb: false, + Mode: "disable", ConfigurationMethod: "file-path", } @@ -223,7 +208,7 @@ func TestIntegrationPostgres(t *testing.T) { cnnstr := postgresTestDBConnString() - db, exe, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + p, exe, err := newPostgres(t.Context(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) require.NoError(t, err) @@ -253,10 +238,13 @@ func TestIntegrationPostgres(t *testing.T) { c14_timetz time with time zone, time date, c15_interval interval, - c16_smallint smallint + c16_smallint smallint, + + c17_json json, + c18_jsonb jsonb ); ` - _, err := db.Exec(sql) + _, err := p.Exec(t.Context(), sql) require.NoError(t, err) sql = ` @@ -264,12 +252,11 @@ func TestIntegrationPostgres(t *testing.T) { 1,2,3, 4.5,6.7,1.1,1.2, 'char10','varchar10','text', - - now(),now(),now(),now(),now(),now(),'15m'::interval, - null + now(),now(),now(),now(),now(),now(),'15m'::interval,null, + '{"key1": "value1"}'::json, '{"key2": "value2"}'::jsonb ); ` - _, err = db.Exec(sql) + _, err = p.Exec(t.Context(), sql) require.NoError(t, err) t.Run("When doing a table query should map Postgres column types to Go types", func(t *testing.T) { @@ -284,14 +271,14 @@ func TestIntegrationPostgres(t *testing.T) { }, }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) frames := queryResult.Frames require.Len(t, frames, 1) - require.Len(t, frames[0].Fields, 18) + require.Len(t, frames[0].Fields, 20) require.Equal(t, int16(1), *frames[0].Fields[0].At(0).(*int16)) require.Equal(t, int32(2), *frames[0].Fields[1].At(0).(*int32)) @@ -312,14 +299,21 @@ func TestIntegrationPostgres(t *testing.T) { require.True(t, ok) _, ok = frames[0].Fields[12].At(0).(*time.Time) require.True(t, ok) - _, ok = frames[0].Fields[13].At(0).(*time.Time) + _, ok = frames[0].Fields[13].At(0).(*string) require.True(t, ok) - _, ok = frames[0].Fields[14].At(0).(*time.Time) + _, ok = frames[0].Fields[14].At(0).(*string) require.True(t, ok) _, ok = frames[0].Fields[15].At(0).(*time.Time) require.True(t, ok) require.Equal(t, "00:15:00", *frames[0].Fields[16].At(0).(*string)) require.Nil(t, frames[0].Fields[17].At(0)) + + _, ok = frames[0].Fields[18].At(0).(*json.RawMessage) + require.True(t, ok) + require.Equal(t, json.RawMessage(`{"key1": "value1"}`), *frames[0].Fields[18].At(0).(*json.RawMessage)) + _, ok = frames[0].Fields[19].At(0).(*json.RawMessage) + require.True(t, ok) + require.Equal(t, json.RawMessage(`{"key2": "value2"}`), *frames[0].Fields[19].At(0).(*json.RawMessage)) }) }) @@ -332,7 +326,7 @@ func TestIntegrationPostgres(t *testing.T) { ) ` - _, err := db.Exec(sql) + _, err := p.Exec(t.Context(), sql) require.NoError(t, err) type metric struct { @@ -359,7 +353,7 @@ func TestIntegrationPostgres(t *testing.T) { } for _, m := range series { - _, err := db.Exec(`INSERT INTO metric ("time", value) VALUES ($1, $2)`, m.Time.UTC(), m.Value) + _, err := p.Exec(t.Context(), `INSERT INTO metric ("time", value) VALUES ($1, $2)`, m.Time.UTC(), m.Value) require.NoError(t, err) } @@ -376,7 +370,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -408,6 +402,27 @@ func TestIntegrationPostgres(t *testing.T) { } }) + t.Run("When doing a query without a format should default to time_series", func(t *testing.T) { + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1 " + }`), + RefID: "A", + }, + }, + } + resp, err := exe.QueryData(t.Context(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Len(t, frames, 1) + require.Len(t, frames[0].Fields, 2) + }) + t.Run("When doing a metric query using timeGroup and $__interval", func(t *testing.T) { mockInterpolate := sqleng.Interpolate sqleng.Interpolate = origInterpolate @@ -432,7 +447,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] frames := queryResult.Frames @@ -460,7 +475,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -514,7 +529,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -540,7 +555,7 @@ func TestIntegrationPostgres(t *testing.T) { } for _, m := range series { - _, err := db.Exec(`INSERT INTO metric ("time", value) VALUES ($1, $2)`, m.Time.UTC(), m.Value) + _, err := p.Exec(t.Context(), `INSERT INTO metric ("time", value) VALUES ($1, $2)`, m.Time.UTC(), m.Value) require.NoError(t, err) } @@ -561,7 +576,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -596,7 +611,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -624,10 +639,10 @@ func TestIntegrationPostgres(t *testing.T) { ValueTwo int64 } - _, err := db.Exec("DROP TABLE IF EXISTS metric_values") + _, err := p.Exec(t.Context(), "DROP TABLE IF EXISTS metric_values") require.NoError(t, err) - _, err = db.Exec(`CREATE TABLE metric_values ( + _, err = p.Exec(t.Context(), `CREATE TABLE metric_values ( "time" TIMESTAMP NULL, "timeInt64" BIGINT NOT NULL, "timeInt64Nullable" BIGINT NULL, "timeFloat64" DOUBLE PRECISION NOT NULL, "timeFloat64Nullable" DOUBLE PRECISION NULL, @@ -680,7 +695,7 @@ func TestIntegrationPostgres(t *testing.T) { // _, err = session.InsertMulti(series) for _, m := range series { - _, err := db.Exec(`INSERT INTO "metric_values" ( + _, err := p.Exec(t.Context(), `INSERT INTO "metric_values" ( time, "timeInt64", "timeInt64Nullable", "timeFloat64", "timeFloat64Nullable", @@ -713,7 +728,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -737,7 +752,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -761,7 +776,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -785,7 +800,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -809,7 +824,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -833,7 +848,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -857,7 +872,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -882,7 +897,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -906,7 +921,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -931,7 +946,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -963,7 +978,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -998,7 +1013,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -1017,9 +1032,9 @@ func TestIntegrationPostgres(t *testing.T) { Tags string } - _, err := db.Exec("DROP TABLE IF EXISTS event") + _, err := p.Exec(t.Context(), "DROP TABLE IF EXISTS event") require.NoError(t, err) - _, err = db.Exec(`CREATE TABLE event (time_sec BIGINT NULL, description VARCHAR(255) NULL, tags VARCHAR(255) NULL)`) + _, err = p.Exec(t.Context(), `CREATE TABLE event (time_sec BIGINT NULL, description VARCHAR(255) NULL, tags VARCHAR(255) NULL)`) require.NoError(t, err) events := []*event{} @@ -1037,7 +1052,7 @@ func TestIntegrationPostgres(t *testing.T) { } for _, e := range events { - _, err := db.Exec("INSERT INTO event (time_sec, description, tags) VALUES ($1, $2, $3)", e.TimeSec, e.Description, e.Tags) + _, err := p.Exec(t.Context(), "INSERT INTO event (time_sec, description, tags) VALUES ($1, $2, $3)", e.TimeSec, e.Description, e.Tags) require.NoError(t, err) } @@ -1058,7 +1073,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["Deploys"] @@ -1085,7 +1100,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["Tickets"] @@ -1108,7 +1123,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -1133,7 +1148,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -1158,7 +1173,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -1184,7 +1199,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -1210,7 +1225,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -1236,7 +1251,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -1262,7 +1277,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -1276,8 +1291,20 @@ func TestIntegrationPostgres(t *testing.T) { }) t.Run("When row limit set to 1", func(t *testing.T) { - dsInfo := sqleng.DataSourceInfo{} - _, handler, err := newPostgres(context.Background(), "error", 1, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + jsonData := sqleng.JsonData{ + MaxOpenConns: 10, + MaxIdleConns: 2, + ConnMaxLifetime: 14400, + Timescaledb: false, + Mode: "disable", + ConfigurationMethod: "file-path", + } + + dsInfo := sqleng.DataSourceInfo{ + JsonData: jsonData, + DecryptedSecureJSONData: map[string]string{}, + } + _, handler, err := newPostgres(t.Context(), "error", 1, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) require.NoError(t, err) @@ -1298,7 +1325,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := handler.QueryData(context.Background(), query) + resp, err := handler.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -1328,7 +1355,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := handler.QueryData(context.Background(), query) + resp, err := handler.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] require.NoError(t, queryResult.Error) @@ -1344,9 +1371,9 @@ func TestIntegrationPostgres(t *testing.T) { }) t.Run("Given an empty table", func(t *testing.T) { - _, err := db.Exec("DROP TABLE IF EXISTS empty_obj") + _, err := p.Exec(t.Context(), "DROP TABLE IF EXISTS empty_obj") require.NoError(t, err) - _, err = db.Exec("CREATE TABLE empty_obj (empty_key VARCHAR(255) NULL, empty_val BIGINT NULL)") + _, err = p.Exec(t.Context(), "CREATE TABLE empty_obj (empty_key VARCHAR(255) NULL, empty_val BIGINT NULL)") require.NoError(t, err) t.Run("When no rows are returned, should return an empty frame", func(t *testing.T) { @@ -1366,7 +1393,7 @@ func TestIntegrationPostgres(t *testing.T) { }, } - resp, err := exe.QueryData(context.Background(), query) + resp, err := exe.QueryData(t.Context(), query) require.NoError(t, err) queryResult := resp.Responses["A"] @@ -1376,6 +1403,154 @@ func TestIntegrationPostgres(t *testing.T) { require.NotNil(t, frames[0].Fields) require.Empty(t, frames[0].Fields) }) + + t.Run("Should handle multiple result sets without panicking", func(t *testing.T) { + // Create a test table for the panic scenario test + sql := ` + DROP TABLE IF EXISTS test_multi_results; + CREATE TABLE test_multi_results( + id integer, + name text, + value numeric + ); + INSERT INTO test_multi_results VALUES + (1, 'test1', 10.5), + (2, 'test2', 20.7), + (3, 'test3', 30.2); + ` + _, err := p.Exec(t.Context(), sql) + require.NoError(t, err) + + t.Run("Should handle compatible multiple result sets", func(t *testing.T) { + // This query returns multiple result sets with the same structure + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + RefID: "A", + JSON: []byte(`{ + "rawSql": "SELECT id, name FROM test_multi_results WHERE id <= 2; SELECT id, name FROM test_multi_results WHERE id >= 2;", + "format": "table" + }`), + TimeRange: backend.TimeRange{ + From: fromStart, + To: fromStart.Add(1 * time.Hour), + }, + }, + }, + } + + // This should not panic and should work correctly + resp, err := exe.QueryData(t.Context(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Len(t, frames, 1) + + // The frame should be properly constructed from both SELECT results + frame := frames[0] + require.Equal(t, 2, len(frame.Fields)) // id, name from both queries + require.Equal(t, "id", frame.Fields[0].Name) + require.Equal(t, "name", frame.Fields[1].Name) + require.Equal(t, 4, frame.Rows()) // 2 rows from first result + 2 rows from second result + }) + + t.Run("Should return error for incompatible multiple result sets", func(t *testing.T) { + // This query returns multiple result sets with different structures - the kind that used to cause panic + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + RefID: "A", + JSON: []byte(`{ + "rawSql": "SELECT id, name FROM test_multi_results WHERE id <= 2; SELECT id, value FROM test_multi_results WHERE id >= 2;", + "format": "table" + }`), + TimeRange: backend.TimeRange{ + From: fromStart, + To: fromStart.Add(1 * time.Hour), + }, + }, + }, + } + + // This should not panic anymore, but should return an error instead + resp, err := exe.QueryData(t.Context(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + + // We expect an error about column mismatch, not a panic + require.Error(t, queryResult.Error) + require.Contains(t, queryResult.Error.Error(), "column name mismatch") + }) + + t.Run("Should return error for incompatible number of columns", func(t *testing.T) { + // This query returns multiple result sets with different number of columns + // This should fix the error "runtime error: index out of range [1] with length 1" + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + RefID: "A", + JSON: []byte(`{ + "rawSql": "SELECT id, name FROM test_multi_results WHERE id = 1; SELECT id FROM test_multi_results WHERE id = 1;", + "format": "table" + }`), + TimeRange: backend.TimeRange{ + From: fromStart, + To: fromStart.Add(1 * time.Hour), + }, + }, + }, + } + + // This should not panic anymore, but should return an error instead + resp, err := exe.QueryData(t.Context(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + + // We expect an error about incompatible result structure, not a panic + require.Error(t, queryResult.Error) + require.Contains(t, queryResult.Error.Error(), "incompatible result structure: expected 2 columns, got 1 columns") + }) + }) + + t.Run("Should handle queries with mixed statement types", func(t *testing.T) { + // This tests a scenario with UPDATE + SELECT that could cause the original panic + query := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + RefID: "A", + JSON: []byte(`{ + "rawSql": "UPDATE test_multi_results SET name = 'updated' WHERE id = 1; SELECT id, name FROM test_multi_results WHERE id = 1;", + "format": "table" + }`), + TimeRange: backend.TimeRange{ + From: fromStart, + To: fromStart.Add(1 * time.Hour), + }, + }, + }, + } + + // This should not panic + resp, err := exe.QueryData(t.Context(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + require.Len(t, frames, 1) + + // Should only contain data from the SELECT part + frame := frames[0] + require.Equal(t, 2, len(frame.Fields)) // id, name + require.Equal(t, 1, frame.Rows()) // 1 row + + // Verify the update worked + nameField := frame.Fields[1] + nameValue := nameField.At(0).(*string) + require.Equal(t, "updated", *nameValue) + }) }) t.Run("Test Postgres connection with pgpass file", func(t *testing.T) { @@ -1385,10 +1560,10 @@ func TestIntegrationPostgres(t *testing.T) { cnnstr := postgresTestDBConnString() require.NotContains(t, cnnstr, "password=", "Make sure that password is not in the connection string") - dbPgpass, _, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + pgpassPool, _, err := newPostgres(t.Context(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) require.NoError(t, err) - _, err = dbPgpass.Exec("SELECT 1") // Test connection + _, err = pgpassPool.Query(t.Context(), "SELECT 1") // Test connection require.NoError(t, err) }) } diff --git a/pkg/tsdb/grafana-postgresql-datasource/proxy.go b/pkg/tsdb/grafana-postgresql-datasource/proxy.go index dca66df38e9..dc7fa069347 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/proxy.go +++ b/pkg/tsdb/grafana-postgresql-datasource/proxy.go @@ -3,40 +3,13 @@ package postgres import ( "context" "net" - "time" - "github.com/lib/pq" "golang.org/x/net/proxy" ) -// we wrap the proxy.Dialer to become dialer that the postgres module accepts -func newPostgresProxyDialer(dialer proxy.Dialer) pq.Dialer { - return &postgresProxyDialer{d: dialer} -} +type DialFunc = func(ctx context.Context, network string, address string) (net.Conn, error) -var _ pq.Dialer = (&postgresProxyDialer{}) - -// postgresProxyDialer implements the postgres dialer using a proxy dialer, as their functions differ slightly -type postgresProxyDialer struct { - d proxy.Dialer -} - -// Dial uses the normal proxy dial function with the updated dialer -func (p *postgresProxyDialer) Dial(network, addr string) (c net.Conn, err error) { - return p.d.Dial(network, addr) -} - -// DialTimeout uses the normal postgres dial timeout function with the updated dialer -func (p *postgresProxyDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - return p.d.(proxy.ContextDialer).DialContext(ctx, network, address) -} - -type PgxDialFunc = func(ctx context.Context, network string, address string) (net.Conn, error) - -func newPgxDialFunc(dialer proxy.Dialer) PgxDialFunc { +func newDialFunc(dialer proxy.Dialer) DialFunc { return func(ctx context.Context, network string, addr string) (net.Conn, error) { return dialer.Dial(network, addr) } diff --git a/pkg/tsdb/grafana-postgresql-datasource/proxy_test.go b/pkg/tsdb/grafana-postgresql-datasource/proxy_test.go index ec36e1a1eae..967c3ecd523 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/proxy_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/proxy_test.go @@ -1,12 +1,11 @@ package postgres import ( - "database/sql" "fmt" "net" "testing" - "github.com/lib/pq" + "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/require" "golang.org/x/net/proxy" ) @@ -25,15 +24,17 @@ func TestPostgresProxyDriver(t *testing.T) { cnnstr := fmt.Sprintf("postgres://auser:password@%s/db?sslmode=disable", dbURL) t.Run("Connector should use dialer context that routes through the socks proxy to db", func(t *testing.T) { - connector, err := pq.NewConnector(cnnstr) + config, err := pgxpool.ParseConfig(cnnstr) require.NoError(t, err) - dialer := newPostgresProxyDialer(&testDialer{}) - connector.Dialer(dialer) + config.ConnConfig.DialFunc = newDialFunc(&testDialer{}) - db := sql.OpenDB(connector) - err = db.Ping() + pool, err := pgxpool.NewWithConfig(t.Context(), config) + require.NoError(t, err) + defer pool.Close() + err = pool.Ping(t.Context()) + require.Error(t, err) require.Contains(t, err.Error(), "test-dialer is not functional") }) } diff --git a/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth.go b/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth.go index 224d00eace5..183c7f57a9c 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth.go +++ b/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth.go @@ -10,11 +10,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/lib/pq" ) func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - err := e.Ping() + err := e.Ping(ctx) if err != nil { logCheckHealthError(ctx, e.dsInfo, err) if strings.EqualFold(req.PluginContext.User.Role, "Admin") { @@ -64,24 +63,6 @@ func ErrToHealthCheckResult(err error) (*backend.CheckHealthResult, error) { } } - if errors.Is(err, pq.ErrSSLNotSupported) { - res.Message = "SSL error: Failed to connect to the server" - } - if strings.HasPrefix(err.Error(), "pq") { - res.Message = "Database error: Failed to connect to the postgres server" - if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil { - details["verboseMessage"] = unwrappedErr.Error() - } - } - var pqErr *pq.Error - if errors.As(err, &pqErr) { - if pqErr != nil { - if pqErr.Code != "" { - res.Message += fmt.Sprintf(". Postgres error code: %s", pqErr.Code.Name()) - } - details["verboseMessage"] = pqErr.Message - } - } if errors.Is(err, ErrParsingPostgresURL) { res.Message = fmt.Sprintf("Connection string error: %s", ErrParsingPostgresURL.Error()) if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil { diff --git a/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth_test.go b/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth_test.go index 70f6ad65e45..4fe87ca478e 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/lib/pq" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -31,15 +30,6 @@ func TestErrToHealthCheckResult(t *testing.T) { JSONDetails: []byte(`{"errorDetailsLink":"https://grafana.com/docs/grafana/latest/datasources/postgres","verboseMessage":"foo\nread tcp: some op"}`), }, }, - { - name: "db error", - err: errors.Join(errors.New("foo"), &pq.Error{Message: pq.ErrCouldNotDetectUsername.Error(), Code: pq.ErrorCode("28P01")}), - want: &backend.CheckHealthResult{ - Status: backend.HealthStatusError, - Message: "foo\npq: pq: Could not detect default username. Please provide one explicitly. Postgres error code: invalid_password", - JSONDetails: []byte(`{"errorDetailsLink":"https://grafana.com/docs/grafana/latest/datasources/postgres","verboseMessage":"pq: Could not detect default username. Please provide one explicitly"}`), - }, - }, { name: "regular error", err: errors.New("internal server error"), diff --git a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go index fdaac59c6f1..5a19ca859f4 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go +++ b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go @@ -2,7 +2,6 @@ package sqleng import ( "context" - "database/sql" "encoding/json" "errors" "fmt" @@ -19,6 +18,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" + "github.com/jackc/pgx/v5/pgxpool" ) // MetaKeyExecutedQueryString is the key where the executed query should get stored @@ -81,13 +83,13 @@ type DataPluginConfiguration struct { type DataSourceHandler struct { macroEngine SQLMacroEngine queryResultTransformer SqlQueryResultTransformer - db *sql.DB timeColumnNames []string metricColumnTypes []string log log.Logger dsInfo DataSourceInfo rowLimit int64 userError string + pool *pgxpool.Pool } type QueryJson struct { @@ -112,7 +114,7 @@ func (e *DataSourceHandler) TransformQueryError(logger log.Logger, err error) er return e.queryResultTransformer.TransformQueryError(logger, err) } -func NewQueryDataHandler(userFacingDefaultError string, db *sql.DB, config DataPluginConfiguration, queryResultTransformer SqlQueryResultTransformer, +func NewQueryDataHandler(userFacingDefaultError string, p *pgxpool.Pool, config DataPluginConfiguration, queryResultTransformer SqlQueryResultTransformer, macroEngine SQLMacroEngine, log log.Logger) (*DataSourceHandler, error) { queryDataHandler := DataSourceHandler{ queryResultTransformer: queryResultTransformer, @@ -132,7 +134,7 @@ func NewQueryDataHandler(userFacingDefaultError string, db *sql.DB, config DataP queryDataHandler.metricColumnTypes = config.MetricColumnTypes } - queryDataHandler.db = db + queryDataHandler.pool = p return &queryDataHandler, nil } @@ -143,16 +145,16 @@ type DBDataResponse struct { func (e *DataSourceHandler) Dispose() { e.log.Debug("Disposing DB...") - if e.db != nil { - if err := e.db.Close(); err != nil { - e.log.Error("Failed to dispose db", "error", err) - } + + if e.pool != nil { + e.pool.Close() } + e.log.Debug("DB disposed") } -func (e *DataSourceHandler) Ping() error { - return e.db.Ping() +func (e *DataSourceHandler) Ping(ctx context.Context) error { + return e.pool.Ping(ctx) } func (e *DataSourceHandler) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { @@ -167,13 +169,13 @@ func (e *DataSourceHandler) QueryData(ctx context.Context, req *backend.QueryDat } err := json.Unmarshal(query.JSON, &queryjson) if err != nil { - return nil, fmt.Errorf("error unmarshal query json: %w", err) + return nil, backend.DownstreamErrorf("error unmarshal query json: %s", err.Error()) } // the fill-params are only stored inside this function, during query-interpolation. we do not support // sending them in "from the outside" if queryjson.Fill || queryjson.FillInterval != 0.0 || queryjson.FillMode != "" || queryjson.FillValue != 0.0 { - return nil, fmt.Errorf("query fill-parameters not supported") + return nil, backend.DownstreamErrorf("query fill-parameters not supported") } if queryjson.RawSql == "" { @@ -181,7 +183,7 @@ func (e *DataSourceHandler) QueryData(ctx context.Context, req *backend.QueryDat } wg.Add(1) - go e.executeQuery(query, &wg, ctx, ch, queryjson) + go e.executeQuery(ctx, query, &wg, ch, queryjson) } wg.Wait() @@ -196,8 +198,21 @@ func (e *DataSourceHandler) QueryData(ctx context.Context, req *backend.QueryDat return result, nil } -func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitGroup, queryContext context.Context, - ch chan DBDataResponse, queryJson QueryJson) { +func (e *DataSourceHandler) execQuery(ctx context.Context, query string) ([]*pgconn.Result, error) { + c, err := e.pool.Acquire(ctx) + if err != nil { + return nil, backend.DownstreamErrorf("failed to acquire connection: %w", err) + } + defer c.Release() + + mrr := c.Conn().PgConn().Exec(ctx, query) + // Close returns the first error that occurred during the MultiResultReader's use. We will log that later. + defer mrr.Close() //nolint:errcheck + return mrr.ReadAll() +} + +func (e *DataSourceHandler) executeQuery(queryContext context.Context, query backend.DataQuery, wg *sync.WaitGroup, + ch chan DBDataResponse, queryJSON QueryJson) { defer wg.Done() queryResult := DBDataResponse{ dataResponse: backend.DataResponse{}, @@ -205,84 +220,77 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG } logger := e.log.FromContext(queryContext) + defer e.handlePanic(logger, &queryResult, ch) - defer func() { - if r := recover(); r != nil { - logger.Error("ExecuteQuery panic", "error", r, "stack", string(debug.Stack())) - if theErr, ok := r.(error); ok { - queryResult.dataResponse.Error = theErr - queryResult.dataResponse.ErrorSource = backend.ErrorSourcePlugin - } else if theErrString, ok := r.(string); ok { - queryResult.dataResponse.Error = errors.New(theErrString) - queryResult.dataResponse.ErrorSource = backend.ErrorSourcePlugin - } else { - queryResult.dataResponse.Error = fmt.Errorf("unexpected error - %s", e.userError) - queryResult.dataResponse.ErrorSource = backend.ErrorSourceDownstream - } - ch <- queryResult - } - }() - - if queryJson.RawSql == "" { + if queryJSON.RawSql == "" { panic("Query model property rawSql should not be empty at this point") } - timeRange := query.TimeRange - - errAppendDebug := func(frameErr string, err error, query string, source backend.ErrorSource) { - var emptyFrame data.Frame - emptyFrame.SetMeta(&data.FrameMeta{ - ExecutedQueryString: query, - }) - if isDownstreamError(err) { - source = backend.ErrorSourceDownstream - } - queryResult.dataResponse.Error = fmt.Errorf("%s: %w", frameErr, err) - queryResult.dataResponse.ErrorSource = source - queryResult.dataResponse.Frames = data.Frames{&emptyFrame} - ch <- queryResult - } - // global substitutions - interpolatedQuery := Interpolate(query, timeRange, e.dsInfo.JsonData.TimeInterval, queryJson.RawSql) + interpolatedQuery := Interpolate(query, query.TimeRange, e.dsInfo.JsonData.TimeInterval, queryJSON.RawSql) // data source specific substitutions - interpolatedQuery, err := e.macroEngine.Interpolate(&query, timeRange, interpolatedQuery) + interpolatedQuery, err := e.macroEngine.Interpolate(&query, query.TimeRange, interpolatedQuery) if err != nil { - errAppendDebug("interpolation failed", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourcePlugin) + e.handleQueryError("interpolation failed", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) return } - rows, err := e.db.QueryContext(queryContext, interpolatedQuery) + results, err := e.execQuery(queryContext, interpolatedQuery) if err != nil { - errAppendDebug("db query error", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourceDownstream) + e.handleQueryError("db query error", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) return } - defer func() { - if err := rows.Close(); err != nil { - logger.Warn("Failed to close rows", "err", err) + + qm, err := e.newProcessCfg(queryContext, query, results, interpolatedQuery) + if err != nil { + e.handleQueryError("failed to get configurations", err, interpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) + return + } + + frame, err := convertResultsToFrame(results, e.rowLimit) + if err != nil { + e.handleQueryError("convert frame from rows error", err, interpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) + return + } + + e.processFrame(frame, qm, queryResult, ch, logger) +} + +func (e *DataSourceHandler) handleQueryError(frameErr string, err error, query string, source backend.ErrorSource, ch chan DBDataResponse, queryResult DBDataResponse) { + var emptyFrame data.Frame + emptyFrame.SetMeta(&data.FrameMeta{ExecutedQueryString: query}) + if isDownstreamError(err) { + source = backend.ErrorSourceDownstream + } + queryResult.dataResponse.Error = fmt.Errorf("%s: %w", frameErr, err) + queryResult.dataResponse.ErrorSource = source + queryResult.dataResponse.Frames = data.Frames{&emptyFrame} + ch <- queryResult +} + +func (e *DataSourceHandler) handlePanic(logger log.Logger, queryResult *DBDataResponse, ch chan DBDataResponse) { + if r := recover(); r != nil { + logger.Error("ExecuteQuery panic", "error", r, "stack", string(debug.Stack())) + if theErr, ok := r.(error); ok { + queryResult.dataResponse.Error = theErr + queryResult.dataResponse.ErrorSource = backend.ErrorSourcePlugin + } else if theErrString, ok := r.(string); ok { + queryResult.dataResponse.Error = errors.New(theErrString) + queryResult.dataResponse.ErrorSource = backend.ErrorSourcePlugin + } else { + queryResult.dataResponse.Error = fmt.Errorf("unexpected error - %s", e.userError) + queryResult.dataResponse.ErrorSource = backend.ErrorSourceDownstream } - }() - - qm, err := e.newProcessCfg(query, queryContext, rows, interpolatedQuery) - if err != nil { - errAppendDebug("failed to get configurations", err, interpolatedQuery, backend.ErrorSourcePlugin) - return - } - - // Convert row.Rows to dataframe - stringConverters := e.queryResultTransformer.GetConverterList() - frame, err := sqlutil.FrameFromRows(rows, e.rowLimit, sqlutil.ToConverters(stringConverters...)...) - if err != nil { - errAppendDebug("convert frame from rows error", err, interpolatedQuery, backend.ErrorSourcePlugin) - return + ch <- *queryResult } +} +func (e *DataSourceHandler) processFrame(frame *data.Frame, qm *dataQueryModel, queryResult DBDataResponse, ch chan DBDataResponse, logger log.Logger) { if frame.Meta == nil { frame.Meta = &data.FrameMeta{} } - - frame.Meta.ExecutedQueryString = interpolatedQuery + frame.Meta.ExecutedQueryString = qm.InterpolatedQuery // If no rows were returned, clear any previously set `Fields` with a single empty `data.Field` slice. // Then assign `queryResult.dataResponse.Frames` the current single frame with that single empty Field. @@ -296,14 +304,14 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG } if err := convertSQLTimeColumnsToEpochMS(frame, qm); err != nil { - errAppendDebug("converting time columns failed", err, interpolatedQuery, backend.ErrorSourcePlugin) + e.handleQueryError("converting time columns failed", err, qm.InterpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) return } if qm.Format == dataQueryFormatSeries { // time series has to have time column if qm.timeIndex == -1 { - errAppendDebug("db has no time column", errors.New("time column is missing; make sure your data includes a time column for time series format or switch to a table format that doesn't require it"), interpolatedQuery, backend.ErrorSourceDownstream) + e.handleQueryError("db has no time column", errors.New("time column is missing; make sure your data includes a time column for time series format or switch to a table format that doesn't require it"), qm.InterpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) return } @@ -321,7 +329,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG var err error if frame, err = convertSQLValueColumnToFloat(frame, i); err != nil { - errAppendDebug("convert value to float failed", err, interpolatedQuery, backend.ErrorSourcePlugin) + e.handleQueryError("convert value to float failed", err, qm.InterpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) return } } @@ -332,7 +340,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG originalData := frame frame, err = data.LongToWide(frame, qm.FillMissing) if err != nil { - errAppendDebug("failed to convert long to wide series when converting from dataframe", err, interpolatedQuery, backend.ErrorSourcePlugin) + e.handleQueryError("failed to convert long to wide series when converting from dataframe", err, qm.InterpolatedQuery, backend.ErrorSourceDownstream, ch, queryResult) return } @@ -364,6 +372,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG if err != nil { logger.Error("Failed to resample dataframe", "err", err) frame.AppendNotices(data.Notice{Text: "Failed to resample dataframe", Severity: data.NoticeSeverityWarning}) + return } } } @@ -384,15 +393,38 @@ var Interpolate = func(query backend.DataQuery, timeRange backend.TimeRange, tim return sql } -func (e *DataSourceHandler) newProcessCfg(query backend.DataQuery, queryContext context.Context, - rows *sql.Rows, interpolatedQuery string) (*dataQueryModel, error) { - columnNames, err := rows.Columns() - if err != nil { - return nil, err +func (e *DataSourceHandler) newProcessCfg(queryContext context.Context, query backend.DataQuery, + results []*pgconn.Result, interpolatedQuery string) (*dataQueryModel, error) { + // Calculate total number of fields to preallocate slices + totalFields := 0 + for _, result := range results { + totalFields += len(result.FieldDescriptions) } - columnTypes, err := rows.ColumnTypes() - if err != nil { - return nil, err + + columnNames := make([]string, 0, totalFields) + columnTypes := make([]string, 0, totalFields) + + // The results will contain column information in the metadata + for _, result := range results { + // Get column names from the result metadata + for _, field := range result.FieldDescriptions { + columnNames = append(columnNames, field.Name) + pqtype, ok := pgtype.NewMap().TypeForOID(field.DataTypeOID) + if !ok { + // Handle special cases for field types + switch field.DataTypeOID { + case pgtype.TimetzOID: + columnTypes = append(columnTypes, "timetz") + // money type is 790 + case 790: + columnTypes = append(columnTypes, "money") + default: + columnTypes = append(columnTypes, "unknown") + } + } else { + columnTypes = append(columnTypes, pqtype.Name) + } + } } qm := &dataQueryModel{ @@ -405,23 +437,23 @@ func (e *DataSourceHandler) newProcessCfg(query backend.DataQuery, queryContext queryContext: queryContext, } - queryJson := QueryJson{} - err = json.Unmarshal(query.JSON, &queryJson) + queryJSON := QueryJson{} + err := json.Unmarshal(query.JSON, &queryJSON) if err != nil { return nil, err } - if queryJson.Fill { + if queryJSON.Fill { qm.FillMissing = &data.FillMissing{} - qm.Interval = time.Duration(queryJson.FillInterval * float64(time.Second)) - switch strings.ToLower(queryJson.FillMode) { + qm.Interval = time.Duration(queryJSON.FillInterval * float64(time.Second)) + switch strings.ToLower(queryJSON.FillMode) { case "null": qm.FillMissing.Mode = data.FillModeNull case "previous": qm.FillMissing.Mode = data.FillModePrevious case "value": qm.FillMissing.Mode = data.FillModeValue - qm.FillMissing.Value = queryJson.FillValue + qm.FillMissing.Value = queryJSON.FillValue default: } } @@ -429,13 +461,14 @@ func (e *DataSourceHandler) newProcessCfg(query backend.DataQuery, queryContext qm.TimeRange.From = query.TimeRange.From.UTC() qm.TimeRange.To = query.TimeRange.To.UTC() - switch queryJson.Format { - case "time_series": - qm.Format = dataQueryFormatSeries + // Default to time_series if no format is provided + switch queryJSON.Format { case "table": qm.Format = dataQueryFormatTable + case "time_series": + fallthrough default: - panic(fmt.Sprintf("Unrecognized query model format: %q", queryJson.Format)) + qm.Format = dataQueryFormatSeries } for i, col := range qm.columnNames { @@ -456,7 +489,7 @@ func (e *DataSourceHandler) newProcessCfg(query backend.DataQuery, queryContext qm.metricIndex = i default: if qm.metricIndex == -1 { - columnType := qm.columnTypes[i].DatabaseTypeName() + columnType := qm.columnTypes[i] for _, mct := range e.metricColumnTypes { if columnType == mct { qm.metricIndex = i @@ -487,7 +520,7 @@ type dataQueryModel struct { FillMissing *data.FillMissing // property not set until after Interpolate() Interval time.Duration columnNames []string - columnTypes []*sql.ColumnType + columnTypes []string timeIndex int timeEndIndex int metricIndex int @@ -511,6 +544,219 @@ func convertSQLTimeColumnsToEpochMS(frame *data.Frame, qm *dataQueryModel) error return nil } +func convertResultsToFrame(results []*pgconn.Result, rowLimit int64) (*data.Frame, error) { + m := pgtype.NewMap() + + // Find the first SELECT result to establish the frame structure + var firstSelectResult *pgconn.Result + for _, result := range results { + if result.CommandTag.Select() { + firstSelectResult = result + break + } + } + + // If no SELECT results found, return empty frame + if firstSelectResult == nil { + return data.NewFrame(""), nil + } + + // Create frame structure based on the first SELECT result + fields := make(data.Fields, len(firstSelectResult.FieldDescriptions)) + fieldTypes, err := getFieldTypesFromDescriptions(firstSelectResult.FieldDescriptions, m) + if err != nil { + return nil, err + } + + for i, v := range firstSelectResult.FieldDescriptions { + fields[i] = data.NewFieldFromFieldType(fieldTypes[i], 0) + fields[i].Name = v.Name + } + frame := *data.NewFrame("", fields...) + + // Process all SELECT results, but validate column compatibility + for _, result := range results { + // Skip non-select statements + if !result.CommandTag.Select() { + continue + } + + // Validate that this result has the same structure as the frame + if len(result.FieldDescriptions) != len(frame.Fields) { + return nil, fmt.Errorf("incompatible result structure: expected %d columns, got %d columns", + len(frame.Fields), len(result.FieldDescriptions)) + } + + // Validate column names and types match + for i, fd := range result.FieldDescriptions { + if fd.Name != frame.Fields[i].Name { + return nil, fmt.Errorf("column name mismatch at position %d: expected %q, got %q", + i, frame.Fields[i].Name, fd.Name) + } + } + + fieldDescriptions := result.FieldDescriptions + for rowIdx := range result.Rows { + if rowIdx == int(rowLimit) { + frame.AppendNotices(data.Notice{ + Severity: data.NoticeSeverityWarning, + Text: fmt.Sprintf("Results have been limited to %v because the SQL row limit was reached", rowLimit), + }) + break + } + row := make([]any, len(fieldDescriptions)) + for colIdx, fd := range fieldDescriptions { + rawValue := result.Rows[rowIdx][colIdx] + + if rawValue == nil { + row[colIdx] = nil + continue + } + + convertedValue, err := convertPostgresValue(rawValue, fd, m) + if err != nil { + return nil, err + } + row[colIdx] = convertedValue + } + + // Validate row length matches frame field count before appending + if len(row) != len(frame.Fields) { + return nil, fmt.Errorf("row data length mismatch: expected %d values, got %d values", + len(frame.Fields), len(row)) + } + + frame.AppendRow(row...) + } + } + + return &frame, nil +} + +// convertPostgresValue converts a raw PostgreSQL value to the appropriate Go type +func convertPostgresValue(rawValue []byte, fd pgconn.FieldDescription, m *pgtype.Map) (interface{}, error) { + dataTypeOID := fd.DataTypeOID + format := fd.Format + + // Convert based on type + switch fd.DataTypeOID { + case pgtype.Int2OID: + var d *int16 + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + return d, nil + case pgtype.Int4OID: + var d *int32 + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + return d, nil + case pgtype.Int8OID: + var d *int64 + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + return d, nil + case pgtype.NumericOID, pgtype.Float8OID, pgtype.Float4OID: + var d *float64 + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + return d, nil + case pgtype.BoolOID: + var d *bool + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + return d, nil + case pgtype.ByteaOID: + d, err := pgtype.ByteaCodec.DecodeValue(pgtype.ByteaCodec{}, m, dataTypeOID, format, rawValue) + if err != nil { + return nil, err + } + str := string(d.([]byte)) + return &str, nil + case pgtype.TimestampOID, pgtype.TimestamptzOID, pgtype.DateOID: + var d *time.Time + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + return d, nil + case pgtype.TimeOID, pgtype.TimetzOID: + var d *string + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + return d, nil + case pgtype.JSONOID, pgtype.JSONBOID: + var d *string + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + // Handle null JSON values + if d == nil { + return nil, nil + } + j := json.RawMessage(*d) + return &j, nil + default: + var d *string + scanPlan := m.PlanScan(dataTypeOID, format, &d) + err := scanPlan.Scan(rawValue, &d) + if err != nil { + return nil, err + } + return d, nil + } +} + +func getFieldTypesFromDescriptions(fieldDescriptions []pgconn.FieldDescription, m *pgtype.Map) ([]data.FieldType, error) { + fieldTypes := make([]data.FieldType, len(fieldDescriptions)) + for i, v := range fieldDescriptions { + typeName, ok := m.TypeForOID(v.DataTypeOID) + if !ok { + fieldTypes[i] = data.FieldTypeNullableString + } else { + switch typeName.Name { + case "int2": + fieldTypes[i] = data.FieldTypeNullableInt16 + case "int4": + fieldTypes[i] = data.FieldTypeNullableInt32 + case "int8": + fieldTypes[i] = data.FieldTypeNullableInt64 + case "float4", "float8", "numeric": + fieldTypes[i] = data.FieldTypeNullableFloat64 + case "bool": + fieldTypes[i] = data.FieldTypeNullableBool + case "timestamptz", "timestamp", "date": + fieldTypes[i] = data.FieldTypeNullableTime + case "json", "jsonb": + fieldTypes[i] = data.FieldTypeNullableJSON + default: + fieldTypes[i] = data.FieldTypeNullableString + } + } + } + return fieldTypes, nil +} + // convertSQLTimeColumnToEpochMS converts column named time to unix timestamp in milliseconds // to make native datetime types and epoch dates work in annotation and table queries. func convertSQLTimeColumnToEpochMS(frame *data.Frame, timeIndex int) error { diff --git a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine_test.go b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine_test.go index 4d511a53f25..f0f5f5b7a9b 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine_test.go @@ -9,6 +9,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -425,6 +427,246 @@ func TestSQLEngine(t *testing.T) { }) } +func TestConvertResultsToFrame(t *testing.T) { + // Import the pgx packages needed for testing + // These imports are included in the main file but need to be accessible for tests + t.Run("convertResultsToFrame with single result", func(t *testing.T) { + // Create mock field descriptions + fieldDescs := []pgconn.FieldDescription{ + {Name: "id", DataTypeOID: pgtype.Int4OID}, + {Name: "name", DataTypeOID: pgtype.TextOID}, + {Name: "value", DataTypeOID: pgtype.Float8OID}, + } + + // Create mock result data + mockRows := [][][]byte{ + {[]byte("1"), []byte("test1"), []byte("10.5")}, + {[]byte("2"), []byte("test2"), []byte("20.7")}, + } + + // Create mock result + result := &pgconn.Result{ + FieldDescriptions: fieldDescs, + Rows: mockRows, + } + result.CommandTag = pgconn.NewCommandTag("SELECT 2") + + results := []*pgconn.Result{result} + + frame, err := convertResultsToFrame(results, 1000) + require.NoError(t, err) + require.NotNil(t, frame) + require.Equal(t, 3, len(frame.Fields)) + require.Equal(t, 2, frame.Rows()) + + // Verify field names + require.Equal(t, "id", frame.Fields[0].Name) + require.Equal(t, "name", frame.Fields[1].Name) + require.Equal(t, "value", frame.Fields[2].Name) + }) + + t.Run("convertResultsToFrame with multiple compatible results", func(t *testing.T) { + // Create mock field descriptions (same structure for both results) + fieldDescs := []pgconn.FieldDescription{ + {Name: "id", DataTypeOID: pgtype.Int4OID}, + {Name: "name", DataTypeOID: pgtype.TextOID}, + } + + // Create first result + mockRows1 := [][][]byte{ + {[]byte("1"), []byte("test1")}, + {[]byte("2"), []byte("test2")}, + } + result1 := &pgconn.Result{ + FieldDescriptions: fieldDescs, + Rows: mockRows1, + } + result1.CommandTag = pgconn.NewCommandTag("SELECT 2") + + // Create second result with same structure + mockRows2 := [][][]byte{ + {[]byte("3"), []byte("test3")}, + {[]byte("4"), []byte("test4")}, + } + result2 := &pgconn.Result{ + FieldDescriptions: fieldDescs, + Rows: mockRows2, + } + result2.CommandTag = pgconn.NewCommandTag("SELECT 2") + + results := []*pgconn.Result{result1, result2} + + frame, err := convertResultsToFrame(results, 1000) + require.NoError(t, err) + require.NotNil(t, frame) + require.Equal(t, 2, len(frame.Fields)) + require.Equal(t, 4, frame.Rows()) // Should have rows from both results + + // Verify field names + require.Equal(t, "id", frame.Fields[0].Name) + require.Equal(t, "name", frame.Fields[1].Name) + }) + + t.Run("convertResultsToFrame with row limit", func(t *testing.T) { + // Create mock field descriptions + fieldDescs := []pgconn.FieldDescription{ + {Name: "id", DataTypeOID: pgtype.Int4OID}, + } + + // Create mock result data with 3 rows + mockRows := [][][]byte{ + {[]byte("1")}, + {[]byte("2")}, + {[]byte("3")}, + } + + result := &pgconn.Result{ + FieldDescriptions: fieldDescs, + Rows: mockRows, + } + result.CommandTag = pgconn.NewCommandTag("SELECT 3") + + results := []*pgconn.Result{result} + + // Set row limit to 2 + frame, err := convertResultsToFrame(results, 2) + require.NoError(t, err) + require.NotNil(t, frame) + require.Equal(t, 1, len(frame.Fields)) + require.Equal(t, 2, frame.Rows()) // Should be limited to 2 rows + + // Should have a notice about the limit + require.NotNil(t, frame.Meta) + require.Len(t, frame.Meta.Notices, 1) + require.Contains(t, frame.Meta.Notices[0].Text, "Results have been limited to 2") + }) + + t.Run("convertResultsToFrame with mixed SELECT and non-SELECT results", func(t *testing.T) { + // Create a non-SELECT result (should be skipped) + nonSelectResult := &pgconn.Result{} + nonSelectResult.CommandTag = pgconn.NewCommandTag("UPDATE 1") + + // Create a SELECT result + fieldDescs := []pgconn.FieldDescription{ + {Name: "id", DataTypeOID: pgtype.Int4OID}, + } + mockRows := [][][]byte{ + {[]byte("1")}, + } + selectResult := &pgconn.Result{ + FieldDescriptions: fieldDescs, + Rows: mockRows, + } + selectResult.CommandTag = pgconn.NewCommandTag("SELECT 1") + + results := []*pgconn.Result{nonSelectResult, selectResult} + + frame, err := convertResultsToFrame(results, 1000) + require.NoError(t, err) + require.NotNil(t, frame) + require.Equal(t, 1, len(frame.Fields)) + require.Equal(t, 1, frame.Rows()) + }) + + t.Run("convertResultsToFrame with no SELECT results", func(t *testing.T) { + // Create only non-SELECT results + result1 := &pgconn.Result{} + result1.CommandTag = pgconn.NewCommandTag("UPDATE 1") + + result2 := &pgconn.Result{} + result2.CommandTag = pgconn.NewCommandTag("INSERT 1") + + results := []*pgconn.Result{result1, result2} + + frame, err := convertResultsToFrame(results, 1000) + require.NoError(t, err) + require.NotNil(t, frame) + require.Equal(t, 0, len(frame.Fields)) + require.Equal(t, 0, frame.Rows()) + }) + + t.Run("convertResultsToFrame with multiple results and row limit per result", func(t *testing.T) { + // Create mock field descriptions (same structure for both results) + fieldDescs := []pgconn.FieldDescription{ + {Name: "id", DataTypeOID: pgtype.Int4OID}, + } + + // Create first result with 3 rows + mockRows1 := [][][]byte{ + {[]byte("1")}, + {[]byte("2")}, + {[]byte("3")}, + } + result1 := &pgconn.Result{ + FieldDescriptions: fieldDescs, + Rows: mockRows1, + } + result1.CommandTag = pgconn.NewCommandTag("SELECT 3") + + // Create second result with 3 rows + mockRows2 := [][][]byte{ + {[]byte("4")}, + {[]byte("5")}, + {[]byte("6")}, + } + result2 := &pgconn.Result{ + FieldDescriptions: fieldDescs, + Rows: mockRows2, + } + result2.CommandTag = pgconn.NewCommandTag("SELECT 3") + + results := []*pgconn.Result{result1, result2} + + // Set row limit to 2 (should limit each result to 2 rows) + frame, err := convertResultsToFrame(results, 2) + require.NoError(t, err) + require.NotNil(t, frame) + require.Equal(t, 1, len(frame.Fields)) + require.Equal(t, 4, frame.Rows()) // 2 rows from each result + + // Should have notices about the limit from both results + require.NotNil(t, frame.Meta) + require.Len(t, frame.Meta.Notices, 2) + require.Contains(t, frame.Meta.Notices[0].Text, "Results have been limited to 2") + require.Contains(t, frame.Meta.Notices[1].Text, "Results have been limited to 2") + }) + + t.Run("convertResultsToFrame handles null values correctly", func(t *testing.T) { + // Create mock field descriptions + fieldDescs := []pgconn.FieldDescription{ + {Name: "id", DataTypeOID: pgtype.Int4OID}, + {Name: "name", DataTypeOID: pgtype.TextOID}, + } + + // Create mock result data with null values + mockRows := [][][]byte{ + {[]byte("1"), nil}, // null name + {nil, []byte("test2")}, // null id + } + + result := &pgconn.Result{ + FieldDescriptions: fieldDescs, + Rows: mockRows, + } + result.CommandTag = pgconn.NewCommandTag("SELECT 2") + + results := []*pgconn.Result{result} + + frame, err := convertResultsToFrame(results, 1000) + require.NoError(t, err) + require.NotNil(t, frame) + require.Equal(t, 2, len(frame.Fields)) + require.Equal(t, 2, frame.Rows()) + + // Check that null values are handled correctly + // The exact representation depends on the field type, but should not panic + require.NotPanics(t, func() { + frame.Fields[0].At(1) // null id + frame.Fields[1].At(0) // null name + }) + }) +} + type testQueryResultTransformer struct { transformQueryErrorWasCalled bool } diff --git a/pkg/tsdb/grafana-postgresql-datasource/standalone/main.go b/pkg/tsdb/grafana-postgresql-datasource/standalone/main.go index 3ad73082408..ddd5105c46b 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/standalone/main.go +++ b/pkg/tsdb/grafana-postgresql-datasource/standalone/main.go @@ -6,16 +6,13 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/setting" postgres "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource" ) func main() { // No need to pass logger name, it will be set by the plugin SDK logger := backend.NewLoggerWith() - // TODO: get rid of setting.NewCfg() once PostgresDSUsePGX is removed - cfg := setting.NewCfg() - if err := datasource.Manage("grafana-postgresql-datasource", postgres.NewInstanceSettings(logger, cfg.DataPath), datasource.ManageOpts{}); err != nil { + if err := datasource.Manage("grafana-postgresql-datasource", postgres.NewInstanceSettings(logger), datasource.ManageOpts{}); err != nil { log.DefaultLogger.Error(err.Error()) os.Exit(1) } diff --git a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime.golden.jsonc b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime.golden.jsonc index 48e55e7b077..5754a588b10 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime.golden.jsonc +++ b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime.golden.jsonc @@ -9,14 +9,14 @@ // } // Name: // Dimensions: 12 Fields by 2 Rows -// +----------------------------------------+----------------------------------------+--------------------------------------+--------------------------------------+---------------------------------+---------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ -// | Name: ts | Name: tsnn | Name: tsz | Name: tsznn | Name: d | Name: dnn | Name: t | Name: tnn | Name: tz | Name: tznn | Name: i | Name: inn | -// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | -// | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*string | Type: []*string | -// +----------------------------------------+----------------------------------------+--------------------------------------+--------------------------------------+---------------------------------+---------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ -// | 2023-11-15 05:06:07.123456 +0000 +0000 | 2023-11-15 05:06:08.123456 +0000 +0000 | 2021-07-22 11:22:33.654321 +0000 UTC | 2021-07-22 11:22:34.654321 +0000 UTC | 2023-12-20 00:00:00 +0000 +0000 | 2023-12-21 00:00:00 +0000 +0000 | 0000-01-01 12:34:56.234567 +0000 UTC | 0000-01-01 12:34:57.234567 +0000 UTC | 0000-01-01 23:12:36.765432 +0100 +0100 | 0000-01-01 23:12:37.765432 +0100 +0100 | 00:00:00.987654 | 00:00:00.887654 | -// | null | 2023-11-15 05:06:09.123456 +0000 +0000 | null | 2021-07-22 11:22:35.654321 +0000 UTC | null | 2023-12-22 00:00:00 +0000 +0000 | null | 0000-01-01 12:34:58.234567 +0000 UTC | null | 0000-01-01 23:12:38.765432 +0100 +0100 | null | 00:00:00.787654 | -// +----------------------------------------+----------------------------------------+--------------------------------------+--------------------------------------+---------------------------------+---------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ +// +--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-------------------------------+-------------------------------+-----------------+-----------------+--------------------+--------------------+-----------------+-----------------+ +// | Name: ts | Name: tsnn | Name: tsz | Name: tsznn | Name: d | Name: dnn | Name: t | Name: tnn | Name: tz | Name: tznn | Name: i | Name: inn | +// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | +// | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*string | Type: []*string | Type: []*string | Type: []*string | Type: []*string | Type: []*string | +// +--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-------------------------------+-------------------------------+-----------------+-----------------+--------------------+--------------------+-----------------+-----------------+ +// | 2023-11-15 05:06:07.123456 +0000 UTC | 2023-11-15 05:06:08.123456 +0000 UTC | 2021-07-22 11:22:33.654321 +0000 +0000 | 2021-07-22 11:22:34.654321 +0000 +0000 | 2023-12-20 00:00:00 +0000 UTC | 2023-12-21 00:00:00 +0000 UTC | 12:34:56.234567 | 12:34:57.234567 | 23:12:36.765432+01 | 23:12:37.765432+01 | 00:00:00.987654 | 00:00:00.887654 | +// | null | 2023-11-15 05:06:09.123456 +0000 UTC | null | 2021-07-22 11:22:35.654321 +0000 +0000 | null | 2023-12-22 00:00:00 +0000 UTC | null | 12:34:58.234567 | null | 23:12:38.765432+01 | null | 00:00:00.787654 | +// +--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-------------------------------+-------------------------------+-----------------+-----------------+--------------------+--------------------+-----------------+-----------------+ // // // 🌟 This was machine generated. Do not edit. 🌟 @@ -83,33 +83,33 @@ }, { "name": "t", - "type": "time", + "type": "string", "typeInfo": { - "frame": "time.Time", + "frame": "string", "nullable": true } }, { "name": "tnn", - "type": "time", + "type": "string", "typeInfo": { - "frame": "time.Time", + "frame": "string", "nullable": true } }, { "name": "tz", - "type": "time", + "type": "string", "typeInfo": { - "frame": "time.Time", + "frame": "string", "nullable": true } }, { "name": "tznn", - "type": "time", + "type": "string", "typeInfo": { - "frame": "time.Time", + "frame": "string", "nullable": true } }, @@ -158,20 +158,20 @@ 1703203200000 ], [ - -62167173903766, + "12:34:56.234567", null ], [ - -62167173902766, - -62167173901766 + "12:34:57.234567", + "12:34:58.234567" ], [ - -62167139243235, + "23:12:36.765432+01", null ], [ - -62167139242235, - -62167139241235 + "23:12:37.765432+01", + "23:12:38.765432+01" ], [ "00:00:00.987654", @@ -201,22 +201,10 @@ ], null, null, - [ - 567000, - 0 - ], - [ - 567000, - 567000 - ], - [ - 432000, - 0 - ], - [ - 432000, - 432000 - ], + null, + null, + null, + null, null, null ] diff --git a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.golden.jsonc b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.golden.jsonc deleted file mode 100644 index 5754a588b10..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.golden.jsonc +++ /dev/null @@ -1,214 +0,0 @@ -// 🌟 This was machine generated. Do not edit. 🌟 -// -// Frame[0] { -// "typeVersion": [ -// 0, -// 0 -// ], -// "executedQueryString": "SELECT * FROM tbl" -// } -// Name: -// Dimensions: 12 Fields by 2 Rows -// +--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-------------------------------+-------------------------------+-----------------+-----------------+--------------------+--------------------+-----------------+-----------------+ -// | Name: ts | Name: tsnn | Name: tsz | Name: tsznn | Name: d | Name: dnn | Name: t | Name: tnn | Name: tz | Name: tznn | Name: i | Name: inn | -// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | -// | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*string | Type: []*string | Type: []*string | Type: []*string | Type: []*string | Type: []*string | -// +--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-------------------------------+-------------------------------+-----------------+-----------------+--------------------+--------------------+-----------------+-----------------+ -// | 2023-11-15 05:06:07.123456 +0000 UTC | 2023-11-15 05:06:08.123456 +0000 UTC | 2021-07-22 11:22:33.654321 +0000 +0000 | 2021-07-22 11:22:34.654321 +0000 +0000 | 2023-12-20 00:00:00 +0000 UTC | 2023-12-21 00:00:00 +0000 UTC | 12:34:56.234567 | 12:34:57.234567 | 23:12:36.765432+01 | 23:12:37.765432+01 | 00:00:00.987654 | 00:00:00.887654 | -// | null | 2023-11-15 05:06:09.123456 +0000 UTC | null | 2021-07-22 11:22:35.654321 +0000 +0000 | null | 2023-12-22 00:00:00 +0000 UTC | null | 12:34:58.234567 | null | 23:12:38.765432+01 | null | 00:00:00.787654 | -// +--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-------------------------------+-------------------------------+-----------------+-----------------+--------------------+--------------------+-----------------+-----------------+ -// -// -// 🌟 This was machine generated. Do not edit. 🌟 -{ - "status": 200, - "frames": [ - { - "schema": { - "meta": { - "typeVersion": [ - 0, - 0 - ], - "executedQueryString": "SELECT * FROM tbl" - }, - "fields": [ - { - "name": "ts", - "type": "time", - "typeInfo": { - "frame": "time.Time", - "nullable": true - } - }, - { - "name": "tsnn", - "type": "time", - "typeInfo": { - "frame": "time.Time", - "nullable": true - } - }, - { - "name": "tsz", - "type": "time", - "typeInfo": { - "frame": "time.Time", - "nullable": true - } - }, - { - "name": "tsznn", - "type": "time", - "typeInfo": { - "frame": "time.Time", - "nullable": true - } - }, - { - "name": "d", - "type": "time", - "typeInfo": { - "frame": "time.Time", - "nullable": true - } - }, - { - "name": "dnn", - "type": "time", - "typeInfo": { - "frame": "time.Time", - "nullable": true - } - }, - { - "name": "t", - "type": "string", - "typeInfo": { - "frame": "string", - "nullable": true - } - }, - { - "name": "tnn", - "type": "string", - "typeInfo": { - "frame": "string", - "nullable": true - } - }, - { - "name": "tz", - "type": "string", - "typeInfo": { - "frame": "string", - "nullable": true - } - }, - { - "name": "tznn", - "type": "string", - "typeInfo": { - "frame": "string", - "nullable": true - } - }, - { - "name": "i", - "type": "string", - "typeInfo": { - "frame": "string", - "nullable": true - } - }, - { - "name": "inn", - "type": "string", - "typeInfo": { - "frame": "string", - "nullable": true - } - } - ] - }, - "data": { - "values": [ - [ - 1700024767123, - null - ], - [ - 1700024768123, - 1700024769123 - ], - [ - 1626952953654, - null - ], - [ - 1626952954654, - 1626952955654 - ], - [ - 1703030400000, - null - ], - [ - 1703116800000, - 1703203200000 - ], - [ - "12:34:56.234567", - null - ], - [ - "12:34:57.234567", - "12:34:58.234567" - ], - [ - "23:12:36.765432+01", - null - ], - [ - "23:12:37.765432+01", - "23:12:38.765432+01" - ], - [ - "00:00:00.987654", - null - ], - [ - "00:00:00.887654", - "00:00:00.787654" - ] - ], - "nanos": [ - [ - 456000, - 0 - ], - [ - 456000, - 456000 - ], - [ - 321000, - 0 - ], - [ - 321000, - 321000 - ], - null, - null, - null, - null, - null, - null, - null, - null - ] - } - } - ] -} \ No newline at end of file diff --git a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.sql b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.sql deleted file mode 100644 index d81b3ca235b..00000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime_pgx.sql +++ /dev/null @@ -1,44 +0,0 @@ --- SELECT * FROM tbl --- test all date/time-based postgres data types -CREATE TEMPORARY TABLE tbl ( - ts timestamp, - tsnn timestamp NOT NULL, - tsz timestamp with time zone, - tsznn timestamp with time zone NOT NULL, - d date, - dnn date NOT NULL, - t time, - tnn time NOT NULL, - tz time with time zone, - tznn time with time zone NOT NULL, - i interval, - inn interval NOT NULL -); - -INSERT INTO tbl (ts, tsnn, tsz, tsznn, d, dnn, t, tnn, tz, tznn, i, inn) VALUES ( -'2023-11-15 05:06:07.123456', -'2023-11-15 05:06:08.123456', -'2021-07-22 13:22:33.654321 Europe/Berlin', -'2021-07-22 13:22:34.654321 Europe/Berlin', -'2023-12-20', -'2023-12-21', -'12:34:56.234567', -'12:34:57.234567', -'23:12:36.765432+1', -'23:12:37.765432+1', -'987654 microsecond', -'887654 microsecond' -), ( -NULL, -'2023-11-15 05:06:09.123456', -NULL, -'2021-07-22 13:22:35.654321 Europe/Berlin', -NULL, -'2023-12-22', -NULL, -'12:34:58.234567', -NULL, -'23:12:38.765432+1', -NULL, -'787654 microsecond' -); \ No newline at end of file diff --git a/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go b/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go index 306b1f43e81..144347ccef3 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go +++ b/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go @@ -3,48 +3,15 @@ package postgres import ( "fmt" "os" - "path/filepath" - "strconv" - "strings" - "sync" - "time" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" ) var validateCertFunc = validateCertFilePaths -var writeCertFileFunc = writeCertFile -type certFileType int - -const ( - rootCert = iota - clientCert - clientKey -) - -type tlsSettingsProvider interface { - getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) -} - -type datasourceCacheManager struct { - locker *locker - cache sync.Map -} - -type tlsManager struct { - logger log.Logger - dsCacheInstance datasourceCacheManager - dataPath string -} - -func newTLSManager(logger log.Logger, dataPath string) tlsSettingsProvider { - return &tlsManager{ - logger: logger, - dataPath: dataPath, - dsCacheInstance: datasourceCacheManager{locker: newLocker()}, - } +type TLSManager struct { + logger log.Logger } type tlsSettings struct { @@ -55,178 +22,122 @@ type tlsSettings struct { CertKeyFile string } -func (m *tlsManager) getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) { - tlsconfig := tlsSettings{ +func newTLSManager(logger log.Logger) *TLSManager { + return &TLSManager{ + logger: logger, + } +} + +// getTLSSettings retrieves TLS settings and handles certificate file creation if needed. +func (m *TLSManager) getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) { + tlsConfig := tlsSettings{ Mode: dsInfo.JsonData.Mode, } - isTLSDisabled := (tlsconfig.Mode == "disable") - - if isTLSDisabled { + if tlsConfig.Mode == "disable" { m.logger.Debug("Postgres TLS/SSL is disabled") - return tlsconfig, nil + return tlsConfig, nil } - m.logger.Debug("Postgres TLS/SSL is enabled", "tlsMode", tlsconfig.Mode) + tlsConfig.ConfigurationMethod = dsInfo.JsonData.ConfigurationMethod + tlsConfig.RootCertFile = dsInfo.JsonData.RootCertFile + tlsConfig.CertFile = dsInfo.JsonData.CertFile + tlsConfig.CertKeyFile = dsInfo.JsonData.CertKeyFile - tlsconfig.ConfigurationMethod = dsInfo.JsonData.ConfigurationMethod - tlsconfig.RootCertFile = dsInfo.JsonData.RootCertFile - tlsconfig.CertFile = dsInfo.JsonData.CertFile - tlsconfig.CertKeyFile = dsInfo.JsonData.CertKeyFile - - if tlsconfig.ConfigurationMethod == "file-content" { - if err := m.writeCertFiles(dsInfo, &tlsconfig); err != nil { - return tlsconfig, err + if tlsConfig.ConfigurationMethod == "file-content" { + if err := m.createCertFiles(dsInfo, &tlsConfig); err != nil { + return tlsConfig, fmt.Errorf("failed to create TLS certificate files: %w", err) } } else { - if err := validateCertFunc(tlsconfig.RootCertFile, tlsconfig.CertFile, tlsconfig.CertKeyFile); err != nil { - return tlsconfig, err + if err := validateCertFunc(tlsConfig.RootCertFile, tlsConfig.CertFile, tlsConfig.CertKeyFile); err != nil { + return tlsConfig, fmt.Errorf("invalid TLS certificate file paths: %w", err) } } - return tlsconfig, nil + + return tlsConfig, nil } -func (t certFileType) String() string { - switch t { - case rootCert: - return "root certificate" - case clientCert: - return "client certificate" - case clientKey: - return "client key" - default: - panic(fmt.Sprintf("Unrecognized certFileType %d", t)) - } -} +// createCertFiles writes certificate files to temporary locations. +func (m *TLSManager) createCertFiles(dsInfo sqleng.DataSourceInfo, tlsConfig *tlsSettings) error { + m.logger.Debug("Writing TLS certificate files to temporary locations") -func getFileName(dataDir string, fileType certFileType) string { - var filename string - switch fileType { - case rootCert: - filename = "root.crt" - case clientCert: - filename = "client.crt" - case clientKey: - filename = "client.key" - default: - panic(fmt.Sprintf("unrecognized certFileType %s", fileType.String())) - } - generatedFilePath := filepath.Join(dataDir, filename) - return generatedFilePath -} - -// writeCertFile writes a certificate file. -func writeCertFile(logger log.Logger, fileContent string, generatedFilePath string) error { - fileContent = strings.TrimSpace(fileContent) - if fileContent != "" { - logger.Debug("Writing cert file", "path", generatedFilePath) - if err := os.WriteFile(generatedFilePath, []byte(fileContent), 0600); err != nil { - return err - } - // Make sure the file has the permissions expected by the Postgresql driver, otherwise it will bail - if err := os.Chmod(generatedFilePath, 0600); err != nil { - return err - } - return nil - } - - logger.Debug("Deleting cert file since no content is provided", "path", generatedFilePath) - exists, err := fileExists(generatedFilePath) - if err != nil { + var err error + if tlsConfig.RootCertFile, err = m.writeCertFile("root-*.crt", dsInfo.DecryptedSecureJSONData["tlsCACert"]); err != nil { return err } - if exists { - if err := os.Remove(generatedFilePath); err != nil { - return fmt.Errorf("failed to remove %q: %w", generatedFilePath, err) - } + if tlsConfig.CertFile, err = m.writeCertFile("client-*.crt", dsInfo.DecryptedSecureJSONData["tlsClientCert"]); err != nil { + return err } + if tlsConfig.CertKeyFile, err = m.writeCertFile("client-*.key", dsInfo.DecryptedSecureJSONData["tlsClientKey"]); err != nil { + return err + } + return nil } -func (m *tlsManager) writeCertFiles(dsInfo sqleng.DataSourceInfo, tlsconfig *tlsSettings) error { - m.logger.Debug("Writing TLS certificate files to disk") - tlsRootCert := dsInfo.DecryptedSecureJSONData["tlsCACert"] - tlsClientCert := dsInfo.DecryptedSecureJSONData["tlsClientCert"] - tlsClientKey := dsInfo.DecryptedSecureJSONData["tlsClientKey"] - if tlsRootCert == "" && tlsClientCert == "" && tlsClientKey == "" { - m.logger.Debug("No TLS/SSL certificates provided") +// writeCertFile writes a single certificate file to a temporary location. +func (m *TLSManager) writeCertFile(pattern, content string) (string, error) { + if content == "" { + return "", nil } - // Calculate all files path - workDir := filepath.Join(m.dataPath, "tls", dsInfo.UID+"generatedTLSCerts") - tlsconfig.RootCertFile = getFileName(workDir, rootCert) - tlsconfig.CertFile = getFileName(workDir, clientCert) - tlsconfig.CertKeyFile = getFileName(workDir, clientKey) - - // Find datasource in the cache, if found, skip writing files - cacheKey := strconv.Itoa(int(dsInfo.ID)) - m.dsCacheInstance.locker.RLock(cacheKey) - item, ok := m.dsCacheInstance.cache.Load(cacheKey) - m.dsCacheInstance.locker.RUnlock(cacheKey) - if ok { - if !item.(time.Time).Before(dsInfo.Updated) { - return nil - } - } - - m.dsCacheInstance.locker.Lock(cacheKey) - defer m.dsCacheInstance.locker.Unlock(cacheKey) - - item, ok = m.dsCacheInstance.cache.Load(cacheKey) - if ok { - if !item.(time.Time).Before(dsInfo.Updated) { - return nil - } - } - - // Write certification directory and files - exists, err := fileExists(workDir) + m.logger.Debug("Writing certificate file", "pattern", pattern) + file, err := os.CreateTemp("", pattern) if err != nil { - return err + return "", fmt.Errorf("failed to create temporary file: %w", err) } - if !exists { - if err := os.MkdirAll(workDir, 0700); err != nil { - return err + defer func() { + if err := file.Close(); err != nil { + m.logger.Error("Failed to close file", "error", err) } + }() + + if _, err := file.WriteString(content); err != nil { + return "", fmt.Errorf("failed to write to temporary file: %w", err) } - if err = writeCertFileFunc(m.logger, tlsRootCert, tlsconfig.RootCertFile); err != nil { - return err - } - if err = writeCertFileFunc(m.logger, tlsClientCert, tlsconfig.CertFile); err != nil { - return err - } - if err = writeCertFileFunc(m.logger, tlsClientKey, tlsconfig.CertKeyFile); err != nil { - return err - } - - // we do not want to point to cert-files that do not exist - if tlsRootCert == "" { - tlsconfig.RootCertFile = "" - } - - if tlsClientCert == "" { - tlsconfig.CertFile = "" - } - - if tlsClientKey == "" { - tlsconfig.CertKeyFile = "" - } - - // Update datasource cache - m.dsCacheInstance.cache.Store(cacheKey, dsInfo.Updated) - return nil + return file.Name(), nil } -// validateCertFilePaths validates configured certificate file paths. -func validateCertFilePaths(rootCert, clientCert, clientKey string) error { - for _, fpath := range []string{rootCert, clientCert, clientKey} { - if fpath == "" { +// cleanupCertFiles removes temporary certificate files. +func (m *TLSManager) cleanupCertFiles(tlsConfig tlsSettings) { + // Only clean up if the configuration method is "file-content" + if tlsConfig.ConfigurationMethod != "file-content" { + m.logger.Debug("Skipping cleanup of TLS certificate files") + return + } + m.logger.Debug("Cleaning up TLS certificate files") + + files := []struct { + path string + name string + }{ + {tlsConfig.RootCertFile, "root certificate"}, + {tlsConfig.CertFile, "client certificate"}, + {tlsConfig.CertKeyFile, "client key"}, + } + + for _, file := range files { + if file.path == "" { continue } - exists, err := fileExists(fpath) + if err := os.Remove(file.path); err != nil { + m.logger.Error("Failed to remove file", "type", file.name, "path", file.path, "error", err) + } else { + m.logger.Debug("Successfully removed file", "type", file.name, "path", file.path) + } + } +} + +// validateCertFilePaths validates the existence of configured certificate file paths. +func validateCertFilePaths(rootCert, clientCert, clientKey string) error { + for _, path := range []string{rootCert, clientCert, clientKey} { + if path == "" { + continue + } + exists, err := fileExists(path) if err != nil { - return err + return fmt.Errorf("error checking file existence: %w", err) } if !exists { return sqleng.ErrCertFileNotExist @@ -235,15 +146,14 @@ func validateCertFilePaths(rootCert, clientCert, clientKey string) error { return nil } -// Exists determines whether a file/directory exists or not. -func fileExists(fpath string) (bool, error) { - _, err := os.Stat(fpath) +// fileExists checks if a file exists at the given path. +func fileExists(path string) (bool, error) { + _, err := os.Stat(path) if err != nil { - if !os.IsNotExist(err) { - return false, err + if os.IsNotExist(err) { + return false, nil } - return false, nil + return false, err } - return true, nil } diff --git a/pkg/tsdb/grafana-postgresql-datasource/tlsmanager_test.go b/pkg/tsdb/grafana-postgresql-datasource/tlsmanager_test.go index c8eda0a83a7..d288bc34b3e 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/tlsmanager_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/tlsmanager_test.go @@ -2,176 +2,21 @@ package postgres import ( "fmt" + "os" "path/filepath" - "strconv" "strings" - "sync" "testing" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - _ "github.com/lib/pq" ) -var writeCertFileCallNum int - -// TestDataSourceCacheManager is to test the Cache manager -func TestDataSourceCacheManager(t *testing.T) { - cfg := setting.NewCfg() - cfg.DataPath = t.TempDir() - mng := tlsManager{ - logger: backend.NewLoggerWith("logger", "tsdb.postgres"), - dsCacheInstance: datasourceCacheManager{locker: newLocker()}, - dataPath: cfg.DataPath, - } - jsonData := sqleng.JsonData{ - Mode: "verify-full", - ConfigurationMethod: "file-content", - } - secureJSONData := map[string]string{ - "tlsClientCert": "I am client certification", - "tlsClientKey": "I am client key", - "tlsCACert": "I am CA certification", - } - - updateTime := time.Now().Add(-5 * time.Minute) - - mockValidateCertFilePaths() - t.Cleanup(resetValidateCertFilePaths) - - t.Run("Check datasource cache creation", func(t *testing.T) { - var wg sync.WaitGroup - wg.Add(10) - for id := int64(1); id <= 10; id++ { - go func(id int64) { - ds := sqleng.DataSourceInfo{ - ID: id, - Updated: updateTime, - Database: "database", - JsonData: jsonData, - DecryptedSecureJSONData: secureJSONData, - UID: "testData", - } - s := tlsSettings{} - err := mng.writeCertFiles(ds, &s) - require.NoError(t, err) - wg.Done() - }(id) - } - wg.Wait() - - t.Run("check cache creation is succeed", func(t *testing.T) { - for id := int64(1); id <= 10; id++ { - updated, ok := mng.dsCacheInstance.cache.Load(strconv.Itoa(int(id))) - require.True(t, ok) - require.Equal(t, updateTime, updated) - } - }) - }) - - t.Run("Check datasource cache modification", func(t *testing.T) { - t.Run("check when version not changed, cache and files are not updated", func(t *testing.T) { - mockWriteCertFile() - t.Cleanup(resetWriteCertFile) - var wg1 sync.WaitGroup - wg1.Add(5) - for id := int64(1); id <= 5; id++ { - go func(id int64) { - ds := sqleng.DataSourceInfo{ - ID: 1, - Updated: updateTime, - Database: "database", - JsonData: jsonData, - DecryptedSecureJSONData: secureJSONData, - UID: "testData", - } - s := tlsSettings{} - err := mng.writeCertFiles(ds, &s) - require.NoError(t, err) - wg1.Done() - }(id) - } - wg1.Wait() - assert.Equal(t, writeCertFileCallNum, 0) - }) - - t.Run("cache is updated with the last datasource version", func(t *testing.T) { - dsV2 := sqleng.DataSourceInfo{ - ID: 1, - Updated: updateTime.Add(time.Minute), - Database: "database", - JsonData: jsonData, - DecryptedSecureJSONData: secureJSONData, - UID: "testData", - } - dsV3 := sqleng.DataSourceInfo{ - ID: 1, - Updated: updateTime.Add(2 * time.Minute), - Database: "database", - JsonData: jsonData, - DecryptedSecureJSONData: secureJSONData, - UID: "testData", - } - s := tlsSettings{} - err := mng.writeCertFiles(dsV2, &s) - require.NoError(t, err) - err = mng.writeCertFiles(dsV3, &s) - require.NoError(t, err) - version, ok := mng.dsCacheInstance.cache.Load("1") - require.True(t, ok) - require.Equal(t, updateTime.Add(2*time.Minute), version) - }) - }) -} - -// Test getFileName - -func TestGetFileName(t *testing.T) { - testCases := []struct { - desc string - datadir string - fileType certFileType - expErr string - expectedGeneratedPath string - }{ - { - desc: "Get File Name for root certification", - datadir: ".", - fileType: rootCert, - expectedGeneratedPath: "root.crt", - }, - { - desc: "Get File Name for client certification", - datadir: ".", - fileType: clientCert, - expectedGeneratedPath: "client.crt", - }, - { - desc: "Get File Name for client certification", - datadir: ".", - fileType: clientKey, - expectedGeneratedPath: "client.key", - }, - } - for _, tt := range testCases { - t.Run(tt.desc, func(t *testing.T) { - generatedPath := getFileName(tt.datadir, tt.fileType) - assert.Equal(t, tt.expectedGeneratedPath, generatedPath) - }) - } -} - // Test getTLSSettings. func TestGetTLSSettings(t *testing.T) { - cfg := setting.NewCfg() - cfg.DataPath = t.TempDir() - mockValidateCertFilePaths() t.Cleanup(resetValidateCertFilePaths) @@ -216,75 +61,13 @@ func TestGetTLSSettings(t *testing.T) { CertKeyFile: "i/am/coding/client.key", }, }, - { - desc: "Custom TLS mode verify-full with certificate files content", - updated: updatedTime.Add(2 * time.Minute), - uid: "xxx", - jsonData: sqleng.JsonData{ - Mode: "verify-full", - ConfigurationMethod: "file-content", - }, - secureJSONData: map[string]string{ - "tlsCACert": "I am CA certification", - "tlsClientCert": "I am client certification", - "tlsClientKey": "I am client key", - }, - tlsSettings: tlsSettings{ - Mode: "verify-full", - ConfigurationMethod: "file-content", - RootCertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "root.crt"), - CertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.crt"), - CertKeyFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.key"), - }, - }, - { - desc: "Custom TLS mode verify-ca with no client certificates with certificate files content", - updated: updatedTime.Add(3 * time.Minute), - uid: "xxx", - jsonData: sqleng.JsonData{ - Mode: "verify-ca", - ConfigurationMethod: "file-content", - }, - secureJSONData: map[string]string{ - "tlsCACert": "I am CA certification", - }, - tlsSettings: tlsSettings{ - Mode: "verify-ca", - ConfigurationMethod: "file-content", - RootCertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "root.crt"), - CertFile: "", - CertKeyFile: "", - }, - }, - { - desc: "Custom TLS mode require with client certificates and no root certificate with certificate files content", - updated: updatedTime.Add(4 * time.Minute), - uid: "xxx", - jsonData: sqleng.JsonData{ - Mode: "require", - ConfigurationMethod: "file-content", - }, - secureJSONData: map[string]string{ - "tlsClientCert": "I am client certification", - "tlsClientKey": "I am client key", - }, - tlsSettings: tlsSettings{ - Mode: "require", - ConfigurationMethod: "file-content", - RootCertFile: "", - CertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.crt"), - CertKeyFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.key"), - }, - }, } for _, tt := range testCases { t.Run(tt.desc, func(t *testing.T) { var settings tlsSettings var err error - mng := tlsManager{ - logger: backend.NewLoggerWith("logger", "tsdb.postgres"), - dsCacheInstance: datasourceCacheManager{locker: newLocker()}, - dataPath: cfg.DataPath, + mng := TLSManager{ + logger: backend.NewLoggerWith("logger", "tsdb.postgres"), } ds := sqleng.DataSourceInfo{ @@ -318,15 +101,145 @@ func resetValidateCertFilePaths() { validateCertFunc = validateCertFilePaths } -func mockWriteCertFile() { - writeCertFileCallNum = 0 - writeCertFileFunc = func(logger log.Logger, fileContent string, generatedFilePath string) error { - writeCertFileCallNum++ - return nil +func TestTLSManager_GetTLSSettings(t *testing.T) { + logger := log.New() + tlsManager := newTLSManager(logger) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "require", + ConfigurationMethod: "file-content", + }, + DecryptedSecureJSONData: map[string]string{ + "tlsCACert": "root-cert-content", + "tlsClientCert": "client-cert-content", + "tlsClientKey": "client-key-content", + }, } + + tlsConfig, err := tlsManager.getTLSSettings(dsInfo) + require.NoError(t, err) + assert.Equal(t, "require", tlsConfig.Mode) + assert.NotEmpty(t, tlsConfig.RootCertFile) + assert.NotEmpty(t, tlsConfig.CertFile) + assert.NotEmpty(t, tlsConfig.CertKeyFile) + + // Cleanup temporary files + tlsManager.cleanupCertFiles(tlsConfig) + assert.NoFileExists(t, tlsConfig.RootCertFile) + assert.NoFileExists(t, tlsConfig.CertFile) + assert.NoFileExists(t, tlsConfig.CertKeyFile) } -func resetWriteCertFile() { - writeCertFileCallNum = 0 - writeCertFileFunc = writeCertFile +func TestTLSManager_CleanupCertFiles_FilePath(t *testing.T) { + logger := log.New() + tlsManager := newTLSManager(logger) + + // Create temporary files for testing + rootCertFile, err := tlsManager.writeCertFile("root-*.crt", "root-cert-content") + require.NoError(t, err) + clientCertFile, err := tlsManager.writeCertFile("client-*.crt", "client-cert-content") + require.NoError(t, err) + clientKeyFile, err := tlsManager.writeCertFile("client-*.key", "client-key-content") + require.NoError(t, err) + + // Simulate a configuration where the method is "file-path" + tlsConfig := tlsSettings{ + ConfigurationMethod: "file-path", + RootCertFile: rootCertFile, + CertFile: clientCertFile, + CertKeyFile: clientKeyFile, + } + + // Call cleanupCertFiles + tlsManager.cleanupCertFiles(tlsConfig) + + // Verify the files are NOT deleted + assert.FileExists(t, rootCertFile, "Root certificate file should not be deleted") + assert.FileExists(t, clientCertFile, "Client certificate file should not be deleted") + assert.FileExists(t, clientKeyFile, "Client key file should not be deleted") + + // Cleanup the files manually + err = os.Remove(rootCertFile) + require.NoError(t, err) + err = os.Remove(clientCertFile) + require.NoError(t, err) + err = os.Remove(clientKeyFile) + require.NoError(t, err) +} + +func TestTLSManager_CreateCertFiles(t *testing.T) { + logger := log.New() + tlsManager := newTLSManager(logger) + + dsInfo := sqleng.DataSourceInfo{ + DecryptedSecureJSONData: map[string]string{ + "tlsCACert": "root-cert-content", + "tlsClientCert": "client-cert-content", + "tlsClientKey": "client-key-content", + }, + } + + tlsConfig := tlsSettings{ + ConfigurationMethod: "file-content", + } + err := tlsManager.createCertFiles(dsInfo, &tlsConfig) + require.NoError(t, err) + + assert.FileExists(t, tlsConfig.RootCertFile) + assert.FileExists(t, tlsConfig.CertFile) + assert.FileExists(t, tlsConfig.CertKeyFile) + + // Cleanup temporary files + tlsManager.cleanupCertFiles(tlsConfig) + assert.NoFileExists(t, tlsConfig.RootCertFile) + assert.NoFileExists(t, tlsConfig.CertFile) + assert.NoFileExists(t, tlsConfig.CertKeyFile) +} + +func TestTLSManager_WriteCertFile(t *testing.T) { + logger := log.New() + tlsManager := newTLSManager(logger) + + // Test writing a valid certificate file + filePath, err := tlsManager.writeCertFile("test-*.crt", "test-cert-content") + require.NoError(t, err) + assert.FileExists(t, filePath) + + content, err := os.ReadFile(filepath.Clean(filePath)) + require.NoError(t, err) + assert.Equal(t, "test-cert-content", string(content)) + + // Cleanup the file + err = os.Remove(filePath) + require.NoError(t, err) + assert.NoFileExists(t, filePath) +} + +func TestTLSManager_CleanupCertFiles(t *testing.T) { + logger := log.New() + tlsManager := newTLSManager(logger) + + // Create temporary files for testing + rootCertFile, err := tlsManager.writeCertFile("root-*.crt", "root-cert-content") + require.NoError(t, err) + clientCertFile, err := tlsManager.writeCertFile("client-*.crt", "client-cert-content") + require.NoError(t, err) + clientKeyFile, err := tlsManager.writeCertFile("client-*.key", "client-key-content") + require.NoError(t, err) + + tlsConfig := tlsSettings{ + ConfigurationMethod: "file-content", + RootCertFile: rootCertFile, + CertFile: clientCertFile, + CertKeyFile: clientKeyFile, + } + + // Cleanup the files + tlsManager.cleanupCertFiles(tlsConfig) + + // Verify the files are deleted + assert.NoFileExists(t, rootCertFile) + assert.NoFileExists(t, clientCertFile) + assert.NoFileExists(t, clientKeyFile) } diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx index 3a7ffccdbb7..ce937f18cf3 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx @@ -11,7 +11,6 @@ import { import { ConfigSection, ConfigSubSection, DataSourceDescription, EditorStack } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; import { - ConnectionLimits, Divider, MaxLifetimeField, MaxOpenConnectionsField, @@ -19,16 +18,16 @@ import { useMigrateDatabaseFields, } from '@grafana/sql'; import { - Input, - Select, - SecretInput, - Field, - Tooltip, - Label, - Icon, - Switch, - SecureSocksProxySettings, Collapse, + Field, + Icon, + Input, + Label, + SecretInput, + SecureSocksProxySettings, + Select, + Switch, + Tooltip, } from '@grafana/ui'; import { PostgresOptions, PostgresTLSMethods, PostgresTLSModes, SecureJsonData } from '../types'; @@ -412,18 +411,15 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps

- {config.featureToggles.postgresDSUsePGX ? ( - - - - - ) : ( - - )} + + + + + {config.secureSocksDSProxyEnabled && ( )} From bca58f5626c8e44a4f9e3497c80dbadb4e7e893b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Sencer=20=C3=96zcan?= <32759850+mustafasencer@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:02:14 +0100 Subject: [PATCH 071/378] feat: add folder tree validation for unified data migration (#114342) * fix: add validator * fix: revert startup index --- .../unified/migrations/resource_migration.go | 27 +- pkg/storage/unified/migrations/service.go | 34 +- pkg/storage/unified/migrations/validator.go | 371 +++++++++++++++--- 3 files changed, 349 insertions(+), 83 deletions(-) diff --git a/pkg/storage/unified/migrations/resource_migration.go b/pkg/storage/unified/migrations/resource_migration.go index cf67d220ec3..281ee1c85e3 100644 --- a/pkg/storage/unified/migrations/resource_migration.go +++ b/pkg/storage/unified/migrations/resource_migration.go @@ -18,6 +18,7 @@ import ( // ValidationFunc is a function that validates migration results. type Validator interface { + Name() string Validate(ctx context.Context, sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error } @@ -27,7 +28,7 @@ type ResourceMigration struct { migrator UnifiedMigrator resources []schema.GroupResource migrationID string - validator Validator // Optional: custom validation logic for this migration + validators []Validator // Optional: custom validation logic for this migration log log.Logger } @@ -36,13 +37,13 @@ func NewResourceMigration( migrator UnifiedMigrator, resources []schema.GroupResource, migrationID string, - validator Validator, + validators []Validator, ) *ResourceMigration { return &ResourceMigration{ migrator: migrator, resources: resources, migrationID: migrationID, - validator: validator, + validators: validators, log: log.New("storage.unified.resource_migration." + migrationID), } } @@ -123,20 +124,22 @@ func (m *ResourceMigration) migrateOrg(ctx context.Context, sess *xorm.Session, return nil } -// validateMigration calls the custom validation function if provided +// validateMigration runs all validators in sequence func (m *ResourceMigration) validateMigration(ctx context.Context, sess *xorm.Session, response *resourcepb.BulkResponse) error { - if m.validator == nil { - m.log.Debug("No validation function provided, skipping validation") + if len(m.validators) == 0 { + m.log.Debug("No validators provided, skipping validation") return nil } - return m.validator.Validate(ctx, sess, response, m.log) -} + for _, validator := range m.validators { + m.log.Debug("Running validator", "name", validator.Name(), "total", len(m.validators)) + if err := validator.Validate(ctx, sess, response, m.log); err != nil { + return fmt.Errorf("validator %s failed: %w", validator.Name(), err) + } + } -// LegacyTableInfo defines how to map a unified storage resource to its legacy table -type LegacyTableInfo struct { - Table string // Legacy table name (e.g., "dashboard", "playlist") - WhereClause string // WHERE clause template with org_id parameter (e.g., "org_id = ? and is_folder = false") + m.log.Debug("All validators passed", "count", len(m.validators)) + return nil } func ParseOrgIDFromNamespace(namespace string) (int64, error) { diff --git a/pkg/storage/unified/migrations/service.go b/pkg/storage/unified/migrations/service.go index 699be937d72..1a78f837066 100644 --- a/pkg/storage/unified/migrations/service.go +++ b/pkg/storage/unified/migrations/service.go @@ -81,8 +81,7 @@ func RegisterMigrations( } // Register resource migrations - // To add a new resource type, simply add another migration here with the appropriate resources - registerResourceMigrations(mg, migrator, client) + registerDashboardAndFolderMigration(mg, migrator, client) // Run all registered migrations (blocking) sec := cfg.Raw.Section("database") @@ -96,18 +95,31 @@ func RegisterMigrations( return nil } -func registerResourceMigrations(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) { +func registerDashboardAndFolderMigration(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) { + folders := schema.GroupResource{Group: "folder.grafana.app", Resource: "folders"} + dashboards := schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"} + + folderCountValidator := NewCountValidator( + client, + folders, + "dashboard", + "org_id = ? and is_folder = true", + ) + + dashboardCountValidator := NewCountValidator( + client, + dashboards, + "dashboard", + "org_id = ? and is_folder = false", + ) + + folderTreeValidator := NewFolderTreeValidator(client, folders) + dashboardsAndFolders := NewResourceMigration( migrator, - []schema.GroupResource{ - {Group: "folder.grafana.app", Resource: "folders"}, - {Group: "dashboard.grafana.app", Resource: "dashboards"}, - }, + []schema.GroupResource{folders, dashboards}, "folders-dashboards", - NewCountValidator(client, map[string]LegacyTableInfo{ - "folder.grafana.app/folders": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = true"}, - "dashboard.grafana.app/dashboards": {Table: "dashboard", WhereClause: "org_id = ? and is_folder = false"}, - }), + []Validator{folderCountValidator, dashboardCountValidator, folderTreeValidator}, ) mg.AddMigration("folders and dashboards migration", dashboardsAndFolders) } diff --git a/pkg/storage/unified/migrations/validator.go b/pkg/storage/unified/migrations/validator.go index 6deef77bb6d..7a973976402 100644 --- a/pkg/storage/unified/migrations/validator.go +++ b/pkg/storage/unified/migrations/validator.go @@ -7,18 +7,80 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util/xorm" + "k8s.io/apimachinery/pkg/runtime/schema" ) -type CountValidator struct { - client resourcepb.ResourceIndexClient - legacyTableMap map[string]LegacyTableInfo +func filterResponse(response *resourcepb.BulkResponse, resources []schema.GroupResource) *resourcepb.BulkResponse { + if len(resources) == 0 { + return response + } + + // Create a resource lookup map for efficient filtering + resourceMap := make(map[string]bool) + for _, res := range resources { + key := fmt.Sprintf("%s/%s", res.Group, res.Resource) + resourceMap[key] = true + } + + // Filter summaries to only include matching resources + var filteredSummaries []*resourcepb.BulkResponse_Summary + for _, summary := range response.Summary { + key := fmt.Sprintf("%s/%s", summary.Group, summary.Resource) + if resourceMap[key] { + filteredSummaries = append(filteredSummaries, summary) + } + } + + // Filter rejected items to only include matching resources + var filteredRejected []*resourcepb.BulkResponse_Rejected + for _, rejected := range response.Rejected { + if rejected.Key != nil { + key := fmt.Sprintf("%s/%s", rejected.Key.Group, rejected.Key.Resource) + if resourceMap[key] { + filteredRejected = append(filteredRejected, rejected) + } + } + } + + // Create filtered response, preserving original processed count for percentage calculations + return &resourcepb.BulkResponse{ + Error: response.Error, + Processed: response.Processed, + Summary: filteredSummaries, + Rejected: filteredRejected, + } } -func NewCountValidator(client resourcepb.ResourceIndexClient, legacyTableMap map[string]LegacyTableInfo) Validator { - return &CountValidator{client: client, legacyTableMap: legacyTableMap} +type CountValidator struct { + name string + client resourcepb.ResourceIndexClient + resource schema.GroupResource + table string + whereClause string +} + +func NewCountValidator( + client resourcepb.ResourceIndexClient, + resource schema.GroupResource, + table string, + whereClause string, +) Validator { + return &CountValidator{ + name: "CountValidator", + client: client, + resource: resource, + table: table, + whereClause: whereClause, + } +} + +func (v *CountValidator) Name() string { + return v.name } func (v *CountValidator) Validate(ctx context.Context, sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error { + // Filter response to only include the configured resource + response = filterResponse(response, []schema.GroupResource{v.resource}) if len(response.Rejected) > 0 { log.Warn("Migration had rejected items", "count", len(response.Rejected)) for i, rejected := range response.Rejected { @@ -34,66 +96,255 @@ func (v *CountValidator) Validate(ctx context.Context, sess *xorm.Session, respo // Rejections are not fatal - they may be expected for invalid data } - // Validate counts for each resource type - for _, summary := range response.Summary { - key := fmt.Sprintf("%s/%s", summary.Group, summary.Resource) - tableInfo, ok := v.legacyTableMap[key] - if !ok { - log.Debug("No legacy table mapping for resource, skipping count validation", - "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), - "namespace", summary.Namespace) - continue + // Should have at most one summary after filtering + if len(response.Summary) == 0 { + log.Debug("No summaries found for resource, skipping count validation", + "resource", fmt.Sprintf("%s.%s", v.resource.Resource, v.resource.Group)) + return nil + } + + if len(response.Summary) > 1 { + return fmt.Errorf("expected at most 1 summary after filtering, got %d", len(response.Summary)) + } + + summary := response.Summary[0] + + // Get legacy count from database + orgID, err := ParseOrgIDFromNamespace(summary.Namespace) + if err != nil { + return fmt.Errorf("invalid namespace %s: %w", summary.Namespace, err) + } + + legacyCount, err := sess.Table(v.table).Where(v.whereClause, orgID).Count() + if err != nil { + return fmt.Errorf("failed to count %s: %w", v.table, err) + } + + // Get unified storage count using GetStats API + statsResp, err := v.client.GetStats(ctx, &resourcepb.ResourceStatsRequest{ + Namespace: summary.Namespace, + Kinds: []string{fmt.Sprintf("%s/%s", summary.Group, summary.Resource)}, + }) + if err != nil { + return fmt.Errorf("failed to get stats for %s/%s in namespace %s: %w", + summary.Group, summary.Resource, summary.Namespace, err) + } + + // Find the count for this specific resource type + var unifiedCount int64 + for _, stat := range statsResp.Stats { + if stat.Group == summary.Group && stat.Resource == summary.Resource { + unifiedCount = stat.Count + break } + } - // Get legacy count from database - orgID, err := ParseOrgIDFromNamespace(summary.Namespace) - if err != nil { - return fmt.Errorf("invalid namespace %s: %w", summary.Namespace, err) - } + // Account for rejected items in validation + expectedCount := unifiedCount + int64(len(response.Rejected)) - legacyCount, err := sess.Table(tableInfo.Table).Where(tableInfo.WhereClause, orgID).Count() - if err != nil { - return fmt.Errorf("failed to count %s: %w", tableInfo.Table, err) - } + log.Info("Count validation", + "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), + "namespace", summary.Namespace, + "legacy_count", legacyCount, + "unified_count", unifiedCount, + "migration_summary_count", summary.Count, + "rejected", len(response.Rejected), + "history", summary.History) - // Get unified storage count using GetStats API - statsResp, err := v.client.GetStats(ctx, &resourcepb.ResourceStatsRequest{ - Namespace: summary.Namespace, - Kinds: []string{fmt.Sprintf("%s/%s", summary.Group, summary.Resource)}, - }) - if err != nil { - return fmt.Errorf("failed to get stats for %s/%s in namespace %s: %w", - summary.Group, summary.Resource, summary.Namespace, err) - } - - // Find the count for this specific resource type - var unifiedCount int64 - for _, stat := range statsResp.Stats { - if stat.Group == summary.Group && stat.Resource == summary.Resource { - unifiedCount = stat.Count - break - } - } - - // Account for rejected items in validation - expectedCount := unifiedCount + int64(len(response.Rejected)) - - log.Info("Count validation", - "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), - "namespace", summary.Namespace, - "legacy_count", legacyCount, - "unified_count", unifiedCount, - "migration_summary_count", summary.Count, - "rejected", len(response.Rejected), - "history", summary.History) - - // Validate that we migrated all items (allowing for rejected items) - if legacyCount > expectedCount { - return fmt.Errorf("count mismatch for %s.%s in namespace %s: legacy has %d, unified has %d, rejected %d", - summary.Resource, summary.Group, summary.Namespace, - legacyCount, unifiedCount, len(response.Rejected)) - } + // Validate that we migrated all items (allowing for rejected items) + if legacyCount > expectedCount { + return fmt.Errorf("count mismatch for %s.%s in namespace %s: legacy has %d, unified has %d, rejected %d", + summary.Resource, summary.Group, summary.Namespace, + legacyCount, unifiedCount, len(response.Rejected)) } return nil } + +type FolderTreeValidator struct { + name string + client resourcepb.ResourceIndexClient + resource schema.GroupResource +} + +func NewFolderTreeValidator( + client resourcepb.ResourceIndexClient, + resource schema.GroupResource, +) Validator { + return &FolderTreeValidator{ + name: "FolderTreeValidator", + client: client, + resource: resource, + } +} + +type legacyFolder struct { + ID int64 `xorm:"id"` + UID string `xorm:"uid"` + FolderUID string `xorm:"folder_uid"` + Title string `xorm:"title"` +} + +func (v *FolderTreeValidator) Name() string { + return v.name +} + +func (v *FolderTreeValidator) Validate(ctx context.Context, sess *xorm.Session, response *resourcepb.BulkResponse, log log.Logger) error { + // Filter response to only include the configured resource (folders) + response = filterResponse(response, []schema.GroupResource{v.resource}) + + // Should have at most one summary after filtering + if len(response.Summary) == 0 { + log.Debug("No summaries found for folders, skipping folder tree validation") + return nil + } + + if len(response.Summary) > 1 { + return fmt.Errorf("expected at most 1 summary after filtering, got %d", len(response.Summary)) + } + + summary := response.Summary[0] + + // Get orgID from namespace + orgID, err := ParseOrgIDFromNamespace(summary.Namespace) + if err != nil { + return fmt.Errorf("invalid namespace %s: %w", summary.Namespace, err) + } + + // Build legacy folder parent map + legacyParentMap, err := v.buildLegacyFolderParentMap(sess, orgID, log) + if err != nil { + return fmt.Errorf("failed to build legacy folder parent map: %w", err) + } + + // Build unified storage folder parent map + unifiedParentMap, err := v.buildUnifiedFolderParentMap(ctx, summary.Namespace, log) + if err != nil { + return fmt.Errorf("failed to build unified folder parent map: %w", err) + } + + // Compare the two maps + mismatches := []string{} + for uid, legacyParent := range legacyParentMap { + unifiedParent, exists := unifiedParentMap[uid] + if !exists { + // Folder exists in legacy but not in unified - might be rejected, skip + log.Debug("Folder exists in legacy but not in unified storage", + "uid", uid, + "legacy_parent", legacyParent) + continue + } + + if legacyParent != unifiedParent { + mismatch := fmt.Sprintf("folder %s: legacy parent=%s, unified parent=%s", + uid, legacyParent, unifiedParent) + mismatches = append(mismatches, mismatch) + log.Warn("Folder parent mismatch", + "uid", uid, + "legacy_parent", legacyParent, + "unified_parent", unifiedParent) + } + } + + // Check for folders in unified but not in legacy (shouldn't happen) + for uid := range unifiedParentMap { + if _, exists := legacyParentMap[uid]; !exists { + mismatch := fmt.Sprintf("folder %s exists in unified but not in legacy", uid) + mismatches = append(mismatches, mismatch) + log.Warn("Folder exists in unified but not in legacy", "uid", uid) + } + } + + if len(mismatches) > 0 { + log.Error("Folder tree structure validation failed", + "mismatch_count", len(mismatches), + "total_legacy_folders", len(legacyParentMap), + "total_unified_folders", len(unifiedParentMap)) + return fmt.Errorf("folder tree structure mismatch: %d folders have incorrect parents", len(mismatches)) + } + + log.Info("Folder tree structure validation passed", + "folder_count", len(legacyParentMap), + "namespace", summary.Namespace) + + return nil +} + +func (v *FolderTreeValidator) buildLegacyFolderParentMap(sess *xorm.Session, orgID int64, log log.Logger) (map[string]string, error) { + // Query all folders for this org + var folders []legacyFolder + err := sess.Table("dashboard"). + Cols("id", "uid", "folder_uid", "title"). + Where("org_id = ? AND is_folder = ?", orgID, true). + Find(&folders) + if err != nil { + return nil, fmt.Errorf("failed to query legacy folders: %w", err) + } + + parentMap := make(map[string]string) + for _, folder := range folders { + parentMap[folder.UID] = folder.FolderUID + } + + if len(parentMap) == 0 { + log.Debug("No legacy folders found for org", "org_id", orgID) + return make(map[string]string), nil + } + + log.Debug("Built legacy folder parent map", + "folder_count", len(parentMap), + "org_id", orgID) + + return parentMap, nil +} + +func (v *FolderTreeValidator) buildUnifiedFolderParentMap(ctx context.Context, namespace string, log log.Logger) (map[string]string, error) { + // Search for all folders in this namespace + searchResp, err := v.client.Search(ctx, &resourcepb.ResourceSearchRequest{ + Options: &resourcepb.ListOptions{ + Key: &resourcepb.ResourceKey{ + Namespace: namespace, + Group: v.resource.Group, + Resource: v.resource.Resource, + }, + }, + Limit: 100000, // Large limit to get all folders + }) + if err != nil { + return nil, fmt.Errorf("failed to search folders in unified storage: %w", err) + } + + if searchResp.Results == nil { + return make(map[string]string), nil + } + + parentMap := make(map[string]string) + for _, row := range searchResp.Results.Rows { + if row.Key == nil { + continue + } + + folderUID := row.Key.Name + parentUID := "" + + folderColIdx := -1 + for i, col := range searchResp.Results.Columns { + if col.Name == "folder" { + folderColIdx = i + break + } + } + + if folderColIdx >= 0 && folderColIdx < len(row.Cells) { + parentUID = string(row.Cells[folderColIdx]) + } + + parentMap[folderUID] = parentUID + } + + log.Debug("Built unified folder parent map", + "folder_count", len(parentMap), + "namespace", namespace) + + return parentMap, nil +} From 4e228cab002d5d0f8feb4e2fe469af3c445fb261 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Mon, 24 Nov 2025 11:15:03 +0000 Subject: [PATCH 072/378] Trace View: UI improvements (#114156) * Fix unclear child span relationship * Trace view UI improvements * Fix tests --- .../TraceTimelineViewer/SpanDetail/index.tsx | 2 +- .../TraceTimelineViewer/SpanDetailRow.tsx | 3 ++ .../SpanTreeOffset.test.tsx | 20 ++++++---- .../TraceTimelineViewer/SpanTreeOffset.tsx | 37 ++++++++++--------- .../components/common/LabeledList.tsx | 7 ++-- 5 files changed, 40 insertions(+), 29 deletions(-) diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx index 238002e7975..3c14fb0c43e 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx @@ -157,7 +157,7 @@ const getStyles = (theme: GrafanaTheme2) => { label: 'ServiceNameAndLinks', display: 'flex', width: '100%', - marginBottom: '16px', + marginBottom: theme.spacing(1), }), operationName: css({ label: 'SpanDetailOperationName', diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx index 7312daaccab..bcc837b4b7b 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx @@ -77,6 +77,8 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => { label: 'detailWrapper', flex: '1', minWidth: 0, + backgroundColor: theme.colors.background.canvas, + border: `1px solid ${theme.colors.border.weak}`, }), }; }); @@ -165,6 +167,7 @@ const UnthemedSpanDetailRow = React.memo((props) => { addHoverIndentGuideId={addHoverIndentGuideId} removeHoverIndentGuideId={removeHoverIndentGuideId} visibleSpanIds={visibleSpanIds} + removeLastIndentGuide={true} />

diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.test.tsx index e9c0ad9f9ad..5f52ac216e4 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.test.tsx @@ -47,11 +47,12 @@ describe('SpanTreeOffset', () => { }); describe('.SpanTreeOffset--indentGuide', () => { - it('renders no indentGuide if span has no ancestors and no children', () => { + it('renders only the trace-level indentGuide if span has no ancestors and no children', () => { jest.mocked(spanAncestorIdsSpy).mockReturnValue([]); render(); const indentGuides = screen.queryAllByTestId('SpanTreeOffset--indentGuide'); - expect(indentGuides.length).toBe(0); + expect(indentGuides.length).toBe(1); + expect(indentGuides[0]).toHaveAttribute('data-ancestor-id', specialRootID); }); it('renders only one SpanTreeOffset--indentGuide for entire trace if span has no ancestors but has children', () => { @@ -66,9 +67,10 @@ describe('SpanTreeOffset', () => { it('renders one SpanTreeOffset--indentGuide per ancestor span when span has no children', () => { render(); const indentGuides = screen.getAllByTestId('SpanTreeOffset--indentGuide'); - expect(indentGuides.length).toBe(2); + expect(indentGuides.length).toBe(3); expect(indentGuides[0]).toHaveAttribute('data-ancestor-id', specialRootID); expect(indentGuides[1]).toHaveAttribute('data-ancestor-id', rootSpanID); + expect(indentGuides[2]).toHaveAttribute('data-ancestor-id', parentSpanID); }); it('renders one SpanTreeOffset--indentGuide per ancestor span, plus one for entire trace when span has children', () => { @@ -112,16 +114,20 @@ describe('SpanTreeOffset', () => { props = { ...props, span: { ...props.span, hasChildren: true } }; }); - it('does not render icon if props.span.hasChildren is false', () => { + it('renders placeholder content when props.span.hasChildren is false', () => { props.span.hasChildren = false; render(); - expect(screen.queryByTestId('icon-wrapper')).not.toBeInTheDocument(); + expect(screen.queryByTestId('icon-arrow-right')).not.toBeInTheDocument(); + expect(screen.queryByTestId('icon-arrow-down')).not.toBeInTheDocument(); + expect(screen.getByTestId('icon-wrapper')).toHaveTextContent('-'); }); - it('does not render icon if props.span.hasChildren is true and showChildrenIcon is false', () => { + it('renders placeholder content when props.span.hasChildren is true but showChildrenIcon is false', () => { props.showChildrenIcon = false; render(); - expect(screen.queryByTestId('icon-wrapper')).not.toBeInTheDocument(); + expect(screen.queryByTestId('icon-arrow-right')).not.toBeInTheDocument(); + expect(screen.queryByTestId('icon-arrow-down')).not.toBeInTheDocument(); + expect(screen.getByTestId('icon-wrapper')).toHaveTextContent('-'); }); it('renders arrow-right if props.span.hasChildren is true and props.childrenVisible is false', () => { diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx index 1d2bea3105e..e2abaff8a70 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx @@ -66,6 +66,8 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2) => ({ right: 0, height: '100%', paddingTop: '1px', + width: '1rem', + textAlign: 'center', }), })); @@ -80,6 +82,7 @@ export type TProps = { removeHoverIndentGuideId: (spanID: string) => void; theme: GrafanaTheme2; visibleSpanIds: string[]; + removeLastIndentGuide?: boolean; }; const UnthemedSpanTreeOffset = React.memo((props) => { @@ -93,6 +96,7 @@ const UnthemedSpanTreeOffset = React.memo((props) => { hoverIndentGuideIds, addHoverIndentGuideId, removeHoverIndentGuideId, + removeLastIndentGuide = false, } = props; const ancestorIds = React.useMemo(() => { @@ -101,8 +105,12 @@ const UnthemedSpanTreeOffset = React.memo((props) => { // necessary padding for the collapse icon on root-level spans. ids.push('root'); ids.reverse(); + + if (removeLastIndentGuide) { + ids.pop(); + } return ids; - }, [span]); + }, [span, removeLastIndentGuide]); /** * If the mouse leaves to anywhere except another span with the same ancestor id, this span's ancestor id is @@ -156,20 +164,15 @@ const UnthemedSpanTreeOffset = React.memo((props) => { )); const styles = getStyles(theme); - // If span has no children, don't show the last indent guide - const displayedAncestorIds = hasChildren ? ancestorIds : ancestorIds.slice(0, -1); - return ( - {displayedAncestorIds.map((ancestorId, index) => ( + {ancestorIds.map((ancestorId, index) => ( ((props) => { onMouseLeave={(event) => handleMouseLeave(event, ancestorId)} /> ))} - {icon && ( - handleMouseEnter(event, spanID)} - onMouseLeave={(event) => handleMouseLeave(event, spanID)} - data-testid="icon-wrapper" - > - {icon} - - )} + icon && handleMouseEnter(event, spanID)} + onMouseLeave={(event) => icon && handleMouseLeave(event, spanID)} + data-testid="icon-wrapper" + > + {icon || (!removeLastIndentGuide && '-')} + ); }); diff --git a/public/app/features/explore/TraceView/components/common/LabeledList.tsx b/public/app/features/explore/TraceView/components/common/LabeledList.tsx index 1c09a2dda25..925579a1bcf 100644 --- a/public/app/features/explore/TraceView/components/common/LabeledList.tsx +++ b/public/app/features/explore/TraceView/components/common/LabeledList.tsx @@ -28,6 +28,7 @@ const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => { listStyle: 'none', margin: 0, padding: 0, + fontSize: theme.typography.size.sm, ...(divider ? { marginRight: '-8px', @@ -49,7 +50,7 @@ const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => { }), LabeledListLabel: css({ label: 'LabeledListLabel', - color: theme.isLight ? '#999' : '#666', + color: theme.colors.text.secondary, marginRight: '0.25rem', }), LabeledListValue: css({ @@ -66,7 +67,7 @@ const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => { LabeledListServiceLine: css({ label: 'LabeledListServiceLine', display: 'inline-block', - width: '1.25rem', + width: '1rem', height: '0.35rem', marginRight: '0.5rem', verticalAlign: 'middle', @@ -95,7 +96,7 @@ export default function LabeledList(props: LabeledListProps) { {label === 'Service:' && (
)} - {icon && } + {icon && } {label} {value} From b5335d9357ef17d14213700ea652f64026069c2f Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Mon, 24 Nov 2025 12:38:44 +0100 Subject: [PATCH 073/378] Alerting: Remove dependency on yaml/v2 package (#114348) --- go.mod | 2 +- pkg/services/ngalert/sender/notifier_test.go | 34 +++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index daab4218c91..576f6c0614f 100644 --- a/go.mod +++ b/go.mod @@ -197,7 +197,6 @@ require ( go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage go.uber.org/mock v0.6.0 // @grafana/grafana-operator-experience-squad go.uber.org/zap v1.27.0 // @grafana/identity-access-team - go.yaml.in/yaml/v2 v2.4.3 // @grafana/alerting-backend go.yaml.in/yaml/v3 v3.0.4 // @grafana/alerting-backend gocloud.dev v0.43.0 // @grafana/grafana-app-platform-squad gocloud.dev/secrets/hashivault v0.43.0 // @grafana/grafana-operator-experience-squad @@ -628,6 +627,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect diff --git a/pkg/services/ngalert/sender/notifier_test.go b/pkg/services/ngalert/sender/notifier_test.go index e7e9aedec96..a8b452437c1 100644 --- a/pkg/services/ngalert/sender/notifier_test.go +++ b/pkg/services/ngalert/sender/notifier_test.go @@ -28,6 +28,7 @@ import ( "net/http/httptest" "net/url" "strconv" + "strings" "testing" "time" @@ -38,7 +39,7 @@ import ( "github.com/prometheus/common/promslog" "github.com/stretchr/testify/require" "go.uber.org/atomic" - "go.yaml.in/yaml/v2" + "go.yaml.in/yaml/v3" "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/discovery" @@ -660,11 +661,10 @@ alerting: alertmanagers: - static_configs: ` - err := yaml.UnmarshalStrict([]byte(s), cfg) - require.NoError(t, err, "Unable to load YAML config.") + mustStrictlyDecodeConfig(t, strings.NewReader(s), cfg) require.Len(t, cfg.AlertingConfig.AlertmanagerConfigs, 1) - err = n.ApplyConfig(cfg, nil) + err := n.ApplyConfig(cfg, nil) require.NoError(t, err, "Error applying the config.") tgs := make(map[string][]*targetgroup.Group) @@ -711,11 +711,10 @@ alerting: regex: 'alertmanager:9093' action: drop ` - err := yaml.UnmarshalStrict([]byte(s), cfg) - require.NoError(t, err, "Unable to load YAML config.") + mustStrictlyDecodeConfig(t, strings.NewReader(s), cfg) require.Len(t, cfg.AlertingConfig.AlertmanagerConfigs, 1) - err = n.ApplyConfig(cfg, nil) + err := n.ApplyConfig(cfg, nil) require.NoError(t, err, "Error applying the config.") tgs := make(map[string][]*targetgroup.Group) @@ -1091,7 +1090,7 @@ alerting: - foo.json ` // 1. Ensure known alertmanagers are not dropped during ApplyConfig. - require.NoError(t, yaml.UnmarshalStrict([]byte(s), cfg)) + mustStrictlyDecodeConfig(t, strings.NewReader(s), cfg) require.Len(t, cfg.AlertingConfig.AlertmanagerConfigs, 1) // First, apply the config and reload. @@ -1117,7 +1116,7 @@ alerting: - files: - foo.json ` - require.NoError(t, yaml.UnmarshalStrict([]byte(s), cfg)) + mustStrictlyDecodeConfig(t, strings.NewReader(s), cfg) require.Len(t, cfg.AlertingConfig.AlertmanagerConfigs, 2) require.NoError(t, n.ApplyConfig(cfg, nil)) @@ -1140,7 +1139,7 @@ alerting: - files: - foo.json ` - require.NoError(t, yaml.UnmarshalStrict([]byte(s), cfg)) + mustStrictlyDecodeConfig(t, strings.NewReader(s), cfg) require.Len(t, cfg.AlertingConfig.AlertmanagerConfigs, 2) require.NoError(t, n.ApplyConfig(cfg, nil)) @@ -1167,9 +1166,22 @@ alerting: regex: 'doesntmatter:1234' action: drop ` - require.NoError(t, yaml.UnmarshalStrict([]byte(s), cfg)) + mustStrictlyDecodeConfig(t, strings.NewReader(s), cfg) require.Len(t, cfg.AlertingConfig.AlertmanagerConfigs, 2) require.NoError(t, n.ApplyConfig(cfg, nil)) require.Empty(t, n.Alertmanagers()) } + +// Maintain strict yaml decode behavior from v2: https://github.com/go-yaml/yaml/issues/639#issuecomment-666935833 +func mustStrictlyDecodeConfig(t testing.TB, r io.Reader, cfg *config.Config) { + t.Helper() + + dec := yaml.NewDecoder(r) + dec.KnownFields(true) + + err := dec.Decode(cfg) + if err != nil { + require.Equal(t, io.EOF, err, "Unable to load YAML config.") + } +} From 01a4a5409a058596ff371a6e97aec5633c8c27a8 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Mon, 24 Nov 2025 12:42:09 +0100 Subject: [PATCH 074/378] Alerting: Add alertingUIUseFullyCompatBackendFilters feature toggle (#114153) * Add alertingUIUseFullyCompatBackendFilters for fully compatible backend filters * Remove hide from admin flag --- .../src/types/featureToggles.gen.ts | 4 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 29 +++ .../alerting/unified/featureToggles.ts | 3 + .../rule-list/hooks/grafanaFilter.test.ts | 228 ++++++++++++++++++ .../unified/rule-list/hooks/grafanaFilter.ts | 26 +- 8 files changed, 289 insertions(+), 13 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 35d876518b9..b02be15bfc2 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -329,6 +329,10 @@ export interface FeatureToggles { */ alertingUIUseBackendFilters?: boolean; /** + * Enables the UI to use rules backend-side filters 100% compatible with the frontend filters + */ + alertingUIUseFullyCompatBackendFilters?: boolean; + /** * Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. */ alertmanagerRemotePrimary?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index ba7abae5a6d..de024e8f763 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -527,6 +527,13 @@ var ( Owner: grafanaAlertingSquad, HideFromDocs: true, }, + { + Name: "alertingUIUseFullyCompatBackendFilters", + Description: "Enables the UI to use rules backend-side filters 100% compatible with the frontend filters", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromDocs: true, + }, { Name: "alertmanagerRemotePrimary", Description: "Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index cd3222235fa..076256f039d 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -73,6 +73,7 @@ cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-e alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false alertingProvenanceLockWrites,experimental,@grafana/alerting-squad,false,false,false alertingUIUseBackendFilters,experimental,@grafana/alerting-squad,false,false,false +alertingUIUseFullyCompatBackendFilters,experimental,@grafana/alerting-squad,false,false,false alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 284ce16e739..8d940cda9f1 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -243,6 +243,10 @@ const ( // Enables the UI to use certain backend-side filters FlagAlertingUIUseBackendFilters = "alertingUIUseBackendFilters" + // FlagAlertingUIUseFullyCompatBackendFilters + // Enables the UI to use rules backend-side filters 100% compatible with the frontend filters + FlagAlertingUIUseFullyCompatBackendFilters = "alertingUIUseFullyCompatBackendFilters" + // FlagAlertmanagerRemotePrimary // Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. FlagAlertmanagerRemotePrimary = "alertmanagerRemotePrimary" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index e791d3bd89b..8561f371fe0 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -540,6 +540,35 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "alertingUIUseCompatibleBackendFilters", + "resourceVersion": "1763478911684", + "creationTimestamp": "2025-11-18T15:15:11Z", + "deletionTimestamp": "2025-11-18T15:17:12Z" + }, + "spec": { + "description": "Enables the UI to use rules backend-side filters 100% compatible with the frontend filters", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, + { + "metadata": { + "name": "alertingUIUseFullyCompatBackendFilters", + "resourceVersion": "1763479032988", + "creationTimestamp": "2025-11-18T15:17:12Z" + }, + "spec": { + "description": "Enables the UI to use rules backend-side filters 100% compatible with the frontend filters", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "alertingUseNewSimplifiedRoutingHashAlgorithm", diff --git a/public/app/features/alerting/unified/featureToggles.ts b/public/app/features/alerting/unified/featureToggles.ts index 46b202ac7f8..7ca1747a8c5 100644 --- a/public/app/features/alerting/unified/featureToggles.ts +++ b/public/app/features/alerting/unified/featureToggles.ts @@ -23,3 +23,6 @@ export const shouldAllowPermanentlyDeletingRules = () => (shouldAllowRecoveringDeletedRules() && config.featureToggles.alertingRulePermanentlyDelete) ?? false; export const shouldUseBackendFilters = () => config.featureToggles.alertingUIUseBackendFilters ?? false; + +export const shouldUseFullyCompatibleBackendFilters = () => + config.featureToggles.alertingUIUseFullyCompatBackendFilters ?? false; diff --git a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts index 3825c905a52..56be25ee248 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts @@ -484,6 +484,190 @@ describe('grafana-managed rules', () => { expect(frontendFilter2.groupMatches(group)).toBe(false); }); }); + + describe('when alertingUIUseFullyCompatBackendFilters is enabled', () => { + testWithFeatureToggles({ enable: ['alertingUIUseFullyCompatBackendFilters'] }); + + it('should populate backend filters correctly (ruleType, dashboardUid)', () => { + // Fully compatible filters should be in backend + const { backendFilter } = getGrafanaFilter( + getFilter({ + ruleType: PromRuleType.Alerting, + dashboardUid: 'dashboard-123', + freeFormWords: ['cpu'], + groupName: 'my-group', + }) + ); + + expect(backendFilter.type).toBe(PromRuleType.Alerting); + expect(backendFilter.dashboardUid).toBe('dashboard-123'); + + // Non-compatible filters should NOT be in backend + expect(backendFilter.title).toBeUndefined(); + expect(backendFilter.searchGroupName).toBeUndefined(); + + // Empty state + const { backendFilter: emptyFilter } = getGrafanaFilter(getFilter({})); + expect(emptyFilter.type).toBeUndefined(); + expect(emptyFilter.dashboardUid).toBeUndefined(); + }); + + it('should apply frontend filters correctly', () => { + const alertingRule = mockGrafanaPromAlertingRule({ + name: 'High CPU Usage', + labels: { severity: 'critical' }, + queriedDatasourceUIDs: ['datasource-uid-1'], + annotations: { [Annotation.dashboardUID]: 'dashboard-a' }, + alerts: [], + }); + const recordingRule = mockPromRecordingRule({ name: 'Test Recording' }); + + // Backend-handled filters (ruleType, dashboardUid) should skip frontend filtering + const { frontendFilter: backendHandledFilter } = getGrafanaFilter( + getFilter({ ruleType: PromRuleType.Recording, dashboardUid: 'dashboard-b' }) + ); + expect(backendHandledFilter.ruleMatches(alertingRule)).toBe(true); + expect(backendHandledFilter.ruleMatches(recordingRule)).toBe(true); + + // Frontend-handled filters (freeFormWords, ruleName) should work + const { frontendFilter: freeFormMatch } = getGrafanaFilter(getFilter({ freeFormWords: ['cpu'] })); + expect(freeFormMatch.ruleMatches(alertingRule)).toBe(true); + + const { frontendFilter: freeFormNoMatch } = getGrafanaFilter(getFilter({ freeFormWords: ['memory'] })); + expect(freeFormNoMatch.ruleMatches(alertingRule)).toBe(false); + + const { frontendFilter: ruleNameMatch } = getGrafanaFilter(getFilter({ ruleName: 'cpu' })); + expect(ruleNameMatch.ruleMatches(alertingRule)).toBe(true); + + const { frontendFilter: ruleNameNoMatch } = getGrafanaFilter(getFilter({ ruleName: 'memory' })); + expect(ruleNameNoMatch.ruleMatches(alertingRule)).toBe(false); + + // Group name filtering + const group: PromRuleGroupDTO = { + name: 'CPU Usage Alerts', + file: 'production/alerts', + rules: [], + interval: 60, + }; + + const { frontendFilter: groupMatch } = getGrafanaFilter(getFilter({ groupName: 'cpu' })); + expect(groupMatch.groupMatches(group)).toBe(true); + + const { frontendFilter: groupNoMatch } = getGrafanaFilter(getFilter({ groupName: 'memory' })); + expect(groupNoMatch.groupMatches(group)).toBe(false); + + // Always-frontend filters (labels, dataSourceNames, namespace) should work + const { frontendFilter: labelsMatch } = getGrafanaFilter(getFilter({ labels: ['severity=critical'] })); + expect(labelsMatch.ruleMatches(alertingRule)).toBe(true); + + const { frontendFilter: labelsNoMatch } = getGrafanaFilter(getFilter({ labels: ['severity=warning'] })); + expect(labelsNoMatch.ruleMatches(alertingRule)).toBe(false); + + const { frontendFilter: dsMatch } = getGrafanaFilter(getFilter({ dataSourceNames: ['prometheus'] })); + expect(dsMatch.ruleMatches(alertingRule)).toBe(true); + + const { frontendFilter: dsNoMatch } = getGrafanaFilter(getFilter({ dataSourceNames: ['loki'] })); + expect(dsNoMatch.ruleMatches(alertingRule)).toBe(false); + + const { frontendFilter: nsMatch } = getGrafanaFilter(getFilter({ namespace: 'production' })); + expect(nsMatch.groupMatches(group)).toBe(true); + + const { frontendFilter: nsNoMatch } = getGrafanaFilter(getFilter({ namespace: 'staging' })); + expect(nsNoMatch.groupMatches(group)).toBe(false); + }); + }); + + describe('when both alertingUIUseBackendFilters and alertingUIUseFullyCompatBackendFilters are enabled', () => { + testWithFeatureToggles({ enable: ['alertingUIUseBackendFilters', 'alertingUIUseFullyCompatBackendFilters'] }); + + it('should include all backend filters (title, ruleType, dashboardUid, searchGroupName)', () => { + const { backendFilter } = getGrafanaFilter( + getFilter({ + freeFormWords: ['cpu'], + ruleName: 'alert', + ruleType: PromRuleType.Alerting, + dashboardUid: 'dashboard-123', + groupName: 'my-group', + }) + ); + + expect(backendFilter.title).toBe('alert cpu'); + expect(backendFilter.type).toBe(PromRuleType.Alerting); + expect(backendFilter.dashboardUid).toBe('dashboard-123'); + expect(backendFilter.searchGroupName).toBe('my-group'); + }); + + it('should skip all backend-handled filters on frontend', () => { + const alertingRule = mockGrafanaPromAlertingRule({ + name: 'High CPU Usage', + annotations: { [Annotation.dashboardUID]: 'dashboard-a' }, + }); + const recordingRule = mockPromRecordingRule({ name: 'Test Recording' }); + + const { frontendFilter } = getGrafanaFilter( + getFilter({ + freeFormWords: ['memory'], + ruleName: 'memory', + ruleType: PromRuleType.Recording, + dashboardUid: 'dashboard-b', + }) + ); + + // All these filters are handled by backend, so frontend should return true + expect(frontendFilter.ruleMatches(alertingRule)).toBe(true); + expect(frontendFilter.ruleMatches(recordingRule)).toBe(true); + }); + + it('should skip groupName filtering on frontend', () => { + const group: PromRuleGroupDTO = { + name: 'CPU Usage Alerts', + file: 'production/alerts', + rules: [], + interval: 60, + }; + + const { frontendFilter } = getGrafanaFilter(getFilter({ groupName: 'memory' })); + // Should return true because groupName filter is null (handled by backend) + expect(frontendFilter.groupMatches(group)).toBe(true); + }); + + it('should still apply always-frontend filters (labels, dataSourceNames, namespace)', () => { + const rule = mockGrafanaPromAlertingRule({ + name: 'High CPU Usage', + labels: { severity: 'critical' }, + queriedDatasourceUIDs: ['datasource-uid-1'], + alerts: [], + }); + + // Labels filter should still work + const { frontendFilter: labelFilter } = getGrafanaFilter(getFilter({ labels: ['severity=warning'] })); + expect(labelFilter.ruleMatches(rule)).toBe(false); + + const { frontendFilter: labelFilter2 } = getGrafanaFilter(getFilter({ labels: ['severity=critical'] })); + expect(labelFilter2.ruleMatches(rule)).toBe(true); + + // DataSourceNames filter should still work + const { frontendFilter: dsFilter } = getGrafanaFilter(getFilter({ dataSourceNames: ['prometheus'] })); + expect(dsFilter.ruleMatches(rule)).toBe(true); + + const { frontendFilter: dsFilter2 } = getGrafanaFilter(getFilter({ dataSourceNames: ['loki'] })); + expect(dsFilter2.ruleMatches(rule)).toBe(false); + + // Namespace filter should still work + const group: PromRuleGroupDTO = { + name: 'Test Group', + file: 'production/alerts', + rules: [], + interval: 60, + }; + + const { frontendFilter: nsFilter } = getGrafanaFilter(getFilter({ namespace: 'production' })); + expect(nsFilter.groupMatches(group)).toBe(true); + + const { frontendFilter: nsFilter2 } = getGrafanaFilter(getFilter({ namespace: 'staging' })); + expect(nsFilter2.groupMatches(group)).toBe(false); + }); + }); }); describe('hasClientSideFilters', () => { @@ -560,5 +744,49 @@ describe('grafana-managed rules', () => { expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); }); }); + + describe('when alertingUIUseFullyCompatBackendFilters is enabled', () => { + testWithFeatureToggles({ enable: ['alertingUIUseFullyCompatBackendFilters'] }); + + it('should return correct values for all filter types', () => { + // Should return false for: empty, backend-handled (ruleType, dashboardUid), and backend-only filters + expect(hasClientSideFilters(getFilter({}))).toBe(false); + expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); + expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); + expect(hasClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); + expect(hasClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); + expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); + + // Should return true for: frontend-handled filters + expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(true); + expect(hasClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(true); + expect(hasClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(true); + expect(hasClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); + expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); + }); + }); + + describe('when both alertingUIUseBackendFilters and alertingUIUseFullyCompatBackendFilters are enabled', () => { + testWithFeatureToggles({ enable: ['alertingUIUseBackendFilters', 'alertingUIUseFullyCompatBackendFilters'] }); + + it('should return correct values for all filter types', () => { + // Should return false for: empty, all backend-handled filters, and backend-only filters + expect(hasClientSideFilters(getFilter({}))).toBe(false); + expect(hasClientSideFilters(getFilter({ freeFormWords: ['cpu'] }))).toBe(false); + expect(hasClientSideFilters(getFilter({ ruleName: 'alert' }))).toBe(false); + expect(hasClientSideFilters(getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); + expect(hasClientSideFilters(getFilter({ dashboardUid: 'test-dashboard' }))).toBe(false); + expect(hasClientSideFilters(getFilter({ groupName: 'test-group' }))).toBe(false); + expect(hasClientSideFilters(getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); + expect(hasClientSideFilters(getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(false); + expect(hasClientSideFilters(getFilter({ contactPoint: 'my-contact-point' }))).toBe(false); + + // Should return true for: always-frontend filters only + expect(hasClientSideFilters(getFilter({ namespace: 'production' }))).toBe(true); + expect(hasClientSideFilters(getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); + expect(hasClientSideFilters(getFilter({ labels: ['severity=critical'] }))).toBe(true); + }); + }); }); }); diff --git a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts index d7d224047d7..5b5ddd81f9b 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.ts @@ -1,7 +1,7 @@ import { PromRuleDTO, PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { GrafanaPromRulesOptions } from '../../api/prometheusApi'; -import { shouldUseBackendFilters } from '../../featureToggles'; +import { shouldUseBackendFilters, shouldUseFullyCompatibleBackendFilters } from '../../featureToggles'; import { RulesFilter } from '../../search/rulesSearchParser'; import { buildTitleSearch, normalizeFilterState } from './filterNormalization'; @@ -57,7 +57,8 @@ export function hasClientSideFilters(filterState: RulesFilter): boolean { */ export function getGrafanaFilter(filterState: RulesFilter) { const normalizedFilterState = normalizeFilterState(filterState); - const useBackendFilters = shouldUseBackendFilters(); + + const { ruleFilterConfig, groupFilterConfig } = buildGrafanaFilterConfigs(); // Build title search for backend filtering const titleSearch = buildTitleSearch(normalizedFilterState); @@ -66,20 +67,18 @@ export function getGrafanaFilter(filterState: RulesFilter) { state: normalizedFilterState.ruleState ? [normalizedFilterState.ruleState] : [], health: normalizedFilterState.ruleHealth ? [normalizedFilterState.ruleHealth] : [], contactPoint: normalizedFilterState.contactPoint ?? undefined, - title: useBackendFilters ? titleSearch : undefined, - type: useBackendFilters ? normalizedFilterState.ruleType : undefined, - dashboardUid: useBackendFilters ? normalizedFilterState.dashboardUid : undefined, - searchGroupName: useBackendFilters ? normalizedFilterState.groupName : undefined, + // If FE filter is defined, don't include the backend filter + title: ruleFilterConfig.ruleName ? undefined : titleSearch, + type: ruleFilterConfig.ruleType ? undefined : normalizedFilterState.ruleType, + dashboardUid: ruleFilterConfig.dashboardUid ? undefined : normalizedFilterState.dashboardUid, + searchGroupName: groupFilterConfig.groupName ? undefined : normalizedFilterState.groupName, }; - const { ruleFilterConfig: grafanaFilterProcessingConfig, groupFilterConfig: grafanaGroupFilterConfig } = - buildGrafanaFilterConfigs(); - return { backendFilter, frontendFilter: { - groupMatches: (group: PromRuleGroupDTO) => groupMatches(group, normalizedFilterState, grafanaGroupFilterConfig), - ruleMatches: (rule: PromRuleDTO) => ruleMatches(rule, normalizedFilterState, grafanaFilterProcessingConfig), + groupMatches: (group: PromRuleGroupDTO) => groupMatches(group, normalizedFilterState, groupFilterConfig), + ruleMatches: (rule: PromRuleDTO) => ruleMatches(rule, normalizedFilterState, ruleFilterConfig), }, }; } @@ -93,17 +92,18 @@ export function getGrafanaFilter(filterState: RulesFilter) { */ function buildGrafanaFilterConfigs() { const useBackendFilters = shouldUseBackendFilters(); + const useFullyCompatibleBackendFilters = shouldUseFullyCompatibleBackendFilters(); const ruleFilterConfig: RuleFilterConfig = { // When backend filtering is enabled, these filters are handled by the backend freeFormWords: useBackendFilters ? null : freeFormFilter, ruleName: useBackendFilters ? null : ruleNameFilter, ruleState: null, - ruleType: useBackendFilters ? null : ruleTypeFilter, + ruleType: useBackendFilters || useFullyCompatibleBackendFilters ? null : ruleTypeFilter, dataSourceNames: dataSourceNamesFilter, labels: labelsFilter, ruleHealth: null, - dashboardUid: useBackendFilters ? null : dashboardUidFilter, + dashboardUid: useBackendFilters || useFullyCompatibleBackendFilters ? null : dashboardUidFilter, plugins: pluginsFilter, contactPoint: null, }; From 67ad43a784f02c98e80414e159425ccbb99a97e0 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Mon, 24 Nov 2025 12:48:22 +0100 Subject: [PATCH 075/378] Alerting: Fix rules API pagination when using in-memory filters (#114334) --- .../ngalert/api/api_prometheus_test.go | 571 ++++++++++++++++++ .../ngalert/api/prometheus/api_prometheus.go | 245 ++++++-- 2 files changed, 768 insertions(+), 48 deletions(-) diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index 2b8ef4b48e7..e31e713fb33 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -23,6 +23,7 @@ import ( contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" @@ -327,6 +328,7 @@ func withLabels(labels data.Labels) forEachState { } } +//nolint:gocyclo func TestRouteGetRuleStatuses(t *testing.T) { timeNow = func() time.Time { return time.Date(2022, 3, 10, 14, 0, 0, 0, time.UTC) } orgID := int64(1) @@ -2219,6 +2221,575 @@ func TestRouteGetRuleStatuses(t *testing.T) { require.True(t, foundNoProv, "should find rule without provenance") require.True(t, foundWithProv, "should find rule with provenance") }) + + t.Run("filter-aware pagination", func(t *testing.T) { + createRulesWithState := func(t *testing.T, store *fakes.RuleStore, aim *fakeAlertInstanceManager, + orgID int64, numGroups int, rulesPerGroup int, + stateFunc func(groupIdx int) eval.State, + healthFunc func(groupIdx int) error, + stateMutators ...func(groupIdx int, s *state.State) *state.State) { + t.Helper() + + // create folders + for i := 1; i <= numGroups; i++ { + store.Folders[orgID] = append(store.Folders[orgID], &folder.Folder{ + ID: int64(i), + UID: fmt.Sprintf("ns-%d", i), + Title: fmt.Sprintf("Namespace %d", i), + Fullpath: fmt.Sprintf("/namespace-%d", i), + }) + } + + for i := 0; i < numGroups; i++ { + for j := 0; j < rulesPerGroup; j++ { + rule := gen.With(gen.WithOrgID(orgID), func(r *ngmodels.AlertRule) { + r.NamespaceUID = fmt.Sprintf("ns-%d", i+1) + r.RuleGroup = fmt.Sprintf("group-%d", i+1) + r.UID = fmt.Sprintf("rule-%d-%d", i+1, j+1) + }, withClassicConditionSingleQuery()).GenerateRef() + + alertState := stateFunc(i) + healthErr := healthFunc(i) + + aim.GenerateAlertInstances(orgID, rule.UID, 1, func(s *state.State) *state.State { + s.State = alertState + s.Error = healthErr + s.Labels = data.Labels{"test": "label"} + + for _, mutator := range stateMutators { + s = mutator(i, s) + } + return s + }) + store.PutRule(context.Background(), rule) + } + } + } + + t.Run("state filter fetches multiple pages to fill group_limit", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create 10 groups (2 rules each = 20 rules total): groups 1,3,5,7,9 firing, groups 2,4,6,8,10 normal + // Request group_limit=3 with state=firing should fetch pages until 3 firing groups collected + createRulesWithState(t, fakeStore, fakeAIM, orgID, 10, 2, + func(i int) eval.State { + if i%2 == 0 { + return eval.Alerting + } + return eval.Normal + }, + func(i int) error { return nil }) + + // Request 3 groups with state=firing filter + req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&group_limit=3", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + // Should return 3 firing groups + require.Len(t, res.Data.RuleGroups, 3) + require.Equal(t, "group-1", res.Data.RuleGroups[0].Name) + require.Equal(t, "group-3", res.Data.RuleGroups[1].Name) + require.Equal(t, "group-5", res.Data.RuleGroups[2].Name) + + // Verify all have firing alerts + for _, rg := range res.Data.RuleGroups { + hasFiring := false + for _, rule := range rg.Rules { + for _, alert := range rule.Alerts { + if alert.State == eval.Alerting.String() { + hasFiring = true + } + } + } + require.True(t, hasFiring) + } + }) + + t.Run("state filter continues when first page has no matches", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create 8 groups (2 rules each = 16 rules total): first 4 normal, last 4 firing + // Request state=firing with group_limit=2 should skip first 4 and return groups 5,6 + createRulesWithState(t, fakeStore, fakeAIM, orgID, 8, 2, + func(i int) eval.State { + if i < 4 { + return eval.Normal // groups 1-4 normal + } + return eval.Alerting // groups 5-8 firing + }, + func(i int) error { return nil }) + + // Request 2 firing groups - should skip past the first page of normal rules + req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&group_limit=2", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + // Should return 2 firing groups + require.Len(t, res.Data.RuleGroups, 2) + require.Equal(t, "group-5", res.Data.RuleGroups[0].Name) + require.Equal(t, "group-6", res.Data.RuleGroups[1].Name) + }) + + t.Run("health filter fetches multiple pages", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create 8 groups (2 rules each = 16 rules total): groups 1,3,5,7 with error health, groups 2,4,6,8 with ok health + // Request health=error with group_limit=3 should fetch pages until 3 error groups collected + createRulesWithState(t, fakeStore, fakeAIM, orgID, 8, 2, + func(i int) eval.State { return eval.Normal }, + func(i int) error { + if i%2 == 0 { + return fmt.Errorf("evaluation error") + } + return nil + }) + + // Request 3 groups with health=error filter + req, err := http.NewRequest("GET", "/api/v1/rules?health=error&group_limit=3", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + // Should return 3 error groups + require.Len(t, res.Data.RuleGroups, 3) + require.Equal(t, "group-1", res.Data.RuleGroups[0].Name) + require.Equal(t, "group-3", res.Data.RuleGroups[1].Name) + require.Equal(t, "group-5", res.Data.RuleGroups[2].Name) + + // Verify all have error health + for _, rg := range res.Data.RuleGroups { + for _, rule := range rg.Rules { + require.Equal(t, "error", rule.Health) + } + } + }) + + t.Run("combined state and health filters", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create 10 groups (2 rules each = 20 rules total) + // Groups 1-5: firing, groups 6-10: normal + // Groups 1,3,5,7,9: ok health, groups 2,4,6,8,10: error health + // Groups matching both filters (firing + ok): 1,3,5 + createRulesWithState(t, fakeStore, fakeAIM, orgID, 10, 2, + func(i int) eval.State { + if i < 5 { + return eval.Alerting + } + return eval.Normal + }, + func(i int) error { + if i%2 == 1 { + return fmt.Errorf("evaluation error") + } + return nil + }) + + // Request 3 groups with state=firing AND health=ok + req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&health=ok&group_limit=3", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + // Should return 3 groups matching both filters + require.Len(t, res.Data.RuleGroups, 3) + require.Equal(t, "group-1", res.Data.RuleGroups[0].Name) + require.Equal(t, "group-3", res.Data.RuleGroups[1].Name) + require.Equal(t, "group-5", res.Data.RuleGroups[2].Name) + + // Verify all match both criteria + for _, rg := range res.Data.RuleGroups { + for _, rule := range rg.Rules { + require.Equal(t, "ok", rule.Health) + hasFiring := false + for _, alert := range rule.Alerts { + if alert.State == eval.Alerting.String() { + hasFiring = true + } + } + require.True(t, hasFiring) + } + } + }) + + t.Run("rule_limit hit before group_limit", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create 5 groups (3 rules each = 15 rules total) + // Request: group_limit=10, rule_limit=8 + // Expected: Should return groups 1-3 (9 rules total, exceeds limit but complete group included) + createRulesWithState(t, fakeStore, fakeAIM, orgID, 5, 3, + func(i int) eval.State { return eval.Alerting }, // all firing + func(i int) error { return nil }) // all healthy + + // Request group_limit=10, rule_limit=8 - rule limit should hit first + req, err := http.NewRequest("GET", "/api/v1/rules?group_limit=10&rule_limit=8", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + // Expected behavior with rule_limit=8: + // Group 1: 3 rules (total: 3, under 8, continue) + // Group 2: 3 rules (total: 6, under 8, continue) + // Group 3: 3 rules (total: 9, exceeds 8 but we include complete group) + // Result: 3 groups, 9 rules total + totalRules := 0 + for _, rg := range res.Data.RuleGroups { + t.Logf("Group %s has %d rules", rg.Name, len(rg.Rules)) + totalRules += len(rg.Rules) + } + t.Logf("Total: %d rules, %d groups", totalRules, len(res.Data.RuleGroups)) + require.Equal(t, 9, totalRules) + require.Equal(t, 3, len(res.Data.RuleGroups)) + }) + + t.Run("rule_limit without group_limit", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create 10 groups (2 rules each = 20 rules total) + // Request rule_limit=7 should stop after 4 groups (8 rules total) + createRulesWithState(t, fakeStore, fakeAIM, orgID, 10, 2, + func(i int) eval.State { return eval.Alerting }, + func(i int) error { return nil }) + + // Request rule_limit=7 only (group_limit unlimited) + req, err := http.NewRequest("GET", "/api/v1/rules?rule_limit=7", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + // Should return 4 groups (8 rules total, stops after exceeding 7) + totalRules := 0 + for _, rg := range res.Data.RuleGroups { + totalRules += len(rg.Rules) + } + require.Equal(t, 8, totalRules) + require.Equal(t, 4, len(res.Data.RuleGroups)) + require.NotEmpty(t, res.Data.NextToken) + }) + + t.Run("empty page in middle of pagination", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create 15 groups (2 rules each = 30 rules total): groups 1-3 firing, 4-8 normal, 9-13 firing, 14-15 normal + // Request state=firing with group_limit=5 should skip over normal groups + createRulesWithState(t, fakeStore, fakeAIM, orgID, 15, 2, + func(i int) eval.State { + groupNum := i + 1 + if groupNum <= 3 || (groupNum >= 9 && groupNum <= 13) { + return eval.Alerting + } + return eval.Normal + }, + func(i int) error { return nil }) + + // Request state=firing, group_limit=5 + req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&group_limit=5", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + // Should return 5 firing groups from pages 1 and 3 (skipping empty page 2) + require.Len(t, res.Data.RuleGroups, 5) + + // Verify all are firing + for _, rg := range res.Data.RuleGroups { + for _, rule := range rg.Rules { + for _, alert := range rule.Alerts { + require.Equal(t, eval.Alerting.String(), alert.State) + } + } + } + }) + + t.Run("group_limit=0 returns empty response", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create 1 group (2 rules) to verify group_limit=0 returns empty + createRulesWithState(t, fakeStore, fakeAIM, orgID, 1, 2, + func(i int) eval.State { return eval.Alerting }, + func(i int) error { return nil }) + + // Request group_limit=0 + req, err := http.NewRequest("GET", "/api/v1/rules?group_limit=0", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + require.Len(t, res.Data.RuleGroups, 0) + require.Empty(t, res.Data.NextToken) + }) + + t.Run("resume with token and filters active", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create 10 groups (2 rules each = 20 rules total): odd groups firing, even groups normal + // Test pagination continuation with state filter: first page returns groups 1,3 then second page returns groups 5,7 + createRulesWithState(t, fakeStore, fakeAIM, orgID, 10, 2, + func(i int) eval.State { + if i%2 == 0 { + return eval.Alerting + } + return eval.Normal + }, + func(i int) error { return nil }) + + // First request: state=firing, group_limit=2 + req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&group_limit=2", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + + var res1 apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res1)) + + // Should return 2 firing groups (1, 3) + require.Len(t, res1.Data.RuleGroups, 2) + require.NotEmpty(t, res1.Data.NextToken) + + // Verify both are firing + for _, rg := range res1.Data.RuleGroups { + for _, rule := range rg.Rules { + require.Equal(t, "firing", rule.State) + } + } + + // Second request: resume with token AND filter still active + req2, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/rules?state=firing&group_limit=2&group_next_token=%s", res1.Data.NextToken), nil) + require.NoError(t, err) + c2 := &contextmodel.ReqContext{ + Context: &web.Context{Req: req2}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp2 := api.RouteGetRuleStatuses(c2) + require.Equal(t, http.StatusOK, resp2.Status()) + + var res2 apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp2.Body(), &res2)) + + // Should return next 2 firing groups (5, 7) + require.Len(t, res2.Data.RuleGroups, 2) + require.NotEmpty(t, res2.Data.NextToken) + + // Verify both are firing + for _, rg := range res2.Data.RuleGroups { + for _, rule := range rg.Rules { + require.Equal(t, "firing", rule.State) + } + } + + // Verify we got different groups than first request + firstGroupNames := make(map[string]bool) + for _, rg := range res1.Data.RuleGroups { + firstGroupNames[rg.Name] = true + } + for _, rg := range res2.Data.RuleGroups { + require.False(t, firstGroupNames[rg.Name]) + } + }) + + t.Run("rule_limit with state filter", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create 10 groups (2 rules each = 20 rules total), alternating firing/normal + // Firing groups: 1,3,5,7,9 (5 groups × 2 rules = 10 firing rules) + // Request state=firing with rule_limit=7 should return groups 1,3,5,7 (8 rules) + createRulesWithState(t, fakeStore, fakeAIM, orgID, 10, 2, + func(i int) eval.State { + if i%2 == 0 { + return eval.Alerting + } + return eval.Normal + }, + func(i int) error { return nil }) + + // Request state=firing, rule_limit=7 + // Should fetch multiple pages to accumulate 7+ firing rules + // Groups 1, 3, 5 = 6 rules (under limit) + // Group 7 = +2 rules = 8 total (exceeds 7, but we include full group) + req, err := http.NewRequest("GET", "/api/v1/rules?state=firing&rule_limit=7", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + // Count total firing rules returned + totalRules := 0 + for _, rg := range res.Data.RuleGroups { + for _, rule := range rg.Rules { + require.Equal(t, "firing", rule.State) + totalRules++ + } + } + + // Should return 4 firing groups (1,3,5,7) with 8 rules total + require.Equal(t, 8, totalRules) + require.Equal(t, 4, len(res.Data.RuleGroups)) + require.NotEmpty(t, res.Data.NextToken) + }) + + t.Run("rule_limit with health filter", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create 8 groups (3 rules each = 24 rules total), alternating ok/error health + // Error groups: 1,3,5,7 (4 groups × 3 rules = 12 error rules) + // Request health=error with rule_limit=8 should return groups 1,3,5 (9 rules) + createRulesWithState(t, fakeStore, fakeAIM, orgID, 8, 3, + func(i int) eval.State { return eval.Normal }, + func(i int) error { + if i%2 == 0 { + return fmt.Errorf("evaluation error") + } + return nil + }) + + // Request health=error, rule_limit=8 + // Should fetch multiple pages to accumulate 8+ error rules + // Groups 1, 3 = 6 rules (under limit) + // Group 5 = +3 rules = 9 total (exceeds 8, but we include full group) + req, err := http.NewRequest("GET", "/api/v1/rules?health=error&rule_limit=8", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + // Count total error rules returned + totalRules := 0 + for _, rg := range res.Data.RuleGroups { + for _, rule := range rg.Rules { + require.Equal(t, "error", rule.Health) + totalRules++ + } + } + + // Should return 3 error groups (1,3,5) with 9 rules total + require.Equal(t, 9, totalRules) + require.Equal(t, 3, len(res.Data.RuleGroups)) + require.NotEmpty(t, res.Data.NextToken) + }) + }) } func setupAPI(t *testing.T) (*fakes.RuleStore, *fakeAlertInstanceManager, PrometheusSrv) { diff --git a/pkg/services/ngalert/api/prometheus/api_prometheus.go b/pkg/services/ngalert/api/prometheus/api_prometheus.go index 00002c61593..082c1bfd333 100644 --- a/pkg/services/ngalert/api/prometheus/api_prometheus.go +++ b/pkg/services/ngalert/api/prometheus/api_prometheus.go @@ -30,6 +30,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) type RuleStoreReader interface { @@ -429,6 +430,177 @@ func RuleAlertStateMutatorGenerator(manager state.AlertInstanceManager) RuleAler } } +// paginationContext holds limits and filters for filter-aware pagination +type paginationContext struct { + opts RuleGroupStatusesOptions + provenanceRecords map[string]ngmodels.Provenance + ruleStatusMutator RuleStatusMutator + alertStateMutator RuleAlertStateMutator + + // Query parameters + namespaceUIDs []string + ruleUIDs []string + dashboardUID string + panelID int64 + ruleGroups []string + receiverName string + title string + searchRuleGroup string + ruleType ngmodels.RuleTypeFilter + ruleNamesSet map[string]struct{} + + // Filters + stateFilterSet map[eval.State]struct{} + healthFilterSet map[string]struct{} + matchers labels.Matchers + labelOptions []ngmodels.LabelOption + limitAlertsPerRule int64 + limitRulesPerGroup int64 +} + +// pageResult is the result of fetching and filtering of one page +type pageResult struct { + groups []apimodels.RuleGroup + totalsDelta map[string]int64 + nextToken string + hasMore bool +} + +func accumulateTotals(dest, source map[string]int64) { + for k, v := range source { + dest[k] += v + } +} + +// fetchAndFilterPage fetches one page from the store and applies filters +func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlertRulesStoreV2, span trace.Span, token string, remainingGroups, remainingRules int64) (pageResult, error) { + byGroupQuery := ngmodels.ListAlertRulesExtendedQuery{ + ListAlertRulesQuery: ngmodels.ListAlertRulesQuery{ + OrgID: ctx.opts.OrgID, + NamespaceUIDs: ctx.namespaceUIDs, + RuleUIDs: ctx.ruleUIDs, + DashboardUID: ctx.dashboardUID, + PanelID: ctx.panelID, + RuleGroups: ctx.ruleGroups, + ReceiverName: ctx.receiverName, + SearchTitle: ctx.title, + SearchRuleGroup: ctx.searchRuleGroup, + }, + RuleType: ctx.ruleType, + Limit: remainingGroups, + RuleLimit: remainingRules, + ContinueToken: token, + } + + ruleList, newToken, err := store.ListAlertRulesByGroup(ctx.opts.Ctx, &byGroupQuery) + if err != nil { + return pageResult{}, err + } + + span.SetAttributes( + attribute.Int("store_rule_list_len", len(ruleList)), + attribute.Bool("store_continue_token_set", newToken != ""), + ) + span.AddEvent("Alert rules retrieved from store") + + groupedRules := getGroupedRules(log, ruleList, ctx.ruleNamesSet, ctx.opts.AllowedNamespaces) + + result := pageResult{ + groups: make([]apimodels.RuleGroup, 0, len(groupedRules)), + totalsDelta: make(map[string]int64), + nextToken: newToken, + hasMore: newToken != "", + } + + for _, rg := range groupedRules { + ruleGroup, totals := toRuleGroup( + log, rg.GroupKey, rg.Folder, rg.Rules, + ctx.provenanceRecords, ctx.limitAlertsPerRule, + ctx.stateFilterSet, ctx.matchers, ctx.labelOptions, + ctx.ruleStatusMutator, ctx.alertStateMutator, + ) + ruleGroup.Totals = totals + accumulateTotals(result.totalsDelta, totals) + + if len(ctx.stateFilterSet) > 0 { + filterRulesByState(ruleGroup, ctx.stateFilterSet) + } + + if len(ctx.healthFilterSet) > 0 { + filterRulesByHealth(ruleGroup, ctx.healthFilterSet) + } + + if ctx.limitRulesPerGroup > -1 && int64(len(ruleGroup.Rules)) > ctx.limitRulesPerGroup { + ruleGroup.Rules = ruleGroup.Rules[0:ctx.limitRulesPerGroup] + } + + if len(ruleGroup.Rules) > 0 { + result.groups = append(result.groups, *ruleGroup) + } + } + + return result, nil +} + +// paginateRuleGroups fetches pages until limits are satisfied applying filters at each step +func paginateRuleGroups(log log.Logger, store ListAlertRulesStoreV2, ctx *paginationContext, span trace.Span, maxGroups, maxRules int64, startToken string) ([]apimodels.RuleGroup, map[string]int64, string, error) { + allGroups := []apimodels.RuleGroup{} + rulesTotals := make(map[string]int64) + + continueToken := startToken + groupsReturned := int64(0) + rulesReturned := int64(0) + + for { + remainingGroups := maxGroups + if maxGroups > 0 { + remainingGroups = maxGroups - groupsReturned + if remainingGroups <= 0 { + break + } + } + remainingRules := maxRules + if maxRules > 0 { + remainingRules = maxRules - rulesReturned + if remainingRules <= 0 { + break + } + } + + page, err := ctx.fetchAndFilterPage(log, store, span, continueToken, remainingGroups, remainingRules) + if err != nil { + return nil, nil, "", err + } + + accumulateTotals(rulesTotals, page.totalsDelta) + + // Add groups and check limits + for _, group := range page.groups { + allGroups = append(allGroups, group) + groupsReturned++ + rulesReturned += int64(len(group.Rules)) + + // Check if we've hit limits + if (maxGroups > 0 && groupsReturned == maxGroups) || (maxRules > 0 && rulesReturned >= maxRules) { + return allGroups, rulesTotals, page.nextToken, nil + } + } + + if !page.hasMore { + return allGroups, rulesTotals, "", nil + } + + if page.nextToken == continueToken { + log.Warn("Pagination loop detected same token, stopping", "token", page.nextToken) + return allGroups, rulesTotals, page.nextToken, nil + } + + continueToken = page.nextToken + } + + return allGroups, rulesTotals, continueToken, nil +} + func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opts RuleGroupStatusesOptions, ruleStatusMutator RuleStatusMutator, alertStateMutator RuleAlertStateMutator, provenanceRecords map[string]ngmodels.Provenance) apimodels.RuleResponse { ctx, span := tracer.Start(opts.Ctx, "api.prometheus.PrepareRuleGroupStatusesV2") defer span.End() @@ -591,62 +763,38 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt } span.SetAttributes(attribute.Int("rule_name_count", len(ruleNamesSet))) - byGroupQuery := ngmodels.ListAlertRulesExtendedQuery{ - ListAlertRulesQuery: ngmodels.ListAlertRulesQuery{ - OrgID: opts.OrgID, - NamespaceUIDs: namespaceUIDs, - RuleUIDs: ruleUIDs, - DashboardUID: dashboardUID, - PanelID: panelID, - RuleGroups: ruleGroups, - ReceiverName: receiverName, - SearchTitle: title, - SearchRuleGroup: searchRuleGroup, - }, - RuleType: ruleType, - Limit: maxGroups, - RuleLimit: maxRules, - ContinueToken: nextToken, + pagCtx := &paginationContext{ + opts: opts, + provenanceRecords: provenanceRecords, + ruleStatusMutator: ruleStatusMutator, + alertStateMutator: alertStateMutator, + namespaceUIDs: namespaceUIDs, + ruleUIDs: ruleUIDs, + dashboardUID: dashboardUID, + panelID: panelID, + ruleGroups: ruleGroups, + receiverName: receiverName, + title: title, + searchRuleGroup: searchRuleGroup, + ruleType: ruleType, + ruleNamesSet: ruleNamesSet, + stateFilterSet: stateFilterSet, + healthFilterSet: healthFilterSet, + matchers: matchers, + labelOptions: labelOptions, + limitAlertsPerRule: limitAlertsPerRule, + limitRulesPerGroup: limitRulesPerGroup, } - ruleList, continueToken, err := store.ListAlertRulesByGroup(opts.Ctx, &byGroupQuery) + + groups, rulesTotals, continueToken, err := paginateRuleGroups(log, store, pagCtx, span, maxGroups, maxRules, nextToken) if err != nil { ruleResponse.Status = "error" ruleResponse.Error = fmt.Sprintf("failure getting rules: %s", err.Error()) ruleResponse.ErrorType = apiv1.ErrServer return ruleResponse } - span.SetAttributes( - attribute.Int("store_rule_list_len", len(ruleList)), - attribute.Bool("store_continue_token_set", continueToken != ""), - ) - span.AddEvent("Alert rules retrieved from store") - - groupedRules := getGroupedRules(log, ruleList, ruleNamesSet, opts.AllowedNamespaces) - rulesTotals := make(map[string]int64, len(groupedRules)) - for _, rg := range groupedRules { - ruleGroup, totals := toRuleGroup(log, rg.GroupKey, rg.Folder, rg.Rules, provenanceRecords, limitAlertsPerRule, stateFilterSet, matchers, labelOptions, ruleStatusMutator, alertStateMutator) - ruleGroup.Totals = totals - for k, v := range totals { - rulesTotals[k] += v - } - - if len(stateFilterSet) > 0 { - filterRulesByState(ruleGroup, stateFilterSet) - } - - if len(healthFilterSet) > 0 { - filterRulesByHealth(ruleGroup, healthFilterSet) - } - - if limitRulesPerGroup > -1 && int64(len(ruleGroup.Rules)) > limitRulesPerGroup { - ruleGroup.Rules = ruleGroup.Rules[0:limitRulesPerGroup] - } - - if len(ruleGroup.Rules) > 0 { - ruleResponse.Data.RuleGroups = append(ruleResponse.Data.RuleGroups, *ruleGroup) - } - } + ruleResponse.Data.RuleGroups = groups ruleResponse.Data.NextToken = continueToken // Only return Totals if there is no pagination @@ -981,6 +1129,7 @@ func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFull // mutate rule for alert states totals, totalsFiltered := ruleAlertStateMutator(rule, &alertingRule, stateFilterSet, matchers, labelOptions) + if alertingRule.State != "" { rulesTotals[alertingRule.State] += 1 } From 2f6836e78a414bf3ffc440bc5c590c44a4df7323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Sencer=20=C3=96zcan?= <32759850+mustafasencer@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:12:52 +0100 Subject: [PATCH 076/378] fix: make graceful handle default for the malformed dashboard jsons during unified migration (#114295) * fix: graceful handling by default * fix: make fallback the default behavior --- .../datamigrations/to_unified_storage.go | 1 - .../apis/dashboard/legacy/sql_dashboards.go | 24 +++--------- .../dashboard/legacy/sql_dashboards_test.go | 37 ++++++------------- pkg/server/wire_gen.go | 4 +- 4 files changed, 18 insertions(+), 48 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go index 2e8a9892ab1..a884c8f16cf 100644 --- a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go +++ b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go @@ -83,7 +83,6 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err legacysql.NewDatabaseProvider(sqlStore), provisioning, acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), - featureToggles, ) if c.Bool("non-interactive") { diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 81ed308cdcc..8c465ef7b24 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -83,9 +83,6 @@ type dashboardSqlAccess struct { namespacer request.NamespaceMapper provisioning provisioning.StubProvisioningService - // TODO: consider enabling this by default for on-prem migrations - invalidDashboardParseFallbackEnabled bool - // Use for writing (not reading) dashStore dashboards.Store dashboardSearchClient legacysearcher.DashboardSearchClient @@ -106,7 +103,6 @@ func ProvideMigratorDashboardAccessor( sql legacysql.LegacyDatabaseProvider, provisioning provisioning.StubProvisioningService, accessControl accesscontrol.AccessControl, - features featuremgmt.FeatureToggles, ) MigrationDashboardAccessor { return &dashboardSqlAccess{ sql: sql, @@ -116,8 +112,6 @@ func ProvideMigratorDashboardAccessor( dashboardPermissionSvc: nil, // not needed for migration libraryPanelSvc: nil, // not needed for migration accessControl: accessControl, - //nolint:staticcheck // not yet migrated to OpenFeature - invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled), } } @@ -141,8 +135,6 @@ func NewDashboardSQLAccess(sql legacysql.LegacyDatabaseProvider, dashboardPermissionSvc: dashboardPermissionSvc, libraryPanelSvc: libraryPanelSvc, accessControl: accessControl, - //nolint:staticcheck // not yet migrated to OpenFeature - invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled), } } @@ -593,17 +585,17 @@ func generateFallbackDashboard(data []byte, title, uid string) ([]byte, error) { func (a *dashboardSqlAccess) parseDashboard(dash *dashboardV1.Dashboard, data []byte, id int64, title string) error { if err := dash.Spec.UnmarshalJSON(data); err != nil { - a.log.Warn("error unmarshalling dashboard spec. Generating fallback dashboard data", "error", err, "uid", dash.UID, "name", dash.Name) + a.log.Warn("error unmarshalling dashboard spec. Generating fallback dashboard data", "error", err, "uid", dash.UID, "id", id, "name", dash.Name) dash.Spec = *dashboardV0.NewDashboardSpec() dashboardData, err := generateFallbackDashboard(data, title, string(dash.UID)) if err != nil { - a.log.Warn("error generating fallback dashboard data", "error", err, "uid", dash.UID, "name", dash.Name) + a.log.Warn("error generating fallback dashboard data", "error", err, "uid", dash.UID, "id", id, "name", dash.Name) return err } if err = dash.Spec.UnmarshalJSON(dashboardData); err != nil { - a.log.Warn("error unmarshalling fallback dashboard data", "error", err, "uid", dash.UID, "name", dash.Name) + a.log.Warn("error unmarshalling fallback dashboard data", "error", err, "uid", dash.UID, "id", id, "name", dash.Name) return err } } @@ -715,14 +707,8 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo } if len(data) > 0 { - if a.invalidDashboardParseFallbackEnabled { - if err := a.parseDashboard(dash, data, dashboard_id, title); err != nil { - return row, err - } - } else { - if err := dash.Spec.UnmarshalJSON(data); err != nil { - return row, fmt.Errorf("JSON unmarshal error for: %s // %w", dash.Name, err) - } + if err := a.parseDashboard(dash, data, dashboard_id, title); err != nil { + return row, err } } // Ignore any saved values for id/version/uid diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go index b142b144800..e95ee0db6aa 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go @@ -35,10 +35,9 @@ func TestScanRow(t *testing.T) { provisioner := provisioning.NewProvisioningServiceMock(context.Background()) provisioner.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "provisioner" } store := &dashboardSqlAccess{ - namespacer: func(_ int64) string { return "default" }, - provisioning: provisioner, - log: log.New("test"), - invalidDashboardParseFallbackEnabled: false, + namespacer: func(_ int64) string { return "default" }, + provisioning: provisioner, + log: log.New("test"), } columns := []string{"orgId", "dashboard_id", "name", "title", "folder_uid", "deleted", "plugin_id", "origin_name", "origin_path", "origin_hash", "origin_ts", "created", "createdBy", "createdByID", "updated", "updatedBy", "updatedByID", "version", "message", "data", "api_version"} @@ -194,39 +193,25 @@ func TestScanRow(t *testing.T) { require.Equal(t, "dashboard.grafana.app/"+migrationAPIVersion, row.Dash.APIVersion) }) - t.Run("should follow dashboard template when failing to unmarshal dashboard if feature flag X is enabled", func(t *testing.T) { + t.Run("should follow dashboard template when failing to unmarshal dashboard", func(t *testing.T) { // row with bad data badData := []byte(`{"rows":[{"panels":[{"targets":[{"refId":"A","target":"aliasSub(alias, '^(.{27}).+', '\1...')"}]}]}]}`) rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, badData, "vXyz") mock.ExpectQuery("SELECT *").WillReturnRows(rows) resultRows, err := mockDB.Query("SELECT *") require.NoError(t, err) - defer resultRows.Close() // nolint:errcheck + defer func() { + _ = resultRows.Close() + }() resultRows.Next() - row, err := store.scanRow(resultRows, false) - require.Error(t, err, "JSON unmarshal error for: Test Dashboard // invalid character '1' in string escape code") - require.NotNil(t, row) - // correctly scans these - require.Equal(t, uid, row.Dash.Name) - require.Equal(t, version, row.RV) - require.Equal(t, "default", row.Dash.Namespace) - require.Equal(t, &continueToken{orgId: int64(1), id: id}, row.token) - - // failure case: does NOT parse the dashboard itself - require.Equal(t, common.Unstructured{ - Object: nil, - }, row.Dash.Spec) - - // store with feature flag enabled store = &dashboardSqlAccess{ - namespacer: func(_ int64) string { return "default" }, - provisioning: provisioner, - log: log.New("test"), - invalidDashboardParseFallbackEnabled: true, + namespacer: func(_ int64) string { return "default" }, + provisioning: provisioner, + log: log.New("test"), } - row, err = store.scanRow(resultRows, false) + row, err := store.scanRow(resultRows, false) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, uid, row.Dash.Name) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index ce8345d2b09..eb448ba41a8 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -532,7 +532,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - migrationDashboardAccessor := legacy.ProvideMigratorDashboardAccessor(legacyDatabaseProvider, stubProvisioningService, accessControl, featureToggles) + migrationDashboardAccessor := legacy.ProvideMigratorDashboardAccessor(legacyDatabaseProvider, stubProvisioningService, accessControl) unifiedMigrator := migrations2.ProvideUnifiedMigrator(migrationDashboardAccessor, resourceClient) unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore, resourceClient) dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg, unifiedStorageMigrationService) @@ -1179,7 +1179,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - migrationDashboardAccessor := legacy.ProvideMigratorDashboardAccessor(legacyDatabaseProvider, stubProvisioningService, accessControl, featureToggles) + migrationDashboardAccessor := legacy.ProvideMigratorDashboardAccessor(legacyDatabaseProvider, stubProvisioningService, accessControl) unifiedMigrator := migrations2.ProvideUnifiedMigrator(migrationDashboardAccessor, resourceClient) unifiedStorageMigrationService := migrations2.ProvideUnifiedStorageMigrationService(unifiedMigrator, cfg, sqlStore, kvStore, resourceClient) dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg, unifiedStorageMigrationService) From 0c965a9cb1b79755b6581f39ce0fb91abbe53869 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Mon, 24 Nov 2025 14:15:23 +0100 Subject: [PATCH 077/378] CloudMigrations: Remove feature toggle and introduce config setting to disable it (#114223) --- conf/defaults.ini | 2 ++ conf/sample.ini | 2 ++ .../configure-grafana/feature-toggles/index.md | 1 - packages/grafana-data/src/types/config.ts | 1 + packages/grafana-data/src/types/featureToggles.gen.ts | 5 ----- packages/grafana-runtime/src/config.ts | 1 + pkg/api/api.go | 3 +-- pkg/api/dtos/frontend_settings.go | 1 + pkg/api/frontendsettings.go | 4 ++-- .../cloudmigration/cloudmigrationimpl/cloudmigration.go | 5 +---- .../cloudmigrationimpl/cloudmigration_test.go | 9 +++------ .../cloudmigrationimpl/snapshot_mgmt_alerts_test.go | 2 -- pkg/services/featuremgmt/registry.go | 7 ------- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 ---- pkg/services/featuremgmt/toggles_gen.json | 3 ++- pkg/services/navtree/navtreeimpl/admin.go | 3 +-- pkg/setting/setting_cloud_migration.go | 2 ++ public/app/routes/routes.tsx | 2 +- 19 files changed, 20 insertions(+), 38 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 125c748326c..a72b0183290 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -2179,6 +2179,8 @@ enabled = true ###################################### Cloud Migration ###################################### [cloud_migration] +# Set to false to disable the Cloud Migration feature +enabled = true # Set to true to enable target-side migration UI is_target = false # Token used to send requests to grafana com diff --git a/conf/sample.ini b/conf/sample.ini index 3944ea20940..d6397f894e4 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -2071,6 +2071,8 @@ default_datasource_uid = ###################################### Cloud Migration ###################################### [cloud_migration] +# Set to false to disable the Cloud Migration feature +;enabled = true # Set to true to enable target-side migration UI ;is_target = false # Token used to send requests to grafana com diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index ce5adca782c..c05ecd45bd9 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -42,7 +42,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | Yes | | `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes | | `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | | -| `onPremToCloudMigrations` | Enable the Grafana Migration Assistant, which helps you easily migrate various on-prem resources to your Grafana Cloud stack. | Yes | | `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes | | `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | | | `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars | Yes | diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index bb7fe29a655..f519b1d56b4 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -306,6 +306,7 @@ export interface GrafanaConfig { sharedWithMeFolderUID: string; rootFolderUID: string; localFileSystemAvailable: boolean; + cloudMigrationEnabled: boolean; cloudMigrationIsTarget: boolean; cloudMigrationPollIntervalMs: number; pluginCatalogURL: string; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b02be15bfc2..509beeb56c9 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -410,11 +410,6 @@ export interface FeatureToggles { */ jitterAlertRulesWithinGroups?: boolean; /** - * Enable the Grafana Migration Assistant, which helps you easily migrate various on-prem resources to your Grafana Cloud stack. - * @default true - */ - onPremToCloudMigrations?: boolean; - /** * Enable the secrets management API and services under app platform */ secretsManagementAppPlatform?: boolean; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 4c7a2714cd6..17fbbef447a 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -241,6 +241,7 @@ export class GrafanaBootConfig { sharedWithMeFolderUID?: string; rootFolderUID?: string; localFileSystemAvailable?: boolean; + cloudMigrationEnabled?: boolean; cloudMigrationIsTarget?: boolean; cloudMigrationPollIntervalMs = 2000; reportingStaticContext?: Record; diff --git a/pkg/api/api.go b/pkg/api/api.go index 6a838206ba9..918ec160b69 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -121,8 +121,7 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/admin/provisioning", reqOrgAdmin, hs.Index) r.Get("/admin/provisioning/*", reqOrgAdmin, hs.Index) - //nolint:staticcheck // not yet migrated to OpenFeature - if hs.Features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) { + if hs.Cfg.CloudMigration.Enabled { r.Get("/admin/migrate-to-cloud", authorize(cloudmigration.MigrationAssistantAccess), hs.Index) } diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index 4d412a0e271..218817c6801 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -287,6 +287,7 @@ type FrontendSettingsDTO struct { PublicDashboardAccessToken string `json:"publicDashboardAccessToken"` PublicDashboardsEnabled bool `json:"publicDashboardsEnabled"` + CloudMigrationEnabled bool `json:"cloudMigrationEnabled"` CloudMigrationIsTarget bool `json:"cloudMigrationIsTarget"` CloudMigrationPollIntervalMs int `json:"cloudMigrationPollIntervalMs"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 8acfdea512e..2f223035bbb 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -191,8 +191,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro hasAccess := accesscontrol.HasAccess(hs.AccessControl, c) trustedTypesDefaultPolicyEnabled := (hs.Cfg.CSPEnabled && strings.Contains(hs.Cfg.CSPTemplate, "require-trusted-types-for")) || (hs.Cfg.CSPReportOnlyEnabled && strings.Contains(hs.Cfg.CSPReportOnlyTemplate, "require-trusted-types-for")) - //nolint:staticcheck // not yet migrated to OpenFeature - isCloudMigrationTarget := hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagOnPremToCloudMigrations) && hs.Cfg.CloudMigration.IsTarget + isCloudMigrationTarget := hs.Cfg.CloudMigration.Enabled && hs.Cfg.CloudMigration.IsTarget featureToggles := hs.Features.GetEnabled(c.Req.Context()) // this is needed for backwards compatibility with external plugins // we should remove this once we can be sure that no external plugins rely on this @@ -260,6 +259,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro PluginRestrictedAPIsBlockList: hs.Cfg.PluginRestrictedAPIsBlockList, PublicDashboardAccessToken: c.PublicDashboardAccessToken, PublicDashboardsEnabled: hs.Cfg.PublicDashboardsEnabled, + CloudMigrationEnabled: hs.Cfg.CloudMigration.Enabled, CloudMigrationIsTarget: isCloudMigrationTarget, CloudMigrationPollIntervalMs: int(hs.Cfg.CloudMigration.FrontendPollInterval.Milliseconds()), SharedWithMeFolderUID: folder.SharedWithMeFolderUID, diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index 30c8a4e409d..2f2befcc779 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -61,7 +61,6 @@ type Service struct { isSyncSnapshotStatusFromGMSRunning int32 - features featuremgmt.FeatureToggles gmsClient gmsclient.Client objectStorage objectstorage.ObjectStorage @@ -119,8 +118,7 @@ func ProvideService( libraryElementsService libraryelements.Service, ngAlert *ngalert.AlertNG, ) (cloudmigration.Service, error) { - //nolint:staticcheck // not yet migrated to OpenFeature - if !features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) { + if !cfg.CloudMigration.Enabled { return &NoopServiceImpl{}, nil } @@ -132,7 +130,6 @@ func ProvideService( store: &sqlStore{db: db, secretsStore: secretsStore, secretsService: secretsService}, log: log.New(LogPrefix), cfg: cfg, - features: features, dsService: dsService, tracer: tracer, metrics: newMetrics(), diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 2d393236801..9a99e2d702b 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -907,6 +907,7 @@ func setUpServiceTest(t *testing.T, cfgOverrides ...configOverrides) cloudmigrat _, err = section.NewKey("domain", "localhost:1234") require.NoError(t, err) + cfg.CloudMigration.Enabled = true cfg.CloudMigration.IsDeveloperMode = true // ensure local implementations are used cfg.CloudMigration.SnapshotFolder = filepath.Join(os.TempDir(), uuid.NewString()) @@ -919,15 +920,11 @@ func setUpServiceTest(t *testing.T, cfgOverrides ...configOverrides) cloudmigrat }, } - featureToggles := featuremgmt.WithFeatures( - featuremgmt.FlagOnPremToCloudMigrations, - ) + featureToggles := featuremgmt.WithFeatures() sqlStore := sqlstore.NewTestStore(t, sqlstore.WithCfg(cfg), - sqlstore.WithFeatureFlags( - featuremgmt.FlagOnPremToCloudMigrations, - ), + sqlstore.WithFeatureFlags(), ) kvStore := kvstore.ProvideService(sqlStore) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go index 611b10c317b..ee2c9889c20 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -43,7 +42,6 @@ func TestGetAlertMuteTimings(t *testing.T) { t.Parallel() s := setUpServiceTest(t).(*Service) - s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations) user := &user.SignedInUser{OrgID: 1} diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index de024e8f763..24e057cd882 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -667,13 +667,6 @@ var ( HideFromDocs: true, RequiresRestart: true, }, - { - Name: "onPremToCloudMigrations", - Description: "Enable the Grafana Migration Assistant, which helps you easily migrate various on-prem resources to your Grafana Cloud stack.", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaOperatorExperienceSquad, - Expression: "true", - }, { Name: "secretsManagementAppPlatform", Description: "Enable the secrets management API and services under app platform", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 076256f039d..fd1fe6619f9 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -92,7 +92,6 @@ kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false alertingQueryOptimization,GA,@grafana/alerting-squad,false,false,false jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false -onPremToCloudMigrations,GA,@grafana/grafana-operator-experience-squad,false,false,false secretsManagementAppPlatform,experimental,@grafana/grafana-operator-experience-squad,false,false,false secretsManagementAppPlatformUI,experimental,@grafana/grafana-operator-experience-squad,false,false,false alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 8d940cda9f1..f68009ab2e3 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -275,10 +275,6 @@ const ( // Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group. Disables sequential evaluation if enabled. FlagJitterAlertRulesWithinGroups = "jitterAlertRulesWithinGroups" - // FlagOnPremToCloudMigrations - // Enable the Grafana Migration Assistant, which helps you easily migrate various on-prem resources to your Grafana Cloud stack. - FlagOnPremToCloudMigrations = "onPremToCloudMigrations" - // FlagSecretsManagementAppPlatform // Enable the secrets management API and services under app platform FlagSecretsManagementAppPlatform = "secretsManagementAppPlatform" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 8561f371fe0..0e7dba8b5ec 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2412,7 +2412,8 @@ "metadata": { "name": "onPremToCloudMigrations", "resourceVersion": "1763734583253", - "creationTimestamp": "2024-01-22T16:09:08Z" + "creationTimestamp": "2024-01-22T16:09:08Z", + "deletionTimestamp": "2025-11-20T09:59:41Z" }, "spec": { "description": "Enable the Grafana Migration Assistant, which helps you easily migrate various on-prem resources to your Grafana Cloud stack.", diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index a12ed52b2f6..ca91ab4f915 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -44,8 +44,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink Text: "Organizations", SubTitle: "Isolated instances of Grafana running on the same server", Id: "global-orgs", Url: s.cfg.AppSubURL + "/admin/orgs", Icon: "building", }) } - //nolint:staticcheck // not yet migrated to OpenFeature - if hasAccess(cloudmigration.MigrationAssistantAccess) && s.features.IsEnabled(ctx, featuremgmt.FlagOnPremToCloudMigrations) { + if hasAccess(cloudmigration.MigrationAssistantAccess) && s.cfg.CloudMigration.Enabled { generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ Text: "Migrate to Grafana Cloud", Id: "migrate-to-cloud", diff --git a/pkg/setting/setting_cloud_migration.go b/pkg/setting/setting_cloud_migration.go index 87f549ca9cd..7c2138b262f 100644 --- a/pkg/setting/setting_cloud_migration.go +++ b/pkg/setting/setting_cloud_migration.go @@ -35,12 +35,14 @@ type CloudMigrationSettings struct { TokenExpiresAfter time.Duration FrontendPollInterval time.Duration + Enabled bool IsTarget bool IsDeveloperMode bool } func (cfg *Cfg) readCloudMigrationSettings() { cloudMigration := cfg.Raw.Section("cloud_migration") + cfg.CloudMigration.Enabled = cloudMigration.Key("enabled").MustBool(true) cfg.CloudMigration.IsTarget = cloudMigration.Key("is_target").MustBool(false) cfg.CloudMigration.GcomAPIToken = cloudMigration.Key("gcom_api_token").MustString("") cfg.CloudMigration.AuthAPIUrl = cloudMigration.Key("auth_api_url").MustString("") diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 462e564bbe5..78cf632a1b1 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -376,7 +376,7 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "ServerStats" */ 'app/features/admin/ServerStats') ), }, - config.featureToggles.onPremToCloudMigrations && { + config.cloudMigrationEnabled && { path: '/admin/migrate-to-cloud', roles: () => contextSrv.evaluatePermission([AccessControlAction.MigrationAssistantMigrate]), component: SafeDynamicImport( From 258173ea94d9a395b216ac1aa63b870aed1f5b56 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Mon, 24 Nov 2025 16:28:13 +0100 Subject: [PATCH 078/378] Dashboards: Fix margin of time controls (#114345) --- public/app/features/dashboard-scene/scene/DashboardControls.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 686a7f785e0..5268ea4d865 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -240,6 +240,7 @@ function getStyles(theme: GrafanaTheme2) { display: 'flex', justifyContent: 'flex-end', gap: theme.spacing(1), + marginBottom: theme.spacing(1), }), rightControlsWrap: css({ flexWrap: 'wrap', From 6a6c5e8ae0ee0770413bb10e13389a4e6f7cf955 Mon Sep 17 00:00:00 2001 From: Kim Nylander <104772500+knylander-grafana@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:33:59 -0500 Subject: [PATCH 079/378] Draft docs for LBAC for traces (#114280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Taylor C <41653732+tacole02@users.noreply.github.com> Co-authored-by: Irene Rodríguez --- .../configure-teamlbac-for-tempo/index.md | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-tempo/index.md diff --git a/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-tempo/index.md b/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-tempo/index.md new file mode 100644 index 00000000000..d2f6840b023 --- /dev/null +++ b/docs/sources/administration/data-source-management/teamlbac/configure-teamlbac-for-tempo/index.md @@ -0,0 +1,166 @@ +--- +title: "Configure Team LBAC for Tempo or Cloud Traces" +description: "Use label-based access control (LBAC) to restrict Cloud Traces data by team and attribute rules." +keywords: + - tempo + - datasource + - team +labels: + products: + - cloud +title: Configure team LBAC for data sources for Tempo +weight: 350 +_build: + list: false +noindex: true +--- + +# Configure team label-based access control for Tempo or Cloud Traces + +{{< docs/private-preview product="Label-based access control for traces" >}} + +Team label-based access control (LBAC) for Cloud Traces or Tempo lets you restrict which spans teams can access by defining rules using trace attributes. LBAC provides fine-grained, team-based access control within a single tenant and mirrors the experience used for logs and metrics LBAC in Grafana Cloud. + +Grafana uses the term LBAC as an umbrella term for label-based access control for all data sources. +Traces use **attributes**, not labels, for access control, but the Grafana UI surfaces this functionality as LBAC for consistency. + +This feature only applies to Grafana Cloud Traces, specifically, the Cloud-provisioned tracing data source. +If you want to use this feature with a Tempo data source, contact Grafana Support to make sure that this is enabled in your organization. + +## How LBAC works + +When a user queries tracing data source, Grafana evaluates the user’s team memberships and the LBAC rules assigned to those teams. These rules are added to the request so the Cloud Traces data source returns only permitted spans or attributes. + +LBAC rules use **attribute selectors**, such as: + +``` +{ resource.service.name="checkout", resource.env="prod" } +``` + +Multiple conditions in the same rule use **AND** (`,`), while multiple rules across teams use **OR**. + +## Before you begin + +- Be sure that you have the permission setup to create a Tempo or Cloud Traces tenant in Grafana Cloud. +- Be sure that you have administrator permissions for Grafana. +- Be sure that you have a team setup in Grafana. + +### Known limitations + +- Autocomplete in search is still under development. +- LBAC is restricted to only contain resource scope attributes. +- There is a slight performance degradation for users with multiple rules. + +## Configure team LBAC for traces + +Follow this workflow when adding a new data source. The data source must be hosted by Grafana and not self-managed. + +1. Start your Grafana Cloud instance. +2. Access Tempo or Cloud Traces data sources details for your stack. +3. Copy Tempo or Cloud Traces details and create a Cloud Access Policy. + - Copy the [details of your Tempo or Cloud Traces setup](https://grafana.com/docs/grafana-cloud/send-data/traces/set-up/locate-url-user-password/). +4. In Grafana Cloud, navigate to **Administration > Users and access > Cloud Access Policies**. + - Create an access policy for the Tempo or Cloud Traces data source. + - Ensure the access policy includes `traces:read` permissions. + - Ensure the access policy doesn't include `labels` rules. +5. In Grafana, select Tempo or Cloud Traces or create new data source. +6. Navigate back to the Tempo or Cloud Traces data source. + - Set up the Tempo or Cloud Traces data source using basic authentication. Use the [userID/tenantID](https://grafana.com/docs/grafana-cloud/send-data/traces/set-up/locate-url-user-password/) as the username. Use the token from your access policy as the password. + - Select **Save and connect**. +7. Go to the **Permissions** tab of the newly created Tempo or Cloud Traces data source. Here, you find the LBAC for data sources rules section. + +8. Choose a team from the **Team** dropdown. +9. Enter attribute selectors such as: + + ``` + { resource.service.name="checkout", resource.env="prod" } + ``` + + Refer to the Examples section below for more examples. + +10. Select **Save**. + +### Examples of LBAC rules + +An LBAC rule is a `logql` query that filters logs or metrics based on labels. +LBAC rules for traces also use some TraceQL syntax for attribute selection. +Each rule operates independently as its own filter, separate from other rules within a team. + +LBAC rules guidelines: + +- Use only resource scope attributes, for example `{ resource.env="prod" }`. +- Only string values are supported. +- Use double quotes for string values, for example: `{ resource.env="prod" }`. +- You can use regular expressions matching with `=~` operator, for example: `{ resource.team =~ "team-a|team-b" }`. +- You can have up to two conditions in the same rule using a comma (`,`) as an `AND` operator, for example: `{ resource.env="prod", resource.team="frontend" }`. + +Refer to [Create LBAC for data sources rules for a supported data source](https://grafana.com/docs/grafana/next/administration/data-source-management/teamlbac/create-teamlbac-rules/) for more information. + +#### Single rule + +Limit users to only see spans from the `payments` API. + +``` +{ resource.service.name="payments-api" } +``` + +This example matches spans with team A **or** team B. This single example is faster than using multiple rules with the same label. + +``` +{ resource.team =~ "team-a|team-b" } +``` + +#### Multiple rules + +Two rules combined to give user access to spans from `prod` environment or the `billing` team. + +``` +{ resource.env="prod" } +{ resource.team="billing" } +``` + +This example gives users access to spans from the `frontend` team in the `prod` environment or the `checkout` team but doesn't enforce additional conditions. The comma (`,`) acts as an `AND` operator within the same rule. + +``` +{ resource.env="prod", resource.team="frontend" } +{ resource.team="checkout" } +``` + +#### User on multiple teams + +Users on multiple teams receive access based on all combined LBAC rules assigned to each team. + +Team A → `{ resource.cluster="us-east-1" }` +Team B → `{ resource.service.name="frontend" }` + +## How LBAC affects returned data + +For Trace-by-ID endpoints, unauthorized spans show only minimal intrinsic fields: + +- `traceId`, `spanId`, `parentId` +- `name`, `kind` +- `timestamps` +- `status` + +For Search, metrics, autocomplete, only spans matching LBAC rules appear. + +## Manage LBAC rules + +To edit an existing LBAC rule, follow these steps: + +1. Open your stack and select your **Tempo** or **Cloud Traces** data source. +2. Select **Permissions**. +3. Scroll to **Data access**. +4. Select the rule you want to edit and click the Pencil (Edit) icon. +5. Modify **Attribute filters**. +6. Select **Save**. + +To delete an existing LBAC rule, follow these steps: + +Open your stack and select your **Tempo** or **Cloud Traces** data source. + +1. Select **Permissions**. +2. Scroll to **Data access**. +3. Select the rule you want to edit. +4. Select the **X** (Delete) icon. +5. Confirm deletion. From 3d95d2aa793629cc92dd2283fcc92861a86bb401 Mon Sep 17 00:00:00 2001 From: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:05:50 -0800 Subject: [PATCH 080/378] SQLExpressions: Add new schema inspector panel (#113545) * chore: initial approach * chore: keep editor and inspector height in tandem * chore: improve drawer animation * feat: hook it up for real * chore: i18n * chore: consistent errors/warnings * chore: remove unused className * chore: oops fix button name! * chore: set up refetch. call on apply query * chore: use getAPINamespace instead of "default" * chore: use dashboard timerange for api call * fix: shadow variable update * fix: add styles back to dependency array. it's weird but oh well * chore: reorder some code for readability * chore: default inspector open * chore: pr feedback - border radius * chore: pr feedback - don't filter queries * chore: pr feedback - button updates * refactor: improve schema inspector logic and loading state management * chore: refactor some logic + tests * chore: i18n --- .../components/expressions/Expression.tsx | 2 +- .../expressions/ExpressionQueryEditor.tsx | 2 +- .../sqlCompletionProvider.ts | 2 +- .../GenAI/GenAIExplanationDrawer.tsx | 0 .../GenAI/GenAISQLExplainButton.tsx | 6 +- .../GenAI/GenAISQLSuggestionsButton.tsx | 6 +- .../GenAI/GenAISuggestionsDrawer.tsx | 0 .../GenAI/SuggestionsDrawerButton.tsx | 0 .../GenAI/hooks/useSQLExplanations.ts | 0 .../GenAI/hooks/useSQLSuggestions.ts | 0 .../GenAI/sqlPromptConfig.ts | 0 .../{ => SqlExpressions}/GenAI/utils.ts | 0 .../SchemaInspector/SchemaInspectorPanel.tsx | 264 ++++++++++++++++++ .../SqlExpressions/SchemaInspector/utils.ts | 18 ++ .../{ => SqlExpressions}/SqlExpr.test.tsx | 105 ++++++- .../{ => SqlExpressions}/SqlExpr.tsx | 141 ++++++++-- .../SqlExpressions/hooks/useSQLSchemas.ts | 95 +++++++ public/locales/en-US/grafana.json | 13 + 18 files changed, 620 insertions(+), 34 deletions(-) rename public/app/features/expressions/components/{ => SqlExpressions/CompletionProvider}/sqlCompletionProvider.ts (95%) rename public/app/features/expressions/components/{ => SqlExpressions}/GenAI/GenAIExplanationDrawer.tsx (100%) rename public/app/features/expressions/components/{ => SqlExpressions}/GenAI/GenAISQLExplainButton.tsx (92%) rename public/app/features/expressions/components/{ => SqlExpressions}/GenAI/GenAISQLSuggestionsButton.tsx (94%) rename public/app/features/expressions/components/{ => SqlExpressions}/GenAI/GenAISuggestionsDrawer.tsx (100%) rename public/app/features/expressions/components/{ => SqlExpressions}/GenAI/SuggestionsDrawerButton.tsx (100%) rename public/app/features/expressions/components/{ => SqlExpressions}/GenAI/hooks/useSQLExplanations.ts (100%) rename public/app/features/expressions/components/{ => SqlExpressions}/GenAI/hooks/useSQLSuggestions.ts (100%) rename public/app/features/expressions/components/{ => SqlExpressions}/GenAI/sqlPromptConfig.ts (100%) rename public/app/features/expressions/components/{ => SqlExpressions}/GenAI/utils.ts (100%) create mode 100644 public/app/features/expressions/components/SqlExpressions/SchemaInspector/SchemaInspectorPanel.tsx create mode 100644 public/app/features/expressions/components/SqlExpressions/SchemaInspector/utils.ts rename public/app/features/expressions/components/{ => SqlExpressions}/SqlExpr.test.tsx (68%) rename public/app/features/expressions/components/{ => SqlExpressions}/SqlExpr.tsx (73%) create mode 100644 public/app/features/expressions/components/SqlExpressions/hooks/useSQLSchemas.ts diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index 173bca42a4c..79f805d52ad 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -18,7 +18,7 @@ import { ClassicConditions } from 'app/features/expressions/components/ClassicCo import { Math } from 'app/features/expressions/components/Math'; import { Reduce } from 'app/features/expressions/components/Reduce'; import { Resample } from 'app/features/expressions/components/Resample'; -import { SqlExpr } from 'app/features/expressions/components/SqlExpr'; +import { SqlExpr } from 'app/features/expressions/components/SqlExpressions/SqlExpr'; import { Threshold } from 'app/features/expressions/components/Threshold'; import { ExpressionQuery, diff --git a/public/app/features/expressions/ExpressionQueryEditor.tsx b/public/app/features/expressions/ExpressionQueryEditor.tsx index 538aa2e52f6..e08cdd5d94c 100644 --- a/public/app/features/expressions/ExpressionQueryEditor.tsx +++ b/public/app/features/expressions/ExpressionQueryEditor.tsx @@ -11,7 +11,7 @@ import { ExpressionTypeDropdown } from './components/ExpressionTypeDropdown'; import { Math } from './components/Math'; import { Reduce } from './components/Reduce'; import { Resample } from './components/Resample'; -import { SqlExpr } from './components/SqlExpr'; +import { SqlExpr } from './components/SqlExpressions/SqlExpr'; import { Threshold } from './components/Threshold'; import { ExpressionQuery, ExpressionQueryType, expressionTypes } from './types'; import { getDefaults } from './utils/expressionTypes'; diff --git a/public/app/features/expressions/components/sqlCompletionProvider.ts b/public/app/features/expressions/components/SqlExpressions/CompletionProvider/sqlCompletionProvider.ts similarity index 95% rename from public/app/features/expressions/components/sqlCompletionProvider.ts rename to public/app/features/expressions/components/SqlExpressions/CompletionProvider/sqlCompletionProvider.ts index 013088aa770..f94b08992bd 100644 --- a/public/app/features/expressions/components/sqlCompletionProvider.ts +++ b/public/app/features/expressions/components/SqlExpressions/CompletionProvider/sqlCompletionProvider.ts @@ -2,7 +2,7 @@ import { SelectableValue } from '@grafana/data'; import { ColumnDefinition, LanguageCompletionProvider, TableDefinition, TableIdentifier } from '@grafana/plugin-ui'; import { config } from '@grafana/runtime'; -import { ALLOWED_FUNCTIONS } from '../utils/metaSqlExpr'; +import { ALLOWED_FUNCTIONS } from '../../../utils/metaSqlExpr'; interface CompletionProviderGetterArgs { getFields: (t: TableIdentifier) => Promise; diff --git a/public/app/features/expressions/components/GenAI/GenAIExplanationDrawer.tsx b/public/app/features/expressions/components/SqlExpressions/GenAI/GenAIExplanationDrawer.tsx similarity index 100% rename from public/app/features/expressions/components/GenAI/GenAIExplanationDrawer.tsx rename to public/app/features/expressions/components/SqlExpressions/GenAI/GenAIExplanationDrawer.tsx diff --git a/public/app/features/expressions/components/GenAI/GenAISQLExplainButton.tsx b/public/app/features/expressions/components/SqlExpressions/GenAI/GenAISQLExplainButton.tsx similarity index 92% rename from public/app/features/expressions/components/GenAI/GenAISQLExplainButton.tsx rename to public/app/features/expressions/components/SqlExpressions/GenAI/GenAISQLExplainButton.tsx index 86c5953025a..cd9a1071c5f 100644 --- a/public/app/features/expressions/components/GenAI/GenAISQLExplainButton.tsx +++ b/public/app/features/expressions/components/SqlExpressions/GenAI/GenAISQLExplainButton.tsx @@ -2,9 +2,9 @@ import { useCallback } from 'react'; import { t } from '@grafana/i18n'; -import { GenAIButton } from '../../../dashboard/components/GenAI/GenAIButton'; -import { EventTrackingSrc } from '../../../dashboard/components/GenAI/tracking'; -import { Message, Role } from '../../../dashboard/components/GenAI/utils'; +import { GenAIButton } from '../../../../dashboard/components/GenAI/GenAIButton'; +import { EventTrackingSrc } from '../../../../dashboard/components/GenAI/tracking'; +import { Message, Role } from '../../../../dashboard/components/GenAI/utils'; import { getSQLExplanationSystemPrompt, QueryUsageContext } from './sqlPromptConfig'; diff --git a/public/app/features/expressions/components/GenAI/GenAISQLSuggestionsButton.tsx b/public/app/features/expressions/components/SqlExpressions/GenAI/GenAISQLSuggestionsButton.tsx similarity index 94% rename from public/app/features/expressions/components/GenAI/GenAISQLSuggestionsButton.tsx rename to public/app/features/expressions/components/SqlExpressions/GenAI/GenAISQLSuggestionsButton.tsx index 3b386879354..bdc839739c3 100644 --- a/public/app/features/expressions/components/GenAI/GenAISQLSuggestionsButton.tsx +++ b/public/app/features/expressions/components/SqlExpressions/GenAI/GenAISQLSuggestionsButton.tsx @@ -2,9 +2,9 @@ import { useCallback } from 'react'; import { t } from '@grafana/i18n'; -import { GenAIButton } from '../../../dashboard/components/GenAI/GenAIButton'; -import { EventTrackingSrc } from '../../../dashboard/components/GenAI/tracking'; -import { Message, Role } from '../../../dashboard/components/GenAI/utils'; +import { GenAIButton } from '../../../../dashboard/components/GenAI/GenAIButton'; +import { EventTrackingSrc } from '../../../../dashboard/components/GenAI/tracking'; +import { Message, Role } from '../../../../dashboard/components/GenAI/utils'; import { getSQLSuggestionSystemPrompt, QueryUsageContext } from './sqlPromptConfig'; diff --git a/public/app/features/expressions/components/GenAI/GenAISuggestionsDrawer.tsx b/public/app/features/expressions/components/SqlExpressions/GenAI/GenAISuggestionsDrawer.tsx similarity index 100% rename from public/app/features/expressions/components/GenAI/GenAISuggestionsDrawer.tsx rename to public/app/features/expressions/components/SqlExpressions/GenAI/GenAISuggestionsDrawer.tsx diff --git a/public/app/features/expressions/components/GenAI/SuggestionsDrawerButton.tsx b/public/app/features/expressions/components/SqlExpressions/GenAI/SuggestionsDrawerButton.tsx similarity index 100% rename from public/app/features/expressions/components/GenAI/SuggestionsDrawerButton.tsx rename to public/app/features/expressions/components/SqlExpressions/GenAI/SuggestionsDrawerButton.tsx diff --git a/public/app/features/expressions/components/GenAI/hooks/useSQLExplanations.ts b/public/app/features/expressions/components/SqlExpressions/GenAI/hooks/useSQLExplanations.ts similarity index 100% rename from public/app/features/expressions/components/GenAI/hooks/useSQLExplanations.ts rename to public/app/features/expressions/components/SqlExpressions/GenAI/hooks/useSQLExplanations.ts diff --git a/public/app/features/expressions/components/GenAI/hooks/useSQLSuggestions.ts b/public/app/features/expressions/components/SqlExpressions/GenAI/hooks/useSQLSuggestions.ts similarity index 100% rename from public/app/features/expressions/components/GenAI/hooks/useSQLSuggestions.ts rename to public/app/features/expressions/components/SqlExpressions/GenAI/hooks/useSQLSuggestions.ts diff --git a/public/app/features/expressions/components/GenAI/sqlPromptConfig.ts b/public/app/features/expressions/components/SqlExpressions/GenAI/sqlPromptConfig.ts similarity index 100% rename from public/app/features/expressions/components/GenAI/sqlPromptConfig.ts rename to public/app/features/expressions/components/SqlExpressions/GenAI/sqlPromptConfig.ts diff --git a/public/app/features/expressions/components/GenAI/utils.ts b/public/app/features/expressions/components/SqlExpressions/GenAI/utils.ts similarity index 100% rename from public/app/features/expressions/components/GenAI/utils.ts rename to public/app/features/expressions/components/SqlExpressions/GenAI/utils.ts diff --git a/public/app/features/expressions/components/SqlExpressions/SchemaInspector/SchemaInspectorPanel.tsx b/public/app/features/expressions/components/SqlExpressions/SchemaInspector/SchemaInspectorPanel.tsx new file mode 100644 index 00000000000..c84bf0d4223 --- /dev/null +++ b/public/app/features/expressions/components/SqlExpressions/SchemaInspector/SchemaInspectorPanel.tsx @@ -0,0 +1,264 @@ +import { css } from '@emotion/css'; +import { useState, useMemo } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { t, Trans } from '@grafana/i18n'; +import { + Stack, + Tab, + TabsBar, + TabContent, + Icon, + Badge, + Text, + useStyles2, + InteractiveTable, + ScrollContainer, + Alert, + Spinner, + IconButton, +} from '@grafana/ui'; + +import { SQLSchemas, SQLSchemaField, SQLSchemaData } from '../hooks/useSQLSchemas'; + +import { getFieldTypeIcon } from './utils'; + +type SchemaField = SQLSchemaField; +type SampleValue = string | number | boolean; +type SampleRow = SampleValue[]; +type SampleRows = SampleRow[]; +type SchemaData = SQLSchemaData; + +interface SchemaInspectorPanelProps { + schemas: SQLSchemas | null; + loading: boolean; + error: Error | null; + onClose: () => void; +} + +export const SchemaInspectorPanel = ({ schemas, loading, error, onClose }: SchemaInspectorPanelProps) => { + const styles = useStyles2(getStyles); + + const schemaResponse: SQLSchemas = schemas ?? {}; + const refIds = Object.keys(schemaResponse); + + const [selectedTab, setSelectedTab] = useState(''); + const activeSchemaTab = refIds.includes(selectedTab) ? selectedTab : refIds[0] || ''; + const activeSchemaData = schemaResponse[activeSchemaTab]; + + const columns = useMemo( + () => [ + { + id: 'field', + header: 'Field', + accessorKey: 'name', + cell: ({ row }: { row: { original: SchemaField } }) => ( + + +
{row.original.name}
+
+ ), + }, + { + id: 'type', + header: 'Type', + accessorKey: 'mysqlType', + cell: ({ row }: { row: { original: SchemaField } }) => , + }, + { + id: 'nullable', + header: 'Nullable', + accessorKey: 'nullable', + cell: ({ row }: { row: { original: SchemaField } }) => ( + + ), + }, + { + id: 'sample', + header: 'Sample values', + accessorKey: 'sample', + cell: ({ + row, + }: { + row: { + original: SchemaField & { + fieldIndex: number; + sampleRows: SchemaData['sampleRows']; + }; + }; + }) => { + const { fieldIndex, sampleRows } = row.original; + + // Extract sample values for this field (column index) + const sampleValues = sampleRows?.map((sampleRow) => sampleRow[fieldIndex]) ?? []; + + // Format as a proper array string with quoted strings + const arrayString = JSON.stringify(sampleValues); + + return
{arrayString}
; + }, + }, + ], + [styles] + ); + + const renderSchemaFields = (fields: SchemaField[], sampleRows: SampleRows | null) => { + // Enhance fields with fieldIndex and sampleRows for the Sample column + const enhancedFields = fields.map((field, index) => ({ + ...field, + fieldIndex: index, + sampleRows, + })); + + return ( +
+ name} + pageSize={0} // No pagination + /> +
+ ); + }; + + const renderSchemaTabContent = ({ columns, error, sampleRows }: SchemaData) => { + if (error) { + return ( +
+ + {error} + +
+ ); + } + + if (!columns || columns.length === 0) { + return ( +
+ + This query returned no schema information. + +
+ ); + } + + return renderSchemaFields(columns, sampleRows); + }; + + const renderContent = () => { + if (error) { + return ( +
+ + {error.message} + +
+ ); + } + + if (loading) { + return ( +
+ + + + Loading schema information... + + +
+ ); + } + + if (!activeSchemaData || refIds.length === 0) { + return ( +
+ +
+ ); + } + + return ( + + {renderSchemaTabContent(activeSchemaData)} + + ); + }; + + return ( +
+
+ {refIds.length > 0 && ( + + {refIds.map((refId) => ( + setSelectedTab(refId)} + /> + ))} + + )} + +
+ {renderContent()} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + schemaInfoContainer: css({ + padding: theme.spacing(1), + }), + schemaInspector: css({ + height: '100%', + display: 'flex', + flexDirection: 'column', + }), + // Unfortunate hack to get the close button to align with the tabs since we need to + // override the default styles of the TabsBar component. + tabsBarWrapper: css({ + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + width: '100%', + padding: `0 ${theme.spacing(1)}`, + }), + tableCell: css({ + fontSize: theme.typography.bodySmall.fontSize, + fontWeight: theme.typography.fontWeightMedium, + fontFamily: theme.typography.fontFamilyMonospace, + }), + tableContainer: css({ + margin: theme.spacing(1), + flex: 1, + overflowY: 'auto', + overflowX: 'auto', + minHeight: 0, // Allow flex child to shrink + border: `1px solid ${theme.colors.border.medium}`, + borderRadius: theme.shape.radius.default, + backgroundColor: theme.colors.background.primary, + }), + nullableIcon: css({ + color: theme.colors.success.text, + }), + requiredIcon: css({ + color: theme.colors.warning.text, + }), +}); diff --git a/public/app/features/expressions/components/SqlExpressions/SchemaInspector/utils.ts b/public/app/features/expressions/components/SqlExpressions/SchemaInspector/utils.ts new file mode 100644 index 00000000000..7412274b3e1 --- /dev/null +++ b/public/app/features/expressions/components/SqlExpressions/SchemaInspector/utils.ts @@ -0,0 +1,18 @@ +export const getFieldTypeIcon = (mysqlType: string) => { + switch (mysqlType.toLowerCase()) { + case 'text': + return 'font'; + case 'double': + case 'float': + case 'int': + case 'bigint': + return 'calculator-alt'; + case 'datetime': + case 'timestamp': + return 'clock-nine'; + case 'boolean': + return 'toggle-on'; + default: + return 'question-circle'; + } +}; diff --git a/public/app/features/expressions/components/SqlExpr.test.tsx b/public/app/features/expressions/components/SqlExpressions/SqlExpr.test.tsx similarity index 68% rename from public/app/features/expressions/components/SqlExpr.test.tsx rename to public/app/features/expressions/components/SqlExpressions/SqlExpr.test.tsx index 53931e785db..b7acc586447 100644 --- a/public/app/features/expressions/components/SqlExpr.test.tsx +++ b/public/app/features/expressions/components/SqlExpressions/SqlExpr.test.tsx @@ -1,6 +1,6 @@ -import { render, waitFor, fireEvent, act } from 'test/test-utils'; +import { render, waitFor, fireEvent, act, testWithFeatureToggles } from 'test/test-utils'; -import { ExpressionQuery, ExpressionQueryType } from '../types'; +import { ExpressionQuery, ExpressionQueryType } from '../../types'; import { SqlExpr, SqlExprProps } from './SqlExpr'; @@ -50,6 +50,20 @@ jest.mock('./GenAI/hooks/useSQLExplanations', () => ({ })), })); +// Mock the backend API +const mockBackendSrv = { + post: jest.fn().mockResolvedValue({ + kind: 'SQLSchemaResponse', + apiVersion: 'query.grafana.app/v0alpha1', + sqlSchemas: {}, + }), +}; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => mockBackendSrv, +})); + // Note: Add more mocks if needed for other lazy components describe('SqlExpr', () => { @@ -204,3 +218,90 @@ describe('SqlExpr with GenAI features', () => { expect(await findByTestId('explanation-drawer')).toBeInTheDocument(); }); }); + +describe('Schema Inspector feature toggle', () => { + const defaultProps: SqlExprProps = { + onChange: jest.fn(), + refIds: [{ value: 'A' }], + query: { refId: 'expression_1', type: ExpressionQueryType.sql, expression: `SELECT * FROM A LIMIT 10` }, + queries: [], + }; + + describe('when feature enabled', () => { + testWithFeatureToggles({ enable: ['queryService', 'grafanaAPIServerWithExperimentalAPIs'] }); + + afterEach(() => { + mockBackendSrv.post.mockResolvedValue({ + kind: 'SQLSchemaResponse', + apiVersion: 'query.grafana.app/v0alpha1', + sqlSchemas: {}, + }); + }); + + it('renders panel open by default', () => { + const { getByText } = render(); + + expect(getByText('No schema information available')).toBeInTheDocument(); + }); + + it('closes panel and shows reopen button when close button clicked', async () => { + const { queryByText, getByLabelText, findByText } = render(); + + expect(queryByText('No schema information available')).toBeInTheDocument(); + + const closeButton = getByLabelText('Close schema inspector'); + await act(async () => fireEvent.click(closeButton)); + + expect(queryByText('No schema information available')).not.toBeInTheDocument(); + expect(await findByText('Inspect schema')).toBeInTheDocument(); + }); + + it('reopens panel when inspect schema button clicked after closing', async () => { + const { queryByText, getByLabelText, getByText } = render(); + + const closeButton = getByLabelText('Close schema inspector'); + await act(async () => fireEvent.click(closeButton)); + + expect(queryByText('No schema information available')).not.toBeInTheDocument(); + + const reopenButton = getByText('Inspect schema'); + await act(async () => fireEvent.click(reopenButton)); + + expect(queryByText('No schema information available')).toBeInTheDocument(); + }); + + it('renders tabs for multiple query refIds', async () => { + mockBackendSrv.post.mockResolvedValue({ + kind: 'SQLSchemaResponse', + apiVersion: 'query.grafana.app/v0alpha1', + sqlSchemas: { + A: { columns: [], sampleRows: [] }, + B: { columns: [], sampleRows: [] }, + C: { columns: [], sampleRows: [] }, + }, + }); + + const propsWithQueries = { + ...defaultProps, + queries: [{ refId: 'A' }, { refId: 'B' }, { refId: 'C' }], + }; + + const { findByRole } = render(); + + expect(await findByRole('tab', { name: 'A' })).toBeInTheDocument(); + expect(await findByRole('tab', { name: 'B' })).toBeInTheDocument(); + expect(await findByRole('tab', { name: 'C' })).toBeInTheDocument(); + }); + }); + + describe('when feature disabled', () => { + testWithFeatureToggles({ enable: [] }); + + it('does not render panel or button', () => { + const { queryByText } = render(); + + expect(queryByText('Inspect schema')).not.toBeInTheDocument(); + expect(queryByText('No schema information available')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/expressions/components/SqlExpr.tsx b/public/app/features/expressions/components/SqlExpressions/SqlExpr.tsx similarity index 73% rename from public/app/features/expressions/components/SqlExpr.tsx rename to public/app/features/expressions/components/SqlExpressions/SqlExpr.tsx index 8ae4d704962..361d748b716 100644 --- a/public/app/features/expressions/components/SqlExpr.tsx +++ b/public/app/features/expressions/components/SqlExpressions/SqlExpr.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { useMemo, useRef, useEffect, useState, lazy, Suspense, useCallback } from 'react'; import { useMeasure } from 'react-use'; import AutoSizer from 'react-virtualized-auto-sizer'; @@ -11,14 +11,16 @@ import { DataQuery } from '@grafana/schema/dist/esm/index'; import { formatSQL } from '@grafana/sql'; import { useStyles2, Stack, Button, Modal } from '@grafana/ui'; -import { ExpressionQueryEditorProps } from '../ExpressionQueryEditor'; -import { SqlExpressionQuery } from '../types'; -import { fetchSQLFields } from '../utils/metaSqlExpr'; +import { ExpressionQueryEditorProps } from '../../ExpressionQueryEditor'; +import { SqlExpressionQuery } from '../../types'; +import { fetchSQLFields } from '../../utils/metaSqlExpr'; +import { QueryToolbox } from '../QueryToolbox'; +import { getSqlCompletionProvider } from './CompletionProvider/sqlCompletionProvider'; import { useSQLExplanations } from './GenAI/hooks/useSQLExplanations'; import { useSQLSuggestions } from './GenAI/hooks/useSQLSuggestions'; -import { QueryToolbox } from './QueryToolbox'; -import { getSqlCompletionProvider } from './sqlCompletionProvider'; +import { SchemaInspectorPanel } from './SchemaInspector/SchemaInspectorPanel'; +import { useSQLSchemas } from './hooks/useSQLSchemas'; // Lazy load the GenAI components to avoid circular dependencies const GenAISQLSuggestionsButton = lazy(() => @@ -53,6 +55,7 @@ const GenAIExplanationDrawer = lazy(() => // Account for Monaco editor's border to prevent clipping const EDITOR_BORDER_ADJUSTMENT = 2; // 1px border on top and bottom +const EDITOR_HEIGHT = 300; export interface SqlExprProps { refIds: Array>; @@ -90,11 +93,12 @@ FROM LIMIT 10`; - const styles = useStyles2(getStyles); - const containerRef = useRef(null); const [dimensions, setDimensions] = useState({ height: 0 }); + const styles = useStyles2((theme) => getStyles(theme, dimensions.height || EDITOR_HEIGHT)); + const containerRef = useRef(null); const [toolboxRef, toolboxMeasure] = useMeasure(); const [isExpanded, setIsExpanded] = useState(false); + const [isSchemaInspectorOpen, setIsSchemaInspectorOpen] = useState(true); const { handleApplySuggestion, handleHistoryUpdate, handleCloseDrawer, handleOpenDrawer, isDrawerOpen, suggestions } = useSQLSuggestions(); @@ -109,6 +113,18 @@ LIMIT updatePrevExpression, } = useSQLExplanations(query.expression || ''); + const { + schemas, + loading: schemasLoading, + error: schemasError, + isFeatureEnabled: isSchemasFeatureEnabled, + refetch: refetchSchemas, + } = useSQLSchemas({ + queries, + enabled: isSchemaInspectorOpen, + timeRange: metadata?.range, + }); + const queryContext = useMemo( () => ({ alerting, @@ -172,7 +188,12 @@ LIMIT onRunQuery(); } - }, [onRunQuery]); + + // Refetch schemas when query is run (only if inspector is open) + if (isSchemaInspectorOpen) { + refetchSchemas(); + } + }, [onRunQuery, refetchSchemas, isSchemaInspectorOpen]); // Set up resize observer to handle container resizing useEffect(() => { @@ -224,6 +245,17 @@ LIMIT const renderSQLButtons = () => (
+ {isSchemasFeatureEnabled && !isSchemaInspectorOpen && ( + + )} @@ -273,16 +305,32 @@ LIMIT <>
{renderSQLButtons()} -
- - {({ formatQuery }) => renderToolbox(formatQuery)} - +
+
+ + {({ formatQuery }) => renderToolbox(formatQuery)} + +
+ {isSchemaInspectorOpen && isSchemasFeatureEnabled && ( +
+ setIsSchemaInspectorOpen(false)} + /> +
+ )}
@@ -335,19 +383,35 @@ LIMIT ); }; -const getStyles = (theme: GrafanaTheme2) => ({ +const getStyles = (theme: GrafanaTheme2, editorHeight: number) => ({ sqlContainer: css({ display: 'grid', + gap: theme.spacing(1), gridTemplateRows: 'auto 1fr', gridTemplateAreas: ` "buttons" - "editor" + "content" `, - gap: theme.spacing(0.5), + }), + + contentContainer: css({ + gridArea: 'content', + display: 'grid', + gap: theme.spacing(1), + gridTemplateColumns: '1fr 0fr', + gridTemplateAreas: '"editor schema"', + [theme.transitions.handleMotion('no-preference')]: { + transition: theme.transitions.create(['grid-template-columns'], { + duration: theme.transitions.duration.standard, + }), + }, + }), + contentContainerWithSchema: css({ + gridTemplateColumns: '1fr 1fr', }), editorContainer: css({ gridArea: 'editor', - height: '240px', + height: editorHeight, // Use dynamic height from ResizeObserver resize: 'vertical', overflow: 'auto', minHeight: '100px', @@ -374,6 +438,37 @@ const getStyles = (theme: GrafanaTheme2) => ({ alignItems: 'center', gap: theme.spacing(1), }), + schemaInspector: css({ + gridArea: 'schema', + height: editorHeight, + overflow: 'hidden', + minWidth: 0, + }), + schemaInspectorOpen: css({ + border: `1px solid ${theme.colors.border.weak}`, + borderRadius: theme.shape.radius.default, + }), + schemaFields: css({ + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(1), + padding: theme.spacing(1), + maxHeight: '120px', + overflowY: 'auto', + }), + fieldItem: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.5), + padding: theme.spacing(1), + backgroundColor: theme.colors.background.secondary, + borderRadius: theme.shape.radius.default, + border: `1px solid ${theme.colors.border.weak}`, + fontSize: theme.typography.bodySmall.fontSize, + }), + responseContainer: css({ + padding: theme.spacing(2), + }), }); async function fetchFields(identifier: TableIdentifier, queries: DataQuery[]) { diff --git a/public/app/features/expressions/components/SqlExpressions/hooks/useSQLSchemas.ts b/public/app/features/expressions/components/SqlExpressions/hooks/useSQLSchemas.ts new file mode 100644 index 00000000000..fd9f5f41a1e --- /dev/null +++ b/public/app/features/expressions/components/SqlExpressions/hooks/useSQLSchemas.ts @@ -0,0 +1,95 @@ +import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; + +import { getAPINamespace } from '@grafana/api-clients'; +import { getDefaultTimeRange, TimeRange } from '@grafana/data'; +import { config, getBackendSrv } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; + +export interface SQLSchemaField { + name: string; + mysqlType: string; + dataFrameFieldType: string; + nullable: boolean; +} + +export interface SQLSchemaData { + columns: SQLSchemaField[] | null; + sampleRows: Array> | null; + error?: string; +} + +export type SQLSchemas = Record; + +export interface SQLSchemasResponse { + kind: string; + apiVersion: string; + sqlSchemas: SQLSchemas; +} + +interface UseSQLSchemasOptions { + queries?: DataQuery[]; + enabled: boolean; + timeRange?: TimeRange; +} + +export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOptions) { + const isFeatureEnabled = useMemo( + () => config.featureToggles.queryService || config.featureToggles.grafanaAPIServerWithExperimentalAPIs || false, + [] + ); + + // Start with loading=true if we're going to fetch on mount + const [schemas, setSchemas] = useState(null); + const [loading, setLoading] = useState(enabled && isFeatureEnabled && Boolean(queries)); + const [error, setError] = useState(null); + + // Store queries in ref so we can access current value without triggering effect + const queriesRef = useRef(queries); + queriesRef.current = queries; + + const fetchSchemas = useCallback(async () => { + if (!enabled || !isFeatureEnabled) { + return; + } + + const currentQueries = queriesRef.current; + if (!currentQueries) { + return; + } + + setLoading(true); + setError(null); + + try { + if (currentQueries.length === 0) { + setSchemas({ kind: 'SQLSchemaResponse', apiVersion: 'query.grafana.app/v0alpha1', sqlSchemas: {} }); + setLoading(false); + return; + } + + const namespace = getAPINamespace(); + const currentTimeRange = timeRange || getDefaultTimeRange(); + + const response = await getBackendSrv().post( + `/apis/query.grafana.app/v0alpha1/namespaces/${namespace}/sqlschemas/name`, + { + queries: currentQueries, + from: currentTimeRange.from.toISOString(), + to: currentTimeRange.to.toISOString(), + } + ); + + setSchemas(response); + } catch (err) { + setError(err instanceof Error ? err : new Error('Failed to fetch SQL schemas')); + } finally { + setLoading(false); + } + }, [enabled, isFeatureEnabled, timeRange]); + + useEffect(() => { + fetchSchemas(); + }, [fetchSchemas]); + + return { schemas, loading, error, isFeatureEnabled, refetch: fetchSchemas }; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 874be92a233..1da6f55fa78 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -7730,11 +7730,24 @@ "label-upsample": "Upsample", "tooltip-s-m-h": "10s, 1m, 30m, 1h" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "Close schema inspector" + }, "sql-expr": { "button-run-query": "Run query", "modal-title": "SQL Editor", "tooltip-experimental": "SQL Expressions LLM integration is experimental. Please report any issues to the Grafana team." }, + "sql-schema": { + "close-schema-inspector": "Close schema inspector", + "error-title": "Error", + "inspect-button": "Inspect schema", + "loading": "Loading schema information...", + "no-data-title": "No schema information available", + "no-fields-desc": "This query returned no schema information.", + "no-fields-title": "No schema information", + "query-error-title": "Query error" + }, "threshold": { "label-input": "Input" } From 11814426e4f643bcf1051632544540abe26044da Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Mon, 24 Nov 2025 09:51:54 -0700 Subject: [PATCH 081/378] Dashboard Schema V2: Support alerting for v2 schema in alert rule editor (#113891) * implement alerting for v2 schema * clean up * clarify * add support for library panels * comment * include missing library panel when v1 returns v2; cleanup * Use type union * remove unneccesary check * add AlertStatesDataLayer so that alert state is displayed next to panel title * fix test * cleanup undefined --- .../rule-editor/AnnotationsStep.tsx | 15 ++- .../rule-editor/DashboardAnnotationField.tsx | 12 +-- .../rule-editor/DashboardPicker.tsx | 91 ++++++++++++++++--- .../rule-editor/useDashboardQuery.ts | 29 ++++-- .../transformSaveModelSchemaV2ToScene.ts | 11 +++ 5 files changed, 122 insertions(+), 36 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx index b7ef11375f0..52d8cab8b70 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx @@ -8,7 +8,6 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Button, Field, Input, Stack, Text, TextArea, useStyles2 } from '@grafana/ui'; -import { DashboardModel } from '../../../../dashboard/state/DashboardModel'; import { AIImproveAnnotationsButtonComponent } from '../../enterprise-components/AI/AIGenImproveAnnotationsButton/addAIImproveAnnotationsButton'; import { RuleFormValues } from '../../types/rule-form'; import { Annotation, annotationLabels } from '../../utils/constants'; @@ -19,7 +18,7 @@ import DashboardAnnotationField from './DashboardAnnotationField'; import { DashboardPicker, PanelDTO, getVisualPanels } from './DashboardPicker'; import { NeedHelpInfo } from './NeedHelpInfo'; import { RuleEditorSection } from './RuleEditorSection'; -import { useDashboardQuery } from './useDashboardQuery'; +import { DashboardResponse, useDashboardQuery } from './useDashboardQuery'; const AnnotationsStep = () => { const styles = useStyles2(getStyles); @@ -40,22 +39,22 @@ const AnnotationsStep = () => { const selectedDashboardUid = annotations.find((annotation) => annotation.key === Annotation.dashboardUID)?.value; const selectedPanelId = Number(annotations.find((annotation) => annotation.key === Annotation.panelID)?.value); - const [selectedDashboard, setSelectedDashboard] = useState(undefined); + const [selectedDashboard, setSelectedDashboard] = useState(undefined); const [selectedPanel, setSelectedPanel] = useState(undefined); - const { dashboardModel, isFetching: isDashboardFetching } = useDashboardQuery(selectedDashboardUid); + const { dashboard, isFetching: isDashboardFetching } = useDashboardQuery(selectedDashboardUid); useEffect(() => { - if (isDashboardFetching || !dashboardModel) { + if (isDashboardFetching || !dashboard) { return; } - setSelectedDashboard(dashboardModel); + setSelectedDashboard(dashboard); - const allPanels = getVisualPanels(dashboardModel); + const allPanels = getVisualPanels(dashboard); const currentPanel = allPanels.find((panel) => panel.id === selectedPanelId); setSelectedPanel(currentPanel); - }, [selectedPanelId, dashboardModel, isDashboardFetching]); + }, [selectedPanelId, dashboard, isDashboardFetching]); const setSelectedDashboardAndPanelId = (dashboardUid: string, panelId: number) => { const updatedAnnotations = produce(annotations, (draft) => { diff --git a/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx b/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx index 26d32af3104..3354a8b102c 100644 --- a/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx @@ -3,11 +3,11 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { Icon, Text, useStyles2 } from '@grafana/ui'; -import { DashboardDataDTO } from 'app/types/dashboard'; import { makeDashboardLink, makePanelLink } from '../../utils/misc'; -import { PanelDTO } from './DashboardPicker'; +import { PanelDTO, getDashboardTitle, getDashboardUid } from './DashboardPicker'; +import { DashboardResponse } from './useDashboardQuery'; const DashboardAnnotationField = ({ dashboard, @@ -17,7 +17,7 @@ const DashboardAnnotationField = ({ onEditClick, onDeleteClick, }: { - dashboard?: DashboardDataDTO; + dashboard?: DashboardResponse; panel?: PanelDTO; dashboardUid: string; //fallback panelId: string; //fallback @@ -26,8 +26,8 @@ const DashboardAnnotationField = ({ }) => { const styles = useStyles2(getStyles); - const dashboardLink = makeDashboardLink(dashboard?.uid || dashboardUid); - const panelLink = makePanelLink(dashboard?.uid || dashboardUid, panel?.id?.toString() || panelId); + const dashboardLink = makeDashboardLink(getDashboardUid(dashboard) || dashboardUid); + const panelLink = makePanelLink(getDashboardUid(dashboard) || dashboardUid, panel?.id?.toString() || panelId); return (
{dashboard && ( @@ -38,7 +38,7 @@ const DashboardAnnotationField = ({ rel="noreferrer" data-testid="dashboard-annotation" > - {dashboard.title} + {getDashboardTitle(dashboard)} )} diff --git a/public/app/features/alerting/unified/components/rule-editor/DashboardPicker.tsx b/public/app/features/alerting/unified/components/rule-editor/DashboardPicker.tsx index afd4a4e80f2..68fa9d2df4d 100644 --- a/public/app/features/alerting/unified/components/rule-editor/DashboardPicker.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DashboardPicker.tsx @@ -7,6 +7,7 @@ import { FixedSizeList } from 'react-window'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { Alert, Button, @@ -18,11 +19,13 @@ import { clearButtonStyles, useStyles2, } from '@grafana/ui'; +import { AnnoKeyFolderTitle } from 'app/features/apiserver/types'; +import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; +import { isDashboardV2Resource } from 'app/features/dashboard/api/utils'; import { getGrafanaSearcher } from 'app/features/search/service/searcher'; +import { DashboardDTO } from 'app/types/dashboard'; -import { DashboardModel } from '../../../../dashboard/state/DashboardModel'; - -import { useDashboardQuery } from './useDashboardQuery'; +import { DashboardResponse, useDashboardQuery } from './useDashboardQuery'; export interface PanelDTO { id?: number; @@ -74,7 +77,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis const [debouncedDashboardFilter, setDebouncedDashboardFilter] = useState(''); const [panelFilter, setPanelFilter] = useState(''); const { value, loading: isDashSearchFetching } = useFilteredDashboards(debouncedDashboardFilter); - const { dashboardModel, isFetching: isDashboardFetching } = useDashboardQuery(selectedDashboardUid); + const { dashboard, isFetching: isDashboardFetching } = useDashboardQuery(selectedDashboardUid); const handleDashboardChange = useCallback((dashboardUid: string) => { setSelectedDashboardUid(dashboardUid); setSelectedPanelId(undefined); @@ -82,7 +85,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis const { dashboards: filteredDashboards = [], locationInfo: locationInfo = {} } = value || {}; - const allDashboardPanels = getVisualPanels(dashboardModel); + const allDashboardPanels = getVisualPanels(dashboard); const filteredPanels = allDashboardPanels @@ -197,7 +200,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis contentClassName={styles.modalContent} > {/* This alert shows if the selected dashboard is not found in the first page of dashboards */} - {!selectedDashboardIsInPageResult && dashboardUid && dashboardModel && ( + {!selectedDashboardIsInPageResult && dashboardUid && dashboard && ( Dashboard: {'{{dashboardTitle}}'} ({'{{ dashboardUid }}'}) in folder {'{{ folderTitle }}'} @@ -318,13 +321,35 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis ); }; -export function getVisualPanels(dashboardModel: DashboardModel | undefined) { - if (!dashboardModel) { +export function getVisualPanels(dashboardDTO: DashboardResponse | undefined) { + if (!dashboardDTO || !('dashboard' in dashboardDTO)) { return []; } - const panelsWithoutRows = dashboardModel.panels.filter((panel) => panel.type !== 'row'); - const panelsNestedInRows = dashboardModel.panels + // process v2 dashboard + if (isDashboardV2Resource(dashboardDTO)) { + return Object.values(dashboardDTO.spec.elements).map((element) => ({ + id: element.spec.id, + title: element.spec.title, + type: element.kind === 'Panel' ? element.spec.vizConfig.group : 'LibraryPanel', + ...(element.kind === 'LibraryPanel' && { + libraryPanel: { + uid: element.spec.libraryPanel.uid, + name: element.spec.libraryPanel.name, + }, + }), + })); + } + + // process v1 dashboard + const { dashboard } = dashboardDTO; + + if (!dashboard || !dashboard.panels) { + return []; + } + + const panelsWithoutRows = dashboard.panels.filter((panel) => panel.type !== 'row'); + const panelsNestedInRows = dashboard.panels .filter((rowPanel) => rowPanel.collapsed) .flatMap((collapsedRow) => collapsedRow.panels ?? []); @@ -332,6 +357,46 @@ export function getVisualPanels(dashboardModel: DashboardModel | undefined) { return allDashboardPanels; } +export function getDashboardTitle(dashboardDTO: DashboardResponse | undefined) { + if (!dashboardDTO || !('dashboard' in dashboardDTO)) { + return ''; + } + + if (isDashboardV2Resource(dashboardDTO)) { + return dashboardDTO.spec.title; + } + + return dashboardDTO.dashboard.title; +} + +export function getDashboardUid(dashboardDTO: DashboardResponse | undefined) { + if (!dashboardDTO || !('dashboard' in dashboardDTO)) { + return ''; + } + + if (isDashboardV2Resource(dashboardDTO)) { + return dashboardDTO.metadata.name; + } + + return dashboardDTO.dashboard.uid; +} + +export function getDashboardFolderTitle( + dashboardDTO: DashboardDTO | DashboardWithAccessInfo | undefined +) { + if (!dashboardDTO || !('dashboard' in dashboardDTO)) { + return undefined; + } + + if (isDashboardV2Resource(dashboardDTO)) { + return dashboardDTO.metadata.annotations?.[AnnoKeyFolderTitle]; + } + + const { meta } = dashboardDTO; + + return meta.folderTitle; +} + const isValidPanel = (panel: PanelDTO): boolean => { const hasValidID = typeof panel.id === 'number'; const isValidPanelType = typeof panel.type === 'string'; diff --git a/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts b/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts index 3427bd0486b..22c95aa797f 100644 --- a/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts +++ b/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts @@ -1,36 +1,47 @@ import memoizeOne from 'memoize-one'; import { useEffect, useState } from 'react'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; +import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; +import { isDashboardV2Resource } from 'app/features/dashboard/api/utils'; import { DashboardDTO } from 'app/types/dashboard'; import { DashboardModel } from '../../../../dashboard/state/DashboardModel'; -const convertToDashboardModel = memoizeOne((dashboardDTO: DashboardDTO) => { +export type DashboardResponse = DashboardDTO | DashboardWithAccessInfo; + +const ensureV1PanelsHaveIds = memoizeOne((dashboardDTO: DashboardDTO): DashboardResponse => { // RTKQuery freezes all returned objects. DashboardModel constructor runs migrations which might change the internal object // Hence we need to add structuredClone to make a deep copy of the API response object - const { dashboard, meta } = structuredClone(dashboardDTO); - return new DashboardModel(dashboard, meta); + const dashboardDTOClone = structuredClone(dashboardDTO); + const model = new DashboardModel(dashboardDTOClone.dashboard, dashboardDTOClone.meta); + + dashboardDTOClone.dashboard.panels = model.panels; + + return dashboardDTOClone; }); export function useDashboardQuery(dashboardUid?: string) { - const [dashboardModel, setDashboardModel] = useState(); + const [dashboard, setDashboard] = useState(); const [isFetching, setIsFetching] = useState(false); useEffect(() => { if (dashboardUid) { setIsFetching(true); getDashboardAPI() .getDashboardDTO(dashboardUid) - .then((dashboard) => { - if (!('dashboard' in dashboard)) { - console.error('Something went wrong, unexpected dashboard format'); + .then((dashboardDTO) => { + if ('dashboard' in dashboardDTO) { + setDashboard(ensureV1PanelsHaveIds(dashboardDTO)); + } else if (isDashboardV2Resource(dashboardDTO)) { + setDashboard(dashboardDTO); } else { - setDashboardModel(convertToDashboardModel(dashboard)); + console.error('Something went wrong, unexpected dashboard format'); } setIsFetching(false); }); } }, [dashboardUid]); - return { dashboardModel, isFetching }; + return { dashboard, isFetching }; } diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 0534ad76e56..683f22c16fb 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -64,6 +64,7 @@ import { DashboardMeta } from 'app/types/dashboard'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; import { dashboardAnalyticsInitializer } from '../behaviors/DashboardAnalyticsInitializerBehavior'; +import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; @@ -123,6 +124,15 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo Date: Mon, 24 Nov 2025 12:39:29 -0500 Subject: [PATCH 082/378] Sparkline: Move codeownership to dataviz (#114318) --- .github/CODEOWNERS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 77b60baf905..61ab3a10b1f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -721,9 +721,9 @@ i18next.config.ts @grafana/grafana-frontend-platform /packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad /packages/grafana-ui/src/components/RadialGauge/ @grafana/dataviz-squad /packages/grafana-ui/src/components/PluginSignatureBadge/ @grafana/plugins-platform-frontend -/packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations +/packages/grafana-ui/src/components/Sparkline/ @grafana/dataviz-squad /packages/grafana-ui/src/components/Table/ @grafana/dataviz-squad -/packages/grafana-ui/src/components/Table/Cells/SparklineCell.tsx @grafana/dataviz-squad @grafana/app-o11y-visualizations +/packages/grafana-ui/src/components/Table/Cells/SparklineCell.tsx @grafana/dataviz-squad /packages/grafana-ui/src/components/uPlot/ @grafana/dataviz-squad /packages/grafana-ui/src/components/ValuePicker/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizLayout/ @grafana/dataviz-squad @@ -992,7 +992,7 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform /public/app/plugins/panel/state-timeline/ @grafana/dataviz-squad /public/app/plugins/panel/status-history/ @grafana/dataviz-squad /public/app/plugins/panel/table/ @grafana/dataviz-squad -/public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx @grafana/dataviz-squad @grafana/app-o11y-visualizations +/public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx @grafana/dataviz-squad /public/app/plugins/panel/timeseries/ @grafana/dataviz-squad /public/app/plugins/panel/trend/ @grafana/dataviz-squad /public/app/plugins/panel/geomap/ @grafana/dataviz-squad From 335111e783c59ff37a77640e976957a1052bdf99 Mon Sep 17 00:00:00 2001 From: Eric Hilse Date: Mon, 24 Nov 2025 10:49:41 -0700 Subject: [PATCH 083/378] Plugins: Add extension points for grafana-setupguide-app in top nav and menu (#114272) * feat: add plugin extension point to SingleTopBar for setup guide integration * feat: add plugin extension point for grafana-setupguide-app in MegaMenu * feat: add TopBarExtensionPoint for grafana-setupguide-app integration * feat: add MegaMenuExtensionPoint for grafana-setupguide-app integration * feat: simplify MegaMenu and TopBar extension points by removing unused context checks * feat: add MegaMenuAction and SingleTopBarAction to PluginExtensionPoints * refactor: simplify return statement in MegaMenu and TopBar extension points * feat: update extension point IDs in MegaMenu and TopBar components to use PluginExtensionPoints --- .../src/types/pluginExtensions.ts | 2 ++ .../AppChrome/MegaMenu/MegaMenu.tsx | 28 +++++++++++-------- .../MegaMenu/MegaMenuExtensionPoint.tsx | 25 +++++++++++++++++ .../AppChrome/TopBar/SingleTopBar.tsx | 2 ++ .../AppChrome/TopBar/TopBarExtensionPoint.tsx | 25 +++++++++++++++++ .../extensions/usePluginComponents.test.tsx | 3 ++ public/test/setupTests.ts | 12 ++++++++ 7 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 public/app/core/components/AppChrome/MegaMenu/MegaMenuExtensionPoint.tsx create mode 100644 public/app/core/components/AppChrome/TopBar/TopBarExtensionPoint.tsx diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 05e7b2b144e..40d20b88c38 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -218,6 +218,8 @@ export enum PluginExtensionPoints { LogsViewResourceAttributes = 'grafana/logsview/resource-attributes', AppChrome = 'grafana/app/chrome/v1', ExtensionSidebar = 'grafana/extension-sidebar/v0-alpha', + MegaMenuAction = 'grafana/megamenu/action', + SingleTopBarAction = 'grafana/singletopbar/action', } // Don't use directly in a plugin! diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index 16d100b3bb4..dead4bd810e 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -13,6 +13,7 @@ import { useGrafana } from 'app/core/context/GrafanaContext'; import { setBookmark } from 'app/core/reducers/navBarTree'; import { useDispatch, useSelector } from 'app/types/store'; +import { MegaMenuExtensionPoint } from './MegaMenuExtensionPoint'; import { MegaMenuHeader } from './MegaMenuHeader'; import { MegaMenuItem } from './MegaMenuItem'; import { usePinnedItems } from './hooks'; @@ -110,18 +111,21 @@ export const MegaMenu = memo(
diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuExtensionPoint.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuExtensionPoint.tsx new file mode 100644 index 00000000000..96e667ac6ec --- /dev/null +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuExtensionPoint.tsx @@ -0,0 +1,25 @@ +import { PluginExtensionPoints } from '@grafana/data'; +import { renderLimitedComponents } from '@grafana/runtime'; +import { usePluginComponents } from 'app/features/plugins/extensions/usePluginComponents'; + +/** + * Extension point for plugins to add components to the mega menu. + * Currently restricted to grafana-setupguide-app plugin. + */ +export function MegaMenuExtensionPoint() { + const { components } = usePluginComponents({ + extensionPointId: PluginExtensionPoints.MegaMenuAction, + }); + + // Return null if no components are registered + if (components.length === 0) { + return null; + } + + return renderLimitedComponents({ + props: {}, + components: components, + limit: 1, + pluginId: 'grafana-setupguide-app', + }); +} diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx index d61dfdb46de..c68f5572b06 100644 --- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx @@ -28,6 +28,7 @@ import { InviteUserButton } from './InviteUserButton'; import { ProfileButton } from './ProfileButton'; import { SignInLink } from './SignInLink'; import { SingleTopBarActions } from './SingleTopBarActions'; +import { TopBarExtensionPoint } from './TopBarExtensionPoint'; import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger'; import { getChromeHeaderLevelHeight } from './useChromeHeaderHeight'; @@ -94,6 +95,7 @@ export const SingleTopBar = memo(function SingleTopBar({ data-testid={!showToolbarLevel ? Components.NavToolbar.container : undefined} minWidth={{ xs: 'unset', lg: 0 }} > + {unifiedHistoryEnabled && !isSmallScreen && } {!isSmallScreen && } diff --git a/public/app/core/components/AppChrome/TopBar/TopBarExtensionPoint.tsx b/public/app/core/components/AppChrome/TopBar/TopBarExtensionPoint.tsx new file mode 100644 index 00000000000..42f81081e30 --- /dev/null +++ b/public/app/core/components/AppChrome/TopBar/TopBarExtensionPoint.tsx @@ -0,0 +1,25 @@ +import { PluginExtensionPoints } from '@grafana/data'; +import { renderLimitedComponents } from '@grafana/runtime'; +import { usePluginComponents } from 'app/features/plugins/extensions/usePluginComponents'; + +/** + * Extension point for plugins to add components to the top bar. + * Currently restricted to grafana-setupguide-app plugin. + */ +export function TopBarExtensionPoint() { + const { components } = usePluginComponents({ + extensionPointId: PluginExtensionPoints.SingleTopBarAction, + }); + + // Return null if no components are registered + if (components.length === 0) { + return null; + } + + return renderLimitedComponents({ + props: {}, + components: components, + limit: 1, + pluginId: 'grafana-setupguide-app', + }); +} diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index d5740d50b5a..930e2dda359 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -23,6 +23,9 @@ import { useLoadAppPlugins } from './useLoadAppPlugins'; import { usePluginComponents } from './usePluginComponents'; import { isGrafanaDevMode } from './utils'; +// Unmock usePluginComponents to test the real implementation +jest.unmock('./usePluginComponents'); + jest.mock('./useLoadAppPlugins'); jest.mock('./utils', () => ({ diff --git a/public/test/setupTests.ts b/public/test/setupTests.ts index d6c97a0d1c8..2c21f3a8a1e 100644 --- a/public/test/setupTests.ts +++ b/public/test/setupTests.ts @@ -34,6 +34,18 @@ i18next.use(initReactI18next).init({ // the factory uses import.meta.url so we can't use it in CommonJS modules. jest.mock('app/features/dashboard-scene/saving/createDetectChangesWorker.ts'); +// Mock useLoadAppPlugins to prevent async state updates in tests +jest.mock('app/features/plugins/extensions/useLoadAppPlugins', () => ({ + useLoadAppPlugins: jest.fn().mockReturnValue({ isLoading: false }), +})); + +// Mock usePluginComponents to return empty components for all tests by default +// Tests that need to test plugin components can override this mock +jest.mock('app/features/plugins/extensions/usePluginComponents', () => ({ + ...jest.requireActual('app/features/plugins/extensions/usePluginComponents'), + usePluginComponents: jest.fn().mockReturnValue({ components: [], isLoading: false }), +})); + // our tests are heavy in CI due to parallelisation and monaco and kusto // so we increase the default timeout to 2secs to avoid flakiness configure({ asyncUtilTimeout: 2000 }); From f1dbbcbe00cf315b9fdc8ca942a97f9d2e6e2da9 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Mon, 24 Nov 2025 18:20:11 +0000 Subject: [PATCH 084/378] Plugins: Add `/meta` and `/metas` APIs to plugins app (#113775) * add /meta and /metas APIs * wrapped storage route * format file * fix switch statement lint issue * fix plugininstaller test --------- Co-authored-by: Todd Treece --- apps/plugins/go.mod | 104 ++- apps/plugins/go.sum | 348 +++++++++- apps/plugins/kinds/manifest.cue | 23 +- apps/plugins/kinds/plugin.cue | 233 +------ apps/plugins/kinds/pluginmeta.cue | 216 +++++++ .../plugins/v0alpha1/plugin_client_gen.go | 24 - .../plugin_getmeta_response_types_gen.go | 463 -------------- .../plugins/v0alpha1/pluginmeta_client_gen.go | 99 +++ .../plugins/v0alpha1/pluginmeta_codec_gen.go | 28 + .../v0alpha1/pluginmeta_metadata_gen.go | 31 + .../plugins/v0alpha1/pluginmeta_object_gen.go | 319 +++++++++ .../plugins/v0alpha1/pluginmeta_schema_gen.go | 34 + .../plugins/v0alpha1/pluginmeta_spec_gen.go | 473 ++++++++++++++ .../plugins/v0alpha1/pluginmeta_status_gen.go | 44 ++ apps/plugins/pkg/apis/plugins_manifest.go | 245 +------ apps/plugins/pkg/app/app.go | 61 +- apps/plugins/pkg/app/meta/cloud.go | 131 ++++ apps/plugins/pkg/app/meta/cloud_test.go | 186 ++++++ apps/plugins/pkg/app/meta/core.go | 603 ++++++++++++++++++ apps/plugins/pkg/app/meta/core_test.go | 302 +++++++++ apps/plugins/pkg/app/meta/manager.go | 131 ++++ apps/plugins/pkg/app/meta/manager_test.go | 434 +++++++++++++ apps/plugins/pkg/app/meta/provider.go | 27 + apps/plugins/pkg/app/storage.go | 200 ++++++ pkg/plugins/models.go | 1 + pkg/registry/apps/apps.go | 2 +- pkg/registry/apps/apps_test.go | 2 +- pkg/registry/apps/plugins/register.go | 94 ++- pkg/server/wire_gen.go | 12 +- .../plugininstaller/service_test.go | 8 +- pkg/tests/apis/plugins/discovery_test.go | 40 +- pkg/tests/apis/plugins/pluginmeta_test.go | 95 +++ pkg/tests/apis/plugins/pluginmetas_test.go | 85 +++ 33 files changed, 4057 insertions(+), 1041 deletions(-) create mode 100644 apps/plugins/kinds/pluginmeta.cue delete mode 100644 apps/plugins/pkg/apis/plugins/v0alpha1/plugin_getmeta_response_types_gen.go create mode 100644 apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go create mode 100644 apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_codec_gen.go create mode 100644 apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go create mode 100644 apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_object_gen.go create mode 100644 apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_schema_gen.go create mode 100644 apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_spec_gen.go create mode 100644 apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go create mode 100644 apps/plugins/pkg/app/meta/cloud.go create mode 100644 apps/plugins/pkg/app/meta/cloud_test.go create mode 100644 apps/plugins/pkg/app/meta/core.go create mode 100644 apps/plugins/pkg/app/meta/core_test.go create mode 100644 apps/plugins/pkg/app/meta/manager.go create mode 100644 apps/plugins/pkg/app/meta/manager_test.go create mode 100644 apps/plugins/pkg/app/meta/provider.go create mode 100644 apps/plugins/pkg/app/storage.go create mode 100644 pkg/tests/apis/plugins/pluginmeta_test.go create mode 100644 pkg/tests/apis/plugins/pluginmetas_test.go diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index b24c9260327..8fef8fe3e60 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -2,10 +2,18 @@ module github.com/grafana/grafana/apps/plugins go 1.25.3 +replace github.com/grafana/grafana => ../.. + +replace github.com/grafana/grafana/pkg/apimachinery => ../../pkg/apimachinery + +replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver + require ( + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 + github.com/grafana/grafana v0.0.0-00010101000000-000000000000 github.com/grafana/grafana-app-sdk v0.48.2 github.com/grafana/grafana-app-sdk/logging v0.48.1 - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde + github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 @@ -14,57 +22,135 @@ require ( ) require ( + github.com/apache/arrow-go/v18 v18.4.1 // indirect + github.com/armon/go-metrics v0.4.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect + github.com/aws/smithy-go v1.23.1 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bwmarrin/snowflake v0.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cheekybits/genny v1.0.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect github.com/go-openapi/jsonreference v0.21.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-stack/stack v1.8.1 // indirect github.com/go-test/deep v1.1.1 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect - github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect + github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect + github.com/grafana/grafana-aws-sdk v1.3.0 // indirect + github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.283.0 // indirect + github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect + github.com/grafana/otel-profiling-go v0.5.1 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect + github.com/grafana/sqlds/v4 v4.2.7 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-metrics v0.5.4 // indirect + github.com/hashicorp/go-msgpack/v2 v2.1.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.7.0 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/memberlist v0.5.2 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/jaegertracing/jaeger-idl v0.5.0 // indirect + github.com/jmespath-community/go-jmespath v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattetti/filebuffer v1.0.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/mdlayher/vsock v1.2.1 // indirect + github.com/miekg/dns v1.1.63 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mithrandie/csvq v1.18.1 // indirect + github.com/mithrandie/csvq-driver v1.7.0 // indirect + github.com/mithrandie/go-file/v2 v2.1.0 // indirect + github.com/mithrandie/go-text v1.6.0 // indirect + github.com/mithrandie/ternary v1.1.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect - github.com/onsi/ginkgo/v2 v2.22.2 // indirect - github.com/onsi/gomega v1.36.2 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/alertmanager v0.28.0 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/exporter-toolkit v0.14.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/tjhop/slog-gokit v0.1.5 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -76,23 +162,31 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.44.0 // indirect + golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect + golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect + golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.39.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect google.golang.org/grpc v1.76.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.2 // indirect k8s.io/apiextensions-apiserver v0.34.2 // indirect k8s.io/client-go v0.34.2 // indirect + k8s.io/component-base v0.34.2 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index b769fbe3916..679aa7bdd49 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -1,11 +1,59 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/apache/arrow-go/v18 v18.4.1 h1:q/jVkBWCJOB9reDgaIZIdruLQUb1kbkvOnOFezVH1C4= +github.com/apache/arrow-go/v18 v18.4.1/go.mod h1:tLyFubsAl17bvFdUAy24bsSvA/6ww95Iqi67fTpGu3E= +github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= +github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE= +github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2/credentials v1.18.14 h1:TxkI7QI+sFkTItN/6cJuMZEIVMFXeu2dI1ZffkXngKI= +github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0= +github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= +github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= +github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -14,13 +62,29 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= +github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -33,16 +97,53 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -50,44 +151,155 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 h1:nbxKRtrbuhvOYmI2RhOYauHRJCtpR+vTNIgg1lFUCws= +github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181/go.mod h1:VtPNIFlEOJPPEc13Ax6ZTbNV3M/sAzLID72YjgzOPVA= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= +github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde h1:ydSrBIOCxJQ84+JU+cyYsOLL40QeXrB7rYfsY/ezU4w= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde/go.mod h1:3MwgP0ISxGviTy3ZUJZsNz/56NNtHztMlH+gcxDt6Tw= +github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= +github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= +github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= +github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc= +github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= +github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= +github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA= +github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= +github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= +github.com/hashicorp/go-msgpack/v2 v2.1.2 h1:4Ee8FTp834e+ewB71RDrQ0VKpyFdrKOjvYtnQ/ltVj0= +github.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/memberlist v0.5.2 h1:rJoNPWZ0juJBgqn48gjy59K5H4rNgvUoM1kUD7bXiuI= +github.com/hashicorp/memberlist v0.5.2/go.mod h1:Ri9p/tRShbjYnpNf4FFPXG7wxEGY4Nrcn6E7jrVa//4= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE= +github.com/jaegertracing/jaeger-idl v0.5.0/go.mod h1:ON90zFo9eoyXrt9F/KN8YeF3zxcnujaisMweFY/rg5k= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/jmespath-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/rw+zyQfyg5UF+L4= +github.com/jmespath-community/go-jmespath v1.1.1/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 h1:SwcnSwBR7X/5EHJQlXBockkJVIMRVt5yKaesBPMtyZQ= +github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6/go.mod h1:WrYiIuiXUMIvTDAQw97C+9l0CnBmCcvosPjN3XDqS/o= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= +github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= +github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= +github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= +github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mithrandie/csvq v1.18.1 h1:f7NB2scbb7xx2ffPduJ2VtZ85RpWXfvanYskAkGlCBU= +github.com/mithrandie/csvq v1.18.1/go.mod h1:MRJj7AtcXfk7jhNGxLuJGP3LORmh4lpiPWxQ7VyCRn8= +github.com/mithrandie/csvq-driver v1.7.0 h1:ejiavXNWwTPMyr3fJFnhcqd1L1cYudA0foQy9cZrqhw= +github.com/mithrandie/csvq-driver v1.7.0/go.mod h1:HcN3xL9UCJnBYA/AIQOOB/KlyfXAiYr5yxDmiwrGk5o= +github.com/mithrandie/go-file/v2 v2.1.0 h1:XA5Tl+73GXMDvgwSE3Sg0uC5FkLr3hnXs8SpUas0hyg= +github.com/mithrandie/go-file/v2 v2.1.0/go.mod h1:9YtTF3Xo59GqC1Pxw6KyGVcM/qubAMlxVsqI/u9r++c= +github.com/mithrandie/go-text v1.6.0 h1:8gOXTMPbMY8DJbKMTv8kHhADcJlDWXqS/YQH4SyWO6s= +github.com/mithrandie/go-text v1.6.0/go.mod h1:xCgj1xiNbI/d4xA9sLVvXkjh5B2tNx2ZT2/3rpmh8to= +github.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QRdC4= +github.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= @@ -95,43 +307,99 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/alertmanager v0.28.0 h1:sLN+6HhZet8hrbmGHLAHWsTXgZSVCvq9Ix3U3wvivqc= +github.com/prometheus/alertmanager v0.28.0/go.mod h1:/okSnb2LlodbMlRoOWQEKtqI/coOo2NKZDm2Hu9QHLQ= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg= +github.com/prometheus/exporter-toolkit v0.14.0/go.mod h1:Gu5LnVvt7Nr/oqTBUC23WILZepW0nffNo10XdhQcwWA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tjhop/slog-gokit v0.1.5 h1:ayloIUi5EK2QYB8eY4DOPO95/mRtMW42lUkp3quJohc= +github.com/tjhop/slog-gokit v0.1.5/go.mod h1:yA48zAHvV+Sg4z4VRyeFyFUNNXd3JY5Zg84u3USICq0= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= @@ -140,8 +408,23 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0/go.mod h1:rjbQTDEPQymPE0YnRQp9/NuPwwtL0sesz/fnqRW/v84= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 h1:nXGeLvT1QtCAhkASkP/ksjkTKZALIaQBIW+JSIw1KIc= +go.opentelemetry.io/contrib/propagators/jaeger v1.38.0/go.mod h1:oMvOXk78ZR3KEuPMBgp/ThAMDy9ku/eyUVztr+3G6Wo= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 h1:oPW/SRFyHgIgxrvNhSBzqvZER2N5kRlci3/rGTOuyWo= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0/go.mod h1:B9Oka5QVD0bnmZNO6gBbBta6nohD/1Z+f9waH2oXyBs= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -150,12 +433,15 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4D go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= @@ -166,34 +452,68 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= +golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= +golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= @@ -209,25 +529,47 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= @@ -240,6 +582,8 @@ k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= +k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= diff --git a/apps/plugins/kinds/manifest.cue b/apps/plugins/kinds/manifest.cue index b3f5251432b..f9a08562cb0 100644 --- a/apps/plugins/kinds/manifest.cue +++ b/apps/plugins/kinds/manifest.cue @@ -4,15 +4,18 @@ manifest: { appName: "plugins" groupOverride: "plugins.grafana.app" versions: { - "v0alpha1": { - served: true - codegen: { - ts: {enabled: false} - go: {enabled: true} - } - kinds: [ - pluginV0Alpha1, - ] - } + "v0alpha1": v0alpha1Version } } + +v0alpha1Version: { + served: true + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + pluginV0Alpha1, + pluginMetaV0Alpha1, + ] +} diff --git a/apps/plugins/kinds/plugin.cue b/apps/plugins/kinds/plugin.cue index 50c475cd430..dc16a3f6de4 100644 --- a/apps/plugins/kinds/plugin.cue +++ b/apps/plugins/kinds/plugin.cue @@ -1,236 +1,15 @@ package plugins -import ( - "time" -) - pluginV0Alpha1: { - kind: "Plugin" + kind: "Plugin" plural: "plugins" - scope: "Namespaced" + scope: "Namespaced" schema: { spec: { - id: string - version: string - url?: string - class: "core" | "external" - } - } - routes: { - "/meta": { - "GET": { - request: {} - response: #JSONData, - responseMetadata: { - typeMeta: false - objectMeta: false - } - } + id: string + version: string + url?: string + class: "core" | "external" } } } - - -// JSON configuration schema for Grafana plugins -// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json -#JSONData: { - // Unique name of the plugin - id: string - - // Plugin type - type: "app" | "datasource" | "panel" | "renderer" - - // Human-readable name of the plugin - name: string - - // Metadata for the plugin - info: #Info - - // Dependency information - dependencies: #Dependencies - - // Optional fields - alerting?: bool - annotations?: bool - autoEnabled?: bool - backend?: bool - buildMode?: string - builtIn?: bool - category?: "tsdb" | "logging" | "cloud" | "tracing" | "profiling" | "sql" | "enterprise" | "iot" | "other" - enterpriseFeatures?: #EnterpriseFeatures - executable?: string - hideFromList?: bool - // +listType=atomic - includes?: [...#Include] - logs?: bool - metrics?: bool - multiValueFilterOperators?: bool - pascalName?: string - preload?: bool - queryOptions?: #QueryOptions - // +listType=atomic - routes?: [...#Route] - skipDataQuery?: bool - state?: "alpha" | "beta" - streaming?: bool - tracing?: bool - iam?: #IAM - // +listType=atomic - roles?: [...#Role] - extensions?: #Extensions -} - -#Info: { - // Required fields - // +listType=set - keywords: [...string] - logos: { - small: string - large: string - } - updated: string & time.Time - version: string - // Optional fields - author?: { - name?: string - email?: string - url?: string - } - description?: string - // +listType=atomic - links?: [...{ - name?: string - url?: string - }] - // +listType=atomic - screenshots?: [...{ - name?: string - path?: string - }] -} - -#Dependencies: { - // Required field - grafanaDependency: string - - // Optional fields - grafanaVersion?: string - // +listType=set - // +listMapKey=id - plugins?: [...{ - id: string - type: "app" | "datasource" | "panel" - name: string - }] - extensions?: { - // +listType=set - exposedComponents?: [...string] - } -} - -#EnterpriseFeatures: { - healthDiagnosticsErrors?: bool | *false - // Allow additional properties - [string]: _ -} - -#Include: { - uid?: string - type?: "dashboard" | "page" | "panel" | "datasource" - name?: string - component?: string - role?: "Admin" | "Editor" | "Viewer" - action?: string - path?: string - addToNav?: bool - defaultNav?: bool - icon?: string -} - -#QueryOptions: { - maxDataPoints?: bool - minInterval?: bool - cacheTimeout?: bool -} - -#Route: { - path?: string - method?: string - url?: string - reqSignedIn?: bool - reqRole?: string - reqAction?: string - // +listType=atomic - headers?: [...string] - body?: [string]: _ - tokenAuth?: { - url?: string - // +listType=set - scopes?: [...string] - params?: [string]: _ - } - jwtTokenAuth?: { - url?: string - // +listType=set - scopes?: [...string] - params?: [string]: _ - } - // +listType=atomic - urlParams?: [...{ - name?: string - content?: string - }] -} - -#IAM: { - // +listType=atomic - permissions?: [...{ - action?: string - scope?: string - }] -} - -#Role: { - role?: { - name?: string - description?: string - // +listType=atomic - permissions?: [...{ - action?: string - scope?: string - }] - } - // +listType=set - grants?: [...string] -} - -#Extensions: { - // +listType=atomic - addedComponents?: [...{ - // +listType=set - targets: [...string] - title: string - description?: string - }] - // +listType=atomic - addedLinks?: [...{ - // +listType=set - targets: [...string] - title: string - description?: string - }] - // +listType=set - // +listMapKey=id - exposedComponents?: [...{ - id: string - title?: string - description?: string - }] - // +listType=set - // +listMapKey=id - extensionPoints?: [...{ - id: string - title?: string - description?: string - }] -} diff --git a/apps/plugins/kinds/pluginmeta.cue b/apps/plugins/kinds/pluginmeta.cue new file mode 100644 index 00000000000..0bac48169c6 --- /dev/null +++ b/apps/plugins/kinds/pluginmeta.cue @@ -0,0 +1,216 @@ +package plugins + +pluginMetaV0Alpha1: { + kind: "PluginMeta" + plural: "pluginsmeta" + scope: "Namespaced" + schema: { + spec: { + pluginJSON: #JSONData, + } + } +} + +// JSON configuration schema for Grafana plugins +// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json +#JSONData: { + // Unique name of the plugin + id: string + + // Plugin type + type: "app" | "datasource" | "panel" | "renderer" + + // Human-readable name of the plugin + name: string + + // Metadata for the plugin + info: #Info + + // Dependency information + dependencies: #Dependencies + + // Optional fields + alerting?: bool + annotations?: bool + autoEnabled?: bool + backend?: bool + buildMode?: string + builtIn?: bool + category?: "tsdb" | "logging" | "cloud" | "tracing" | "profiling" | "sql" | "enterprise" | "iot" | "other" + enterpriseFeatures?: #EnterpriseFeatures + executable?: string + hideFromList?: bool + // +listType=atomic + includes?: [...#Include] + logs?: bool + metrics?: bool + multiValueFilterOperators?: bool + pascalName?: string + preload?: bool + queryOptions?: #QueryOptions + // +listType=atomic + routes?: [...#Route] + skipDataQuery?: bool + state?: "alpha" | "beta" + streaming?: bool + tracing?: bool + iam?: #IAM + // +listType=atomic + roles?: [...#Role] + extensions?: #Extensions +} + +#Info: { + // Required fields + // +listType=set + keywords: [...string] + logos: { + small: string + large: string + } + updated: string + version: string + // Optional fields + author?: { + name?: string + email?: string + url?: string + } + description?: string + // +listType=atomic + links?: [...{ + name?: string + url?: string + }] + // +listType=atomic + screenshots?: [...{ + name?: string + path?: string + }] +} + +#Dependencies: { + // Required field + grafanaDependency: string + + // Optional fields + grafanaVersion?: string + // +listType=set + // +listMapKey=id + plugins?: [...{ + id: string + type: "app" | "datasource" | "panel" + name: string + }] + extensions?: { + // +listType=set + exposedComponents?: [...string] + } +} + +#EnterpriseFeatures: { + healthDiagnosticsErrors?: bool | *false + // Allow additional properties + [string]: _ +} + +#Include: { + uid?: string + type?: "dashboard" | "page" | "panel" | "datasource" + name?: string + component?: string + role?: "Admin" | "Editor" | "Viewer" + action?: string + path?: string + addToNav?: bool + defaultNav?: bool + icon?: string +} + +#QueryOptions: { + maxDataPoints?: bool + minInterval?: bool + cacheTimeout?: bool +} + +#Route: { + path?: string + method?: string + url?: string + reqSignedIn?: bool + reqRole?: string + reqAction?: string + // +listType=atomic + headers?: [...string] + body?: [string]: _ + tokenAuth?: { + url?: string + // +listType=set + scopes?: [...string] + params?: [string]: _ + } + jwtTokenAuth?: { + url?: string + // +listType=set + scopes?: [...string] + params?: [string]: _ + } + // +listType=atomic + urlParams?: [...{ + name?: string + content?: string + }] +} + +#IAM: { + // +listType=atomic + permissions?: [...{ + action?: string + scope?: string + }] +} + +#Role: { + role?: { + name?: string + description?: string + // +listType=atomic + permissions?: [...{ + action?: string + scope?: string + }] + } + // +listType=set + grants?: [...string] +} + +#Extensions: { + // +listType=atomic + addedComponents?: [...{ + // +listType=set + targets: [...string] + title: string + description?: string + }] + // +listType=atomic + addedLinks?: [...{ + // +listType=set + targets: [...string] + title: string + description?: string + }] + // +listType=set + // +listMapKey=id + exposedComponents?: [...{ + id: string + title?: string + description?: string + }] + // +listType=set + // +listMapKey=id + extensionPoints?: [...{ + id: string + title?: string + description?: string + }] +} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_client_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_client_gen.go index 3e9bcb3a2bb..42fc1fb6d54 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_client_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_client_gen.go @@ -2,9 +2,6 @@ package v0alpha1 import ( "context" - "encoding/json" - "fmt" - "net/http" "github.com/grafana/grafana-app-sdk/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -100,24 +97,3 @@ func (c *PluginClient) UpdateStatus(ctx context.Context, identifier resource.Ide func (c *PluginClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } - -type GetMetaRequest struct { - Headers http.Header -} - -func (c *PluginClient) GetMeta(ctx context.Context, identifier resource.Identifier, request GetMetaRequest) (*GetMeta, error) { - resp, err := c.client.SubresourceRequest(ctx, identifier, resource.CustomRouteRequestOptions{ - Path: "/meta", - Verb: "GET", - Headers: request.Headers, - }) - if err != nil { - return nil, err - } - cast := GetMeta{} - err = json.Unmarshal(resp, &cast) - if err != nil { - return nil, fmt.Errorf("unable to unmarshal response bytes into GetMeta: %w", err) - } - return &cast, nil -} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_getmeta_response_types_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_getmeta_response_types_gen.go deleted file mode 100644 index 6b0e6899a5b..00000000000 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_getmeta_response_types_gen.go +++ /dev/null @@ -1,463 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v0alpha1 - -import ( - time "time" -) - -// +k8s:openapi-gen=true -type Info struct { - // Required fields - // +listType=set - Keywords []string `json:"keywords"` - Logos V0alpha1InfoLogos `json:"logos"` - Updated time.Time `json:"updated"` - Version string `json:"version"` - // Optional fields - Author *V0alpha1InfoAuthor `json:"author,omitempty"` - Description *string `json:"description,omitempty"` - // +listType=atomic - Links []V0alpha1InfoLinks `json:"links,omitempty"` - // +listType=atomic - Screenshots []V0alpha1InfoScreenshots `json:"screenshots,omitempty"` -} - -// NewInfo creates a new Info object. -func NewInfo() *Info { - return &Info{ - Keywords: []string{}, - Logos: *NewV0alpha1InfoLogos(), - } -} - -// +k8s:openapi-gen=true -type Dependencies struct { - // Required field - GrafanaDependency string `json:"grafanaDependency"` - // Optional fields - GrafanaVersion *string `json:"grafanaVersion,omitempty"` - // +listType=set - // +listMapKey=id - Plugins []V0alpha1DependenciesPlugins `json:"plugins,omitempty"` - Extensions *V0alpha1DependenciesExtensions `json:"extensions,omitempty"` -} - -// NewDependencies creates a new Dependencies object. -func NewDependencies() *Dependencies { - return &Dependencies{} -} - -// +k8s:openapi-gen=true -type EnterpriseFeatures struct { - // Allow additional properties - HealthDiagnosticsErrors *bool `json:"healthDiagnosticsErrors,omitempty"` -} - -// NewEnterpriseFeatures creates a new EnterpriseFeatures object. -func NewEnterpriseFeatures() *EnterpriseFeatures { - return &EnterpriseFeatures{ - HealthDiagnosticsErrors: (func(input bool) *bool { return &input })(false), - } -} - -// +k8s:openapi-gen=true -type Include struct { - Uid *string `json:"uid,omitempty"` - Type *IncludeType `json:"type,omitempty"` - Name *string `json:"name,omitempty"` - Component *string `json:"component,omitempty"` - Role *IncludeRole `json:"role,omitempty"` - Action *string `json:"action,omitempty"` - Path *string `json:"path,omitempty"` - AddToNav *bool `json:"addToNav,omitempty"` - DefaultNav *bool `json:"defaultNav,omitempty"` - Icon *string `json:"icon,omitempty"` -} - -// NewInclude creates a new Include object. -func NewInclude() *Include { - return &Include{} -} - -// +k8s:openapi-gen=true -type QueryOptions struct { - MaxDataPoints *bool `json:"maxDataPoints,omitempty"` - MinInterval *bool `json:"minInterval,omitempty"` - CacheTimeout *bool `json:"cacheTimeout,omitempty"` -} - -// NewQueryOptions creates a new QueryOptions object. -func NewQueryOptions() *QueryOptions { - return &QueryOptions{} -} - -// +k8s:openapi-gen=true -type Route struct { - Path *string `json:"path,omitempty"` - Method *string `json:"method,omitempty"` - Url *string `json:"url,omitempty"` - ReqSignedIn *bool `json:"reqSignedIn,omitempty"` - ReqRole *string `json:"reqRole,omitempty"` - ReqAction *string `json:"reqAction,omitempty"` - // +listType=atomic - Headers []string `json:"headers,omitempty"` - Body map[string]interface{} `json:"body,omitempty"` - TokenAuth *V0alpha1RouteTokenAuth `json:"tokenAuth,omitempty"` - JwtTokenAuth *V0alpha1RouteJwtTokenAuth `json:"jwtTokenAuth,omitempty"` - // +listType=atomic - UrlParams []V0alpha1RouteUrlParams `json:"urlParams,omitempty"` -} - -// NewRoute creates a new Route object. -func NewRoute() *Route { - return &Route{} -} - -// +k8s:openapi-gen=true -type IAM struct { - // +listType=atomic - Permissions []V0alpha1IAMPermissions `json:"permissions,omitempty"` -} - -// NewIAM creates a new IAM object. -func NewIAM() *IAM { - return &IAM{} -} - -// +k8s:openapi-gen=true -type Role struct { - Role *V0alpha1RoleRole `json:"role,omitempty"` - // +listType=set - Grants []string `json:"grants,omitempty"` -} - -// NewRole creates a new Role object. -func NewRole() *Role { - return &Role{} -} - -// +k8s:openapi-gen=true -type Extensions struct { - // +listType=atomic - AddedComponents []V0alpha1ExtensionsAddedComponents `json:"addedComponents,omitempty"` - // +listType=atomic - AddedLinks []V0alpha1ExtensionsAddedLinks `json:"addedLinks,omitempty"` - // +listType=set - // +listMapKey=id - ExposedComponents []V0alpha1ExtensionsExposedComponents `json:"exposedComponents,omitempty"` - // +listType=set - // +listMapKey=id - ExtensionPoints []V0alpha1ExtensionsExtensionPoints `json:"extensionPoints,omitempty"` -} - -// NewExtensions creates a new Extensions object. -func NewExtensions() *Extensions { - return &Extensions{} -} - -// +k8s:openapi-gen=true -type GetMeta struct { - // Unique name of the plugin - Id string `json:"id"` - // Plugin type - Type GetMetaType `json:"type"` - // Human-readable name of the plugin - Name string `json:"name"` - // Metadata for the plugin - Info Info `json:"info"` - // Dependency information - Dependencies Dependencies `json:"dependencies"` - // Optional fields - Alerting *bool `json:"alerting,omitempty"` - Annotations *bool `json:"annotations,omitempty"` - AutoEnabled *bool `json:"autoEnabled,omitempty"` - Backend *bool `json:"backend,omitempty"` - BuildMode *string `json:"buildMode,omitempty"` - BuiltIn *bool `json:"builtIn,omitempty"` - Category *GetMetaCategory `json:"category,omitempty"` - EnterpriseFeatures *EnterpriseFeatures `json:"enterpriseFeatures,omitempty"` - Executable *string `json:"executable,omitempty"` - HideFromList *bool `json:"hideFromList,omitempty"` - // +listType=atomic - Includes []Include `json:"includes,omitempty"` - Logs *bool `json:"logs,omitempty"` - Metrics *bool `json:"metrics,omitempty"` - MultiValueFilterOperators *bool `json:"multiValueFilterOperators,omitempty"` - PascalName *string `json:"pascalName,omitempty"` - Preload *bool `json:"preload,omitempty"` - QueryOptions *QueryOptions `json:"queryOptions,omitempty"` - // +listType=atomic - Routes []Route `json:"routes,omitempty"` - SkipDataQuery *bool `json:"skipDataQuery,omitempty"` - State *GetMetaState `json:"state,omitempty"` - Streaming *bool `json:"streaming,omitempty"` - Tracing *bool `json:"tracing,omitempty"` - Iam *IAM `json:"iam,omitempty"` - // +listType=atomic - Roles []Role `json:"roles,omitempty"` - Extensions *Extensions `json:"extensions,omitempty"` -} - -// NewGetMeta creates a new GetMeta object. -func NewGetMeta() *GetMeta { - return &GetMeta{ - Info: *NewInfo(), - Dependencies: *NewDependencies(), - } -} - -// +k8s:openapi-gen=true -type V0alpha1InfoLogos struct { - Small string `json:"small"` - Large string `json:"large"` -} - -// NewV0alpha1InfoLogos creates a new V0alpha1InfoLogos object. -func NewV0alpha1InfoLogos() *V0alpha1InfoLogos { - return &V0alpha1InfoLogos{} -} - -// +k8s:openapi-gen=true -type V0alpha1InfoAuthor struct { - Name *string `json:"name,omitempty"` - Email *string `json:"email,omitempty"` - Url *string `json:"url,omitempty"` -} - -// NewV0alpha1InfoAuthor creates a new V0alpha1InfoAuthor object. -func NewV0alpha1InfoAuthor() *V0alpha1InfoAuthor { - return &V0alpha1InfoAuthor{} -} - -// +k8s:openapi-gen=true -type V0alpha1InfoLinks struct { - Name *string `json:"name,omitempty"` - Url *string `json:"url,omitempty"` -} - -// NewV0alpha1InfoLinks creates a new V0alpha1InfoLinks object. -func NewV0alpha1InfoLinks() *V0alpha1InfoLinks { - return &V0alpha1InfoLinks{} -} - -// +k8s:openapi-gen=true -type V0alpha1InfoScreenshots struct { - Name *string `json:"name,omitempty"` - Path *string `json:"path,omitempty"` -} - -// NewV0alpha1InfoScreenshots creates a new V0alpha1InfoScreenshots object. -func NewV0alpha1InfoScreenshots() *V0alpha1InfoScreenshots { - return &V0alpha1InfoScreenshots{} -} - -// +k8s:openapi-gen=true -type V0alpha1DependenciesPlugins struct { - Id string `json:"id"` - Type V0alpha1DependenciesPluginsType `json:"type"` - Name string `json:"name"` -} - -// NewV0alpha1DependenciesPlugins creates a new V0alpha1DependenciesPlugins object. -func NewV0alpha1DependenciesPlugins() *V0alpha1DependenciesPlugins { - return &V0alpha1DependenciesPlugins{} -} - -// +k8s:openapi-gen=true -type V0alpha1DependenciesExtensions struct { - // +listType=set - ExposedComponents []string `json:"exposedComponents,omitempty"` -} - -// NewV0alpha1DependenciesExtensions creates a new V0alpha1DependenciesExtensions object. -func NewV0alpha1DependenciesExtensions() *V0alpha1DependenciesExtensions { - return &V0alpha1DependenciesExtensions{} -} - -// +k8s:openapi-gen=true -type V0alpha1RouteTokenAuth struct { - Url *string `json:"url,omitempty"` - // +listType=set - Scopes []string `json:"scopes,omitempty"` - Params map[string]interface{} `json:"params,omitempty"` -} - -// NewV0alpha1RouteTokenAuth creates a new V0alpha1RouteTokenAuth object. -func NewV0alpha1RouteTokenAuth() *V0alpha1RouteTokenAuth { - return &V0alpha1RouteTokenAuth{} -} - -// +k8s:openapi-gen=true -type V0alpha1RouteJwtTokenAuth struct { - Url *string `json:"url,omitempty"` - // +listType=set - Scopes []string `json:"scopes,omitempty"` - Params map[string]interface{} `json:"params,omitempty"` -} - -// NewV0alpha1RouteJwtTokenAuth creates a new V0alpha1RouteJwtTokenAuth object. -func NewV0alpha1RouteJwtTokenAuth() *V0alpha1RouteJwtTokenAuth { - return &V0alpha1RouteJwtTokenAuth{} -} - -// +k8s:openapi-gen=true -type V0alpha1RouteUrlParams struct { - Name *string `json:"name,omitempty"` - Content *string `json:"content,omitempty"` -} - -// NewV0alpha1RouteUrlParams creates a new V0alpha1RouteUrlParams object. -func NewV0alpha1RouteUrlParams() *V0alpha1RouteUrlParams { - return &V0alpha1RouteUrlParams{} -} - -// +k8s:openapi-gen=true -type V0alpha1IAMPermissions struct { - Action *string `json:"action,omitempty"` - Scope *string `json:"scope,omitempty"` -} - -// NewV0alpha1IAMPermissions creates a new V0alpha1IAMPermissions object. -func NewV0alpha1IAMPermissions() *V0alpha1IAMPermissions { - return &V0alpha1IAMPermissions{} -} - -// +k8s:openapi-gen=true -type V0alpha1RoleRolePermissions struct { - Action *string `json:"action,omitempty"` - Scope *string `json:"scope,omitempty"` -} - -// NewV0alpha1RoleRolePermissions creates a new V0alpha1RoleRolePermissions object. -func NewV0alpha1RoleRolePermissions() *V0alpha1RoleRolePermissions { - return &V0alpha1RoleRolePermissions{} -} - -// +k8s:openapi-gen=true -type V0alpha1RoleRole struct { - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - // +listType=atomic - Permissions []V0alpha1RoleRolePermissions `json:"permissions,omitempty"` -} - -// NewV0alpha1RoleRole creates a new V0alpha1RoleRole object. -func NewV0alpha1RoleRole() *V0alpha1RoleRole { - return &V0alpha1RoleRole{} -} - -// +k8s:openapi-gen=true -type V0alpha1ExtensionsAddedComponents struct { - // +listType=set - Targets []string `json:"targets"` - Title string `json:"title"` - Description *string `json:"description,omitempty"` -} - -// NewV0alpha1ExtensionsAddedComponents creates a new V0alpha1ExtensionsAddedComponents object. -func NewV0alpha1ExtensionsAddedComponents() *V0alpha1ExtensionsAddedComponents { - return &V0alpha1ExtensionsAddedComponents{ - Targets: []string{}, - } -} - -// +k8s:openapi-gen=true -type V0alpha1ExtensionsAddedLinks struct { - // +listType=set - Targets []string `json:"targets"` - Title string `json:"title"` - Description *string `json:"description,omitempty"` -} - -// NewV0alpha1ExtensionsAddedLinks creates a new V0alpha1ExtensionsAddedLinks object. -func NewV0alpha1ExtensionsAddedLinks() *V0alpha1ExtensionsAddedLinks { - return &V0alpha1ExtensionsAddedLinks{ - Targets: []string{}, - } -} - -// +k8s:openapi-gen=true -type V0alpha1ExtensionsExposedComponents struct { - Id string `json:"id"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` -} - -// NewV0alpha1ExtensionsExposedComponents creates a new V0alpha1ExtensionsExposedComponents object. -func NewV0alpha1ExtensionsExposedComponents() *V0alpha1ExtensionsExposedComponents { - return &V0alpha1ExtensionsExposedComponents{} -} - -// +k8s:openapi-gen=true -type V0alpha1ExtensionsExtensionPoints struct { - Id string `json:"id"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` -} - -// NewV0alpha1ExtensionsExtensionPoints creates a new V0alpha1ExtensionsExtensionPoints object. -func NewV0alpha1ExtensionsExtensionPoints() *V0alpha1ExtensionsExtensionPoints { - return &V0alpha1ExtensionsExtensionPoints{} -} - -// +k8s:openapi-gen=true -type IncludeType string - -const ( - IncludeTypeDashboard IncludeType = "dashboard" - IncludeTypePage IncludeType = "page" - IncludeTypePanel IncludeType = "panel" - IncludeTypeDatasource IncludeType = "datasource" -) - -// +k8s:openapi-gen=true -type IncludeRole string - -const ( - IncludeRoleAdmin IncludeRole = "Admin" - IncludeRoleEditor IncludeRole = "Editor" - IncludeRoleViewer IncludeRole = "Viewer" -) - -// +k8s:openapi-gen=true -type GetMetaType string - -const ( - GetMetaTypeApp GetMetaType = "app" - GetMetaTypeDatasource GetMetaType = "datasource" - GetMetaTypePanel GetMetaType = "panel" - GetMetaTypeRenderer GetMetaType = "renderer" -) - -// +k8s:openapi-gen=true -type GetMetaCategory string - -const ( - GetMetaCategoryTsdb GetMetaCategory = "tsdb" - GetMetaCategoryLogging GetMetaCategory = "logging" - GetMetaCategoryCloud GetMetaCategory = "cloud" - GetMetaCategoryTracing GetMetaCategory = "tracing" - GetMetaCategoryProfiling GetMetaCategory = "profiling" - GetMetaCategorySql GetMetaCategory = "sql" - GetMetaCategoryEnterprise GetMetaCategory = "enterprise" - GetMetaCategoryIot GetMetaCategory = "iot" - GetMetaCategoryOther GetMetaCategory = "other" -) - -// +k8s:openapi-gen=true -type GetMetaState string - -const ( - GetMetaStateAlpha GetMetaState = "alpha" - GetMetaStateBeta GetMetaState = "beta" -) - -// +k8s:openapi-gen=true -type V0alpha1DependenciesPluginsType string - -const ( - V0alpha1DependenciesPluginsTypeApp V0alpha1DependenciesPluginsType = "app" - V0alpha1DependenciesPluginsTypeDatasource V0alpha1DependenciesPluginsType = "datasource" - V0alpha1DependenciesPluginsTypePanel V0alpha1DependenciesPluginsType = "panel" -) diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go new file mode 100644 index 00000000000..e7788e27a33 --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type PluginMetaClient struct { + client *resource.TypedClient[*PluginMeta, *PluginMetaList] +} + +func NewPluginMetaClient(client resource.Client) *PluginMetaClient { + return &PluginMetaClient{ + client: resource.NewTypedClient[*PluginMeta, *PluginMetaList](client, PluginMetaKind()), + } +} + +func NewPluginMetaClientFromGenerator(generator resource.ClientGenerator) (*PluginMetaClient, error) { + c, err := generator.ClientFor(PluginMetaKind()) + if err != nil { + return nil, err + } + return NewPluginMetaClient(c), nil +} + +func (c *PluginMetaClient) Get(ctx context.Context, identifier resource.Identifier) (*PluginMeta, error) { + return c.client.Get(ctx, identifier) +} + +func (c *PluginMetaClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginMetaList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *PluginMetaClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginMetaList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *PluginMetaClient) Create(ctx context.Context, obj *PluginMeta, opts resource.CreateOptions) (*PluginMeta, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = PluginMetaKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *PluginMetaClient) Update(ctx context.Context, obj *PluginMeta, opts resource.UpdateOptions) (*PluginMeta, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *PluginMetaClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*PluginMeta, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *PluginMetaClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus PluginMetaStatus, opts resource.UpdateOptions) (*PluginMeta, error) { + return c.client.Update(ctx, &PluginMeta{ + TypeMeta: metav1.TypeMeta{ + Kind: PluginMetaKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *PluginMetaClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_codec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_codec_gen.go new file mode 100644 index 00000000000..77fb6f918a2 --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// PluginMetaJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type PluginMetaJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*PluginMetaJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*PluginMetaJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &PluginMetaJSONCodec{} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go new file mode 100644 index 00000000000..7d3b3c9c6b8 --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type PluginMetaMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewPluginMetaMetadata creates a new PluginMetaMetadata object. +func NewPluginMetaMetadata() *PluginMetaMetadata { + return &PluginMetaMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_object_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_object_gen.go new file mode 100644 index 00000000000..dac431ebf12 --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type PluginMeta struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the PluginMeta + Spec PluginMetaSpec `json:"spec" yaml:"spec"` + + Status PluginMetaStatus `json:"status" yaml:"status"` +} + +func (o *PluginMeta) GetSpec() any { + return o.Spec +} + +func (o *PluginMeta) SetSpec(spec any) error { + cast, ok := spec.(PluginMetaSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *PluginMeta) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *PluginMeta) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *PluginMeta) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(PluginMetaStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type PluginMetaStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *PluginMeta) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *PluginMeta) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *PluginMeta) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *PluginMeta) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *PluginMeta) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *PluginMeta) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *PluginMeta) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *PluginMeta) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *PluginMeta) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *PluginMeta) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *PluginMeta) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *PluginMeta) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *PluginMeta) DeepCopy() *PluginMeta { + cpy := &PluginMeta{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *PluginMeta) DeepCopyInto(dst *PluginMeta) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &PluginMeta{} + +// +k8s:openapi-gen=true +type PluginMetaList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []PluginMeta `json:"items" yaml:"items"` +} + +func (o *PluginMetaList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *PluginMetaList) Copy() resource.ListObject { + cpy := &PluginMetaList{ + TypeMeta: o.TypeMeta, + Items: make([]PluginMeta, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*PluginMeta); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *PluginMetaList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *PluginMetaList) SetItems(items []resource.Object) { + o.Items = make([]PluginMeta, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*PluginMeta) + } +} + +func (o *PluginMetaList) DeepCopy() *PluginMetaList { + cpy := &PluginMetaList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *PluginMetaList) DeepCopyInto(dst *PluginMetaList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &PluginMetaList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *PluginMetaSpec) DeepCopy() *PluginMetaSpec { + cpy := &PluginMetaSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *PluginMetaSpec) DeepCopyInto(dst *PluginMetaSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of PluginMetaStatus +func (s *PluginMetaStatus) DeepCopy() *PluginMetaStatus { + cpy := &PluginMetaStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies PluginMetaStatus into another PluginMetaStatus object +func (s *PluginMetaStatus) DeepCopyInto(dst *PluginMetaStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_schema_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_schema_gen.go new file mode 100644 index 00000000000..a4022c8de97 --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaPluginMeta = resource.NewSimpleSchema("plugins.grafana.app", "v0alpha1", &PluginMeta{}, &PluginMetaList{}, resource.WithKind("PluginMeta"), + resource.WithPlural("pluginmetas"), resource.WithScope(resource.NamespacedScope)) + kindPluginMeta = resource.Kind{ + Schema: schemaPluginMeta, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &PluginMetaJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func PluginMetaKind() resource.Kind { + return kindPluginMeta +} + +// Schema returns a resource.SimpleSchema representation of PluginMeta +func PluginMetaSchema() *resource.SimpleSchema { + return schemaPluginMeta +} + +// Interface compliance checks +var _ resource.Schema = kindPluginMeta diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_spec_gen.go new file mode 100644 index 00000000000..f9a58b291ad --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_spec_gen.go @@ -0,0 +1,473 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// JSON configuration schema for Grafana plugins +// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json +// +k8s:openapi-gen=true +type PluginMetaJSONData struct { + // Unique name of the plugin + Id string `json:"id"` + // Plugin type + Type PluginMetaJSONDataType `json:"type"` + // Human-readable name of the plugin + Name string `json:"name"` + // Metadata for the plugin + Info PluginMetaInfo `json:"info"` + // Dependency information + Dependencies PluginMetaDependencies `json:"dependencies"` + // Optional fields + Alerting *bool `json:"alerting,omitempty"` + Annotations *bool `json:"annotations,omitempty"` + AutoEnabled *bool `json:"autoEnabled,omitempty"` + Backend *bool `json:"backend,omitempty"` + BuildMode *string `json:"buildMode,omitempty"` + BuiltIn *bool `json:"builtIn,omitempty"` + Category *PluginMetaJSONDataCategory `json:"category,omitempty"` + EnterpriseFeatures *PluginMetaEnterpriseFeatures `json:"enterpriseFeatures,omitempty"` + Executable *string `json:"executable,omitempty"` + HideFromList *bool `json:"hideFromList,omitempty"` + // +listType=atomic + Includes []PluginMetaInclude `json:"includes,omitempty"` + Logs *bool `json:"logs,omitempty"` + Metrics *bool `json:"metrics,omitempty"` + MultiValueFilterOperators *bool `json:"multiValueFilterOperators,omitempty"` + PascalName *string `json:"pascalName,omitempty"` + Preload *bool `json:"preload,omitempty"` + QueryOptions *PluginMetaQueryOptions `json:"queryOptions,omitempty"` + // +listType=atomic + Routes []PluginMetaRoute `json:"routes,omitempty"` + SkipDataQuery *bool `json:"skipDataQuery,omitempty"` + State *PluginMetaJSONDataState `json:"state,omitempty"` + Streaming *bool `json:"streaming,omitempty"` + Tracing *bool `json:"tracing,omitempty"` + Iam *PluginMetaIAM `json:"iam,omitempty"` + // +listType=atomic + Roles []PluginMetaRole `json:"roles,omitempty"` + Extensions *PluginMetaExtensions `json:"extensions,omitempty"` +} + +// NewPluginMetaJSONData creates a new PluginMetaJSONData object. +func NewPluginMetaJSONData() *PluginMetaJSONData { + return &PluginMetaJSONData{ + Info: *NewPluginMetaInfo(), + Dependencies: *NewPluginMetaDependencies(), + } +} + +// +k8s:openapi-gen=true +type PluginMetaInfo struct { + // Required fields + // +listType=set + Keywords []string `json:"keywords"` + Logos PluginMetaV0alpha1InfoLogos `json:"logos"` + Updated string `json:"updated"` + Version string `json:"version"` + // Optional fields + Author *PluginMetaV0alpha1InfoAuthor `json:"author,omitempty"` + Description *string `json:"description,omitempty"` + // +listType=atomic + Links []PluginMetaV0alpha1InfoLinks `json:"links,omitempty"` + // +listType=atomic + Screenshots []PluginMetaV0alpha1InfoScreenshots `json:"screenshots,omitempty"` +} + +// NewPluginMetaInfo creates a new PluginMetaInfo object. +func NewPluginMetaInfo() *PluginMetaInfo { + return &PluginMetaInfo{ + Keywords: []string{}, + Logos: *NewPluginMetaV0alpha1InfoLogos(), + } +} + +// +k8s:openapi-gen=true +type PluginMetaDependencies struct { + // Required field + GrafanaDependency string `json:"grafanaDependency"` + // Optional fields + GrafanaVersion *string `json:"grafanaVersion,omitempty"` + // +listType=set + // +listMapKey=id + Plugins []PluginMetaV0alpha1DependenciesPlugins `json:"plugins,omitempty"` + Extensions *PluginMetaV0alpha1DependenciesExtensions `json:"extensions,omitempty"` +} + +// NewPluginMetaDependencies creates a new PluginMetaDependencies object. +func NewPluginMetaDependencies() *PluginMetaDependencies { + return &PluginMetaDependencies{} +} + +// +k8s:openapi-gen=true +type PluginMetaEnterpriseFeatures struct { + // Allow additional properties + HealthDiagnosticsErrors *bool `json:"healthDiagnosticsErrors,omitempty"` +} + +// NewPluginMetaEnterpriseFeatures creates a new PluginMetaEnterpriseFeatures object. +func NewPluginMetaEnterpriseFeatures() *PluginMetaEnterpriseFeatures { + return &PluginMetaEnterpriseFeatures{ + HealthDiagnosticsErrors: (func(input bool) *bool { return &input })(false), + } +} + +// +k8s:openapi-gen=true +type PluginMetaInclude struct { + Uid *string `json:"uid,omitempty"` + Type *PluginMetaIncludeType `json:"type,omitempty"` + Name *string `json:"name,omitempty"` + Component *string `json:"component,omitempty"` + Role *PluginMetaIncludeRole `json:"role,omitempty"` + Action *string `json:"action,omitempty"` + Path *string `json:"path,omitempty"` + AddToNav *bool `json:"addToNav,omitempty"` + DefaultNav *bool `json:"defaultNav,omitempty"` + Icon *string `json:"icon,omitempty"` +} + +// NewPluginMetaInclude creates a new PluginMetaInclude object. +func NewPluginMetaInclude() *PluginMetaInclude { + return &PluginMetaInclude{} +} + +// +k8s:openapi-gen=true +type PluginMetaQueryOptions struct { + MaxDataPoints *bool `json:"maxDataPoints,omitempty"` + MinInterval *bool `json:"minInterval,omitempty"` + CacheTimeout *bool `json:"cacheTimeout,omitempty"` +} + +// NewPluginMetaQueryOptions creates a new PluginMetaQueryOptions object. +func NewPluginMetaQueryOptions() *PluginMetaQueryOptions { + return &PluginMetaQueryOptions{} +} + +// +k8s:openapi-gen=true +type PluginMetaRoute struct { + Path *string `json:"path,omitempty"` + Method *string `json:"method,omitempty"` + Url *string `json:"url,omitempty"` + ReqSignedIn *bool `json:"reqSignedIn,omitempty"` + ReqRole *string `json:"reqRole,omitempty"` + ReqAction *string `json:"reqAction,omitempty"` + // +listType=atomic + Headers []string `json:"headers,omitempty"` + Body map[string]interface{} `json:"body,omitempty"` + TokenAuth *PluginMetaV0alpha1RouteTokenAuth `json:"tokenAuth,omitempty"` + JwtTokenAuth *PluginMetaV0alpha1RouteJwtTokenAuth `json:"jwtTokenAuth,omitempty"` + // +listType=atomic + UrlParams []PluginMetaV0alpha1RouteUrlParams `json:"urlParams,omitempty"` +} + +// NewPluginMetaRoute creates a new PluginMetaRoute object. +func NewPluginMetaRoute() *PluginMetaRoute { + return &PluginMetaRoute{} +} + +// +k8s:openapi-gen=true +type PluginMetaIAM struct { + // +listType=atomic + Permissions []PluginMetaV0alpha1IAMPermissions `json:"permissions,omitempty"` +} + +// NewPluginMetaIAM creates a new PluginMetaIAM object. +func NewPluginMetaIAM() *PluginMetaIAM { + return &PluginMetaIAM{} +} + +// +k8s:openapi-gen=true +type PluginMetaRole struct { + Role *PluginMetaV0alpha1RoleRole `json:"role,omitempty"` + // +listType=set + Grants []string `json:"grants,omitempty"` +} + +// NewPluginMetaRole creates a new PluginMetaRole object. +func NewPluginMetaRole() *PluginMetaRole { + return &PluginMetaRole{} +} + +// +k8s:openapi-gen=true +type PluginMetaExtensions struct { + // +listType=atomic + AddedComponents []PluginMetaV0alpha1ExtensionsAddedComponents `json:"addedComponents,omitempty"` + // +listType=atomic + AddedLinks []PluginMetaV0alpha1ExtensionsAddedLinks `json:"addedLinks,omitempty"` + // +listType=set + // +listMapKey=id + ExposedComponents []PluginMetaV0alpha1ExtensionsExposedComponents `json:"exposedComponents,omitempty"` + // +listType=set + // +listMapKey=id + ExtensionPoints []PluginMetaV0alpha1ExtensionsExtensionPoints `json:"extensionPoints,omitempty"` +} + +// NewPluginMetaExtensions creates a new PluginMetaExtensions object. +func NewPluginMetaExtensions() *PluginMetaExtensions { + return &PluginMetaExtensions{} +} + +// +k8s:openapi-gen=true +type PluginMetaSpec struct { + PluginJSON PluginMetaJSONData `json:"pluginJSON"` +} + +// NewPluginMetaSpec creates a new PluginMetaSpec object. +func NewPluginMetaSpec() *PluginMetaSpec { + return &PluginMetaSpec{ + PluginJSON: *NewPluginMetaJSONData(), + } +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1InfoLogos struct { + Small string `json:"small"` + Large string `json:"large"` +} + +// NewPluginMetaV0alpha1InfoLogos creates a new PluginMetaV0alpha1InfoLogos object. +func NewPluginMetaV0alpha1InfoLogos() *PluginMetaV0alpha1InfoLogos { + return &PluginMetaV0alpha1InfoLogos{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1InfoAuthor struct { + Name *string `json:"name,omitempty"` + Email *string `json:"email,omitempty"` + Url *string `json:"url,omitempty"` +} + +// NewPluginMetaV0alpha1InfoAuthor creates a new PluginMetaV0alpha1InfoAuthor object. +func NewPluginMetaV0alpha1InfoAuthor() *PluginMetaV0alpha1InfoAuthor { + return &PluginMetaV0alpha1InfoAuthor{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1InfoLinks struct { + Name *string `json:"name,omitempty"` + Url *string `json:"url,omitempty"` +} + +// NewPluginMetaV0alpha1InfoLinks creates a new PluginMetaV0alpha1InfoLinks object. +func NewPluginMetaV0alpha1InfoLinks() *PluginMetaV0alpha1InfoLinks { + return &PluginMetaV0alpha1InfoLinks{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1InfoScreenshots struct { + Name *string `json:"name,omitempty"` + Path *string `json:"path,omitempty"` +} + +// NewPluginMetaV0alpha1InfoScreenshots creates a new PluginMetaV0alpha1InfoScreenshots object. +func NewPluginMetaV0alpha1InfoScreenshots() *PluginMetaV0alpha1InfoScreenshots { + return &PluginMetaV0alpha1InfoScreenshots{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1DependenciesPlugins struct { + Id string `json:"id"` + Type PluginMetaV0alpha1DependenciesPluginsType `json:"type"` + Name string `json:"name"` +} + +// NewPluginMetaV0alpha1DependenciesPlugins creates a new PluginMetaV0alpha1DependenciesPlugins object. +func NewPluginMetaV0alpha1DependenciesPlugins() *PluginMetaV0alpha1DependenciesPlugins { + return &PluginMetaV0alpha1DependenciesPlugins{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1DependenciesExtensions struct { + // +listType=set + ExposedComponents []string `json:"exposedComponents,omitempty"` +} + +// NewPluginMetaV0alpha1DependenciesExtensions creates a new PluginMetaV0alpha1DependenciesExtensions object. +func NewPluginMetaV0alpha1DependenciesExtensions() *PluginMetaV0alpha1DependenciesExtensions { + return &PluginMetaV0alpha1DependenciesExtensions{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1RouteTokenAuth struct { + Url *string `json:"url,omitempty"` + // +listType=set + Scopes []string `json:"scopes,omitempty"` + Params map[string]interface{} `json:"params,omitempty"` +} + +// NewPluginMetaV0alpha1RouteTokenAuth creates a new PluginMetaV0alpha1RouteTokenAuth object. +func NewPluginMetaV0alpha1RouteTokenAuth() *PluginMetaV0alpha1RouteTokenAuth { + return &PluginMetaV0alpha1RouteTokenAuth{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1RouteJwtTokenAuth struct { + Url *string `json:"url,omitempty"` + // +listType=set + Scopes []string `json:"scopes,omitempty"` + Params map[string]interface{} `json:"params,omitempty"` +} + +// NewPluginMetaV0alpha1RouteJwtTokenAuth creates a new PluginMetaV0alpha1RouteJwtTokenAuth object. +func NewPluginMetaV0alpha1RouteJwtTokenAuth() *PluginMetaV0alpha1RouteJwtTokenAuth { + return &PluginMetaV0alpha1RouteJwtTokenAuth{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1RouteUrlParams struct { + Name *string `json:"name,omitempty"` + Content *string `json:"content,omitempty"` +} + +// NewPluginMetaV0alpha1RouteUrlParams creates a new PluginMetaV0alpha1RouteUrlParams object. +func NewPluginMetaV0alpha1RouteUrlParams() *PluginMetaV0alpha1RouteUrlParams { + return &PluginMetaV0alpha1RouteUrlParams{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1IAMPermissions struct { + Action *string `json:"action,omitempty"` + Scope *string `json:"scope,omitempty"` +} + +// NewPluginMetaV0alpha1IAMPermissions creates a new PluginMetaV0alpha1IAMPermissions object. +func NewPluginMetaV0alpha1IAMPermissions() *PluginMetaV0alpha1IAMPermissions { + return &PluginMetaV0alpha1IAMPermissions{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1RoleRolePermissions struct { + Action *string `json:"action,omitempty"` + Scope *string `json:"scope,omitempty"` +} + +// NewPluginMetaV0alpha1RoleRolePermissions creates a new PluginMetaV0alpha1RoleRolePermissions object. +func NewPluginMetaV0alpha1RoleRolePermissions() *PluginMetaV0alpha1RoleRolePermissions { + return &PluginMetaV0alpha1RoleRolePermissions{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1RoleRole struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + // +listType=atomic + Permissions []PluginMetaV0alpha1RoleRolePermissions `json:"permissions,omitempty"` +} + +// NewPluginMetaV0alpha1RoleRole creates a new PluginMetaV0alpha1RoleRole object. +func NewPluginMetaV0alpha1RoleRole() *PluginMetaV0alpha1RoleRole { + return &PluginMetaV0alpha1RoleRole{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1ExtensionsAddedComponents struct { + // +listType=set + Targets []string `json:"targets"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` +} + +// NewPluginMetaV0alpha1ExtensionsAddedComponents creates a new PluginMetaV0alpha1ExtensionsAddedComponents object. +func NewPluginMetaV0alpha1ExtensionsAddedComponents() *PluginMetaV0alpha1ExtensionsAddedComponents { + return &PluginMetaV0alpha1ExtensionsAddedComponents{ + Targets: []string{}, + } +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1ExtensionsAddedLinks struct { + // +listType=set + Targets []string `json:"targets"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` +} + +// NewPluginMetaV0alpha1ExtensionsAddedLinks creates a new PluginMetaV0alpha1ExtensionsAddedLinks object. +func NewPluginMetaV0alpha1ExtensionsAddedLinks() *PluginMetaV0alpha1ExtensionsAddedLinks { + return &PluginMetaV0alpha1ExtensionsAddedLinks{ + Targets: []string{}, + } +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1ExtensionsExposedComponents struct { + Id string `json:"id"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` +} + +// NewPluginMetaV0alpha1ExtensionsExposedComponents creates a new PluginMetaV0alpha1ExtensionsExposedComponents object. +func NewPluginMetaV0alpha1ExtensionsExposedComponents() *PluginMetaV0alpha1ExtensionsExposedComponents { + return &PluginMetaV0alpha1ExtensionsExposedComponents{} +} + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1ExtensionsExtensionPoints struct { + Id string `json:"id"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` +} + +// NewPluginMetaV0alpha1ExtensionsExtensionPoints creates a new PluginMetaV0alpha1ExtensionsExtensionPoints object. +func NewPluginMetaV0alpha1ExtensionsExtensionPoints() *PluginMetaV0alpha1ExtensionsExtensionPoints { + return &PluginMetaV0alpha1ExtensionsExtensionPoints{} +} + +// +k8s:openapi-gen=true +type PluginMetaJSONDataType string + +const ( + PluginMetaJSONDataTypeApp PluginMetaJSONDataType = "app" + PluginMetaJSONDataTypeDatasource PluginMetaJSONDataType = "datasource" + PluginMetaJSONDataTypePanel PluginMetaJSONDataType = "panel" + PluginMetaJSONDataTypeRenderer PluginMetaJSONDataType = "renderer" +) + +// +k8s:openapi-gen=true +type PluginMetaJSONDataCategory string + +const ( + PluginMetaJSONDataCategoryTsdb PluginMetaJSONDataCategory = "tsdb" + PluginMetaJSONDataCategoryLogging PluginMetaJSONDataCategory = "logging" + PluginMetaJSONDataCategoryCloud PluginMetaJSONDataCategory = "cloud" + PluginMetaJSONDataCategoryTracing PluginMetaJSONDataCategory = "tracing" + PluginMetaJSONDataCategoryProfiling PluginMetaJSONDataCategory = "profiling" + PluginMetaJSONDataCategorySql PluginMetaJSONDataCategory = "sql" + PluginMetaJSONDataCategoryEnterprise PluginMetaJSONDataCategory = "enterprise" + PluginMetaJSONDataCategoryIot PluginMetaJSONDataCategory = "iot" + PluginMetaJSONDataCategoryOther PluginMetaJSONDataCategory = "other" +) + +// +k8s:openapi-gen=true +type PluginMetaJSONDataState string + +const ( + PluginMetaJSONDataStateAlpha PluginMetaJSONDataState = "alpha" + PluginMetaJSONDataStateBeta PluginMetaJSONDataState = "beta" +) + +// +k8s:openapi-gen=true +type PluginMetaIncludeType string + +const ( + PluginMetaIncludeTypeDashboard PluginMetaIncludeType = "dashboard" + PluginMetaIncludeTypePage PluginMetaIncludeType = "page" + PluginMetaIncludeTypePanel PluginMetaIncludeType = "panel" + PluginMetaIncludeTypeDatasource PluginMetaIncludeType = "datasource" +) + +// +k8s:openapi-gen=true +type PluginMetaIncludeRole string + +const ( + PluginMetaIncludeRoleAdmin PluginMetaIncludeRole = "Admin" + PluginMetaIncludeRoleEditor PluginMetaIncludeRole = "Editor" + PluginMetaIncludeRoleViewer PluginMetaIncludeRole = "Viewer" +) + +// +k8s:openapi-gen=true +type PluginMetaV0alpha1DependenciesPluginsType string + +const ( + PluginMetaV0alpha1DependenciesPluginsTypeApp PluginMetaV0alpha1DependenciesPluginsType = "app" + PluginMetaV0alpha1DependenciesPluginsTypeDatasource PluginMetaV0alpha1DependenciesPluginsType = "datasource" + PluginMetaV0alpha1DependenciesPluginsTypePanel PluginMetaV0alpha1DependenciesPluginsType = "panel" +) diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go new file mode 100644 index 00000000000..60fa37dbb32 --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type PluginMetastatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State PluginMetaStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewPluginMetastatusOperatorState creates a new PluginMetastatusOperatorState object. +func NewPluginMetastatusOperatorState() *PluginMetastatusOperatorState { + return &PluginMetastatusOperatorState{} +} + +// +k8s:openapi-gen=true +type PluginMetaStatus struct { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]PluginMetastatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewPluginMetaStatus creates a new PluginMetaStatus object. +func NewPluginMetaStatus() *PluginMetaStatus { + return &PluginMetaStatus{} +} + +// +k8s:openapi-gen=true +type PluginMetaStatusOperatorStateState string + +const ( + PluginMetaStatusOperatorStateStateSuccess PluginMetaStatusOperatorStateState = "success" + PluginMetaStatusOperatorStateStateInProgress PluginMetaStatusOperatorStateState = "in_progress" + PluginMetaStatusOperatorStateStateFailed PluginMetaStatusOperatorStateState = "failed" +) diff --git a/apps/plugins/pkg/apis/plugins_manifest.go b/apps/plugins/pkg/apis/plugins_manifest.go index 09c57eaa186..b5a47a6715b 100644 --- a/apps/plugins/pkg/apis/plugins_manifest.go +++ b/apps/plugins/pkg/apis/plugins_manifest.go @@ -20,9 +20,12 @@ import ( ) var ( - rawSchemaPluginv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Plugin":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"getMetaDependencies":{"type":"object","required":["grafanaDependency"],"properties":{"extensions":{"type":"object","properties":{"exposedComponents":{"description":"+listType=set","type":"array","items":{"type":"string"}}},"additionalProperties":false},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id","type","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["app","datasource","panel"]}},"additionalProperties":false}}},"additionalProperties":false},"getMetaEnterpriseFeatures":{"type":"object","properties":{"healthDiagnosticsErrors":{"description":"Allow additional properties","type":"boolean","default":false}},"additionalProperties":false},"getMetaExtensions":{"type":"object","properties":{"addedComponents":{"description":"+listType=atomic","type":"array","items":{"type":"object","required":["targets","title"],"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","type":"array","items":{"type":"string"}},"title":{"type":"string"}},"additionalProperties":false}},"addedLinks":{"description":"+listType=atomic","type":"array","items":{"type":"object","required":["targets","title"],"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","type":"array","items":{"type":"string"}},"title":{"type":"string"}},"additionalProperties":false}},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"additionalProperties":false}},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","type":"array","items":{"type":"object","required":["id"],"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"getMetaIAM":{"type":"object","properties":{"permissions":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"scope":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"getMetaInclude":{"type":"object","properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"type":"string","enum":["Admin","Editor","Viewer"]},"type":{"type":"string","enum":["dashboard","page","panel","datasource"]},"uid":{"type":"string"}},"additionalProperties":false},"getMetaInfo":{"type":"object","required":["keywords","logos","updated","version"],"properties":{"author":{"description":"Optional fields","type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","type":"array","items":{"type":"string"}},"links":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"url":{"type":"string"}},"additionalProperties":false}},"logos":{"type":"object","required":["small","large"],"properties":{"large":{"type":"string"},"small":{"type":"string"}},"additionalProperties":false},"screenshots":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"path":{"type":"string"}},"additionalProperties":false}},"updated":{"type":"string","format":"date-time"},"version":{"type":"string"}},"additionalProperties":false},"getMetaQueryOptions":{"type":"object","properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"additionalProperties":false},"getMetaRole":{"type":"object","properties":{"grants":{"description":"+listType=set","type":"array","items":{"type":"string"}},"role":{"type":"object","properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"scope":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false}},"additionalProperties":false},"getMetaRoute":{"type":"object","properties":{"body":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"headers":{"description":"+listType=atomic","type":"array","items":{"type":"string"}},"jwtTokenAuth":{"type":"object","properties":{"params":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"scopes":{"description":"+listType=set","type":"array","items":{"type":"string"}},"url":{"type":"string"}},"additionalProperties":false},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"type":"object","properties":{"params":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"scopes":{"description":"+listType=set","type":"array","items":{"type":"string"}},"url":{"type":"string"}},"additionalProperties":false},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","type":"array","items":{"type":"object","properties":{"content":{"type":"string"},"name":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external"],"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) - versionSchemaPluginv0alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaPluginv0alpha1, &versionSchemaPluginv0alpha1) + rawSchemaPluginv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Plugin":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external"],"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + versionSchemaPluginv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaPluginv0alpha1, &versionSchemaPluginv0alpha1) + rawSchemaPluginMetav0alpha1 = []byte(`{"Dependencies":{"additionalProperties":false,"properties":{"extensions":{"additionalProperties":false,"properties":{"exposedComponents":{"description":"+listType=set","items":{"type":"string"},"type":"array"}},"type":"object"},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"enum":["app","datasource","panel"],"type":"string"}},"required":["id","type","name"],"type":"object"},"type":"array"}},"required":["grafanaDependency"],"type":"object"},"EnterpriseFeatures":{"additionalProperties":false,"properties":{"healthDiagnosticsErrors":{"default":false,"description":"Allow additional properties","type":"boolean"}},"type":"object"},"Extensions":{"additionalProperties":false,"properties":{"addedComponents":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"addedLinks":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"}},"type":"object"},"IAM":{"additionalProperties":false,"properties":{"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"Include":{"additionalProperties":false,"properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"enum":["Admin","Editor","Viewer"],"type":"string"},"type":{"enum":["dashboard","page","panel","datasource"],"type":"string"},"uid":{"type":"string"}},"type":"object"},"Info":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"description":"Optional fields","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","items":{"type":"string"},"type":"array"},"links":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"type":"array"},"logos":{"additionalProperties":false,"properties":{"large":{"type":"string"},"small":{"type":"string"}},"required":["small","large"],"type":"object"},"screenshots":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"path":{"type":"string"}},"type":"object"},"type":"array"},"updated":{"type":"string"},"version":{"type":"string"}},"required":["keywords","logos","updated","version"],"type":"object"},"JSONData":{"additionalProperties":false,"description":"JSON configuration schema for Grafana plugins\nConverted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json","properties":{"alerting":{"description":"Optional fields","type":"boolean"},"annotations":{"type":"boolean"},"autoEnabled":{"type":"boolean"},"backend":{"type":"boolean"},"buildMode":{"type":"string"},"builtIn":{"type":"boolean"},"category":{"enum":["tsdb","logging","cloud","tracing","profiling","sql","enterprise","iot","other"],"type":"string"},"dependencies":{"$ref":"#/components/schemas/Dependencies","description":"Dependency information"},"enterpriseFeatures":{"$ref":"#/components/schemas/EnterpriseFeatures"},"executable":{"type":"string"},"extensions":{"$ref":"#/components/schemas/Extensions"},"hideFromList":{"type":"boolean"},"iam":{"$ref":"#/components/schemas/IAM"},"id":{"description":"Unique name of the plugin","type":"string"},"includes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Include"},"type":"array"},"info":{"$ref":"#/components/schemas/Info","description":"Metadata for the plugin"},"logs":{"type":"boolean"},"metrics":{"type":"boolean"},"multiValueFilterOperators":{"type":"boolean"},"name":{"description":"Human-readable name of the plugin","type":"string"},"pascalName":{"type":"string"},"preload":{"type":"boolean"},"queryOptions":{"$ref":"#/components/schemas/QueryOptions"},"roles":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Role"},"type":"array"},"routes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Route"},"type":"array"},"skipDataQuery":{"type":"boolean"},"state":{"enum":["alpha","beta"],"type":"string"},"streaming":{"type":"boolean"},"tracing":{"type":"boolean"},"type":{"description":"Plugin type","enum":["app","datasource","panel","renderer"],"type":"string"}},"required":["id","type","name","info","dependencies"],"type":"object"},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"PluginMeta":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"QueryOptions":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"type":"object"},"Role":{"additionalProperties":false,"properties":{"grants":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"role":{"additionalProperties":false,"properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"}},"type":"object"},"Route":{"additionalProperties":false,"properties":{"body":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"headers":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"jwtTokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"content":{"type":"string"},"name":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"spec":{"additionalProperties":false,"properties":{"pluginJSON":{"$ref":"#/components/schemas/JSONData"}},"required":["pluginJSON"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + versionSchemaPluginMetav0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaPluginMetav0alpha1, &versionSchemaPluginMetav0alpha1) ) var appManifestData = app.ManifestData{ @@ -40,229 +43,14 @@ var appManifestData = app.ManifestData{ Scope: "Namespaced", Conversion: false, Schema: &versionSchemaPluginv0alpha1, - Routes: map[string]spec3.PathProps{ - "/meta": { - Get: &spec3.Operation{ - OperationProps: spec3.OperationProps{ + }, - OperationId: "getMeta", - - Responses: &spec3.Responses{ - ResponsesProps: spec3.ResponsesProps{ - Default: &spec3.Response{ - ResponseProps: spec3.ResponseProps{ - Description: "Default OK response", - Content: map[string]*spec3.MediaType{ - "application/json": { - MediaTypeProps: spec3.MediaTypeProps{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "alerting": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Description: "Optional fields", - }, - }, - "annotations": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - }, - }, - "autoEnabled": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - }, - }, - "backend": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - }, - }, - "buildMode": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - }, - }, - "builtIn": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - }, - }, - "category": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Enum: []interface{}{ - "tsdb", - "logging", - "cloud", - "tracing", - "profiling", - "sql", - "enterprise", - "iot", - "other", - }, - }, - }, - "dependencies": { - SchemaProps: spec.SchemaProps{ - - Description: "Dependency information", - Ref: spec.MustCreateRef("#/components/schemas/getMetaDependencies"), - }, - }, - "enterpriseFeatures": { - SchemaProps: spec.SchemaProps{ - - Ref: spec.MustCreateRef("#/components/schemas/getMetaEnterpriseFeatures"), - }, - }, - "executable": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - }, - }, - "extensions": { - SchemaProps: spec.SchemaProps{ - - Ref: spec.MustCreateRef("#/components/schemas/getMetaExtensions"), - }, - }, - "hideFromList": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - }, - }, - "iam": { - SchemaProps: spec.SchemaProps{ - - Ref: spec.MustCreateRef("#/components/schemas/getMetaIAM"), - }, - }, - "id": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Description: "Unique name of the plugin", - }, - }, - "includes": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Description: "+listType=atomic", - }, - }, - "info": { - SchemaProps: spec.SchemaProps{ - - Description: "Metadata for the plugin", - Ref: spec.MustCreateRef("#/components/schemas/getMetaInfo"), - }, - }, - "logs": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - }, - }, - "metrics": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - }, - }, - "multiValueFilterOperators": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - }, - }, - "name": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Description: "Human-readable name of the plugin", - }, - }, - "pascalName": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - }, - }, - "preload": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - }, - }, - "queryOptions": { - SchemaProps: spec.SchemaProps{ - - Ref: spec.MustCreateRef("#/components/schemas/getMetaQueryOptions"), - }, - }, - "roles": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Description: "+listType=atomic", - }, - }, - "routes": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Description: "+listType=atomic", - }, - }, - "skipDataQuery": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - }, - }, - "state": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Enum: []interface{}{ - "alpha", - "beta", - }, - }, - }, - "streaming": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - }, - }, - "tracing": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - }, - }, - "type": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Description: "Plugin type", - Enum: []interface{}{ - "app", - "datasource", - "panel", - "renderer", - }, - }, - }, - }, - Required: []string{ - "id", - "type", - "name", - "info", - "dependencies", - }, - }}, - }}, - }, - }, - }, - }}, - }, - }, - }, - }, + { + Kind: "PluginMeta", + Plural: "PluginMetas", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaPluginMetav0alpha1, }, }, Routes: app.ManifestVersionRoutes{ @@ -283,7 +71,8 @@ func RemoteManifest() app.Manifest { } var kindVersionToGoType = map[string]resource.Kind{ - "Plugin/v0alpha1": v0alpha1.PluginKind(), + "Plugin/v0alpha1": v0alpha1.PluginKind(), + "PluginMeta/v0alpha1": v0alpha1.PluginMetaKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. @@ -293,9 +82,7 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist return goType, exists } -var customRouteToGoResponseType = map[string]any{ - "v0alpha1|Plugin|meta|GET": v0alpha1.GetMeta{}, -} +var customRouteToGoResponseType = map[string]any{} // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. // kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. diff --git a/apps/plugins/pkg/app/app.go b/apps/plugins/pkg/app/app.go index 903e2580ab0..21c132ad081 100644 --- a/apps/plugins/pkg/app/app.go +++ b/apps/plugins/pkg/app/app.go @@ -2,27 +2,23 @@ package app import ( "context" - "net/http" + "fmt" "github.com/grafana/grafana-app-sdk/app" - "github.com/grafana/grafana-app-sdk/k8s" - "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana-app-sdk/operator" - "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog/v2" - pluginsapi "github.com/grafana/grafana/apps/plugins/pkg/apis" pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" + "github.com/grafana/grafana/apps/plugins/pkg/app/meta" ) func New(cfg app.Config) (app.App, error) { cfg.KubeConfig.APIPath = "apis" - clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig()) - client, err := pluginsv0alpha1.NewPluginClientFromGenerator(clientGenerator) - if err != nil { - return nil, err + + specificConfig, ok := cfg.SpecificConfig.(*PluginAppConfig) + if !ok { + return nil, fmt.Errorf("invalid config type") } simpleConfig := simple.AppConfig{ @@ -38,27 +34,9 @@ func New(cfg app.Config) (app.App, error) { ManagedKinds: []simple.AppManagedKind{ { Kind: pluginsv0alpha1.PluginKind(), - CustomRoutes: simple.AppCustomRouteHandlers{ - simple.AppCustomRoute{ - Method: http.MethodGet, - Path: "meta", - }: func(ctx context.Context, w app.CustomRouteResponseWriter, req *app.CustomRouteRequest) error { - plugin, err := client.Get(ctx, resource.Identifier{ - Namespace: req.ResourceIdentifier.Namespace, - Name: req.ResourceIdentifier.Name, - }) - if err != nil { - return err - } - logging.DefaultLogger.Debug("fetched plugin", "plugin", plugin) - // TODO: Implement this in future PR - w.WriteHeader(http.StatusNotImplemented) - if _, err := w.Write([]byte("Not implemented")); err != nil { - return err - } - return nil - }, - }, + }, + { + Kind: pluginsv0alpha1.PluginMetaKind(), }, }, } @@ -73,23 +51,12 @@ func New(cfg app.Config) (app.App, error) { return nil, err } + // Register MetaProviderManager as a runnable so its cleanup goroutine is managed by the app lifecycle + a.AddRunnable(specificConfig.MetaProviderManager) + return a, nil } -func GetKinds() map[schema.GroupVersion][]resource.Kind { - kinds := make(map[schema.GroupVersion][]resource.Kind) - manifest := pluginsapi.LocalManifest() - for _, v := range manifest.ManifestData.Versions { - gv := schema.GroupVersion{ - Group: manifest.ManifestData.Group, - Version: v.Name, - } - for _, k := range v.Kinds { - kind, ok := pluginsapi.ManifestGoTypeAssociator(k.Kind, v.Name) - if ok { - kinds[gv] = append(kinds[gv], kind) - } - } - } - return kinds +type PluginAppConfig struct { + MetaProviderManager *meta.ProviderManager } diff --git a/apps/plugins/pkg/app/meta/cloud.go b/apps/plugins/pkg/app/meta/cloud.go new file mode 100644 index 00000000000..f49445d093a --- /dev/null +++ b/apps/plugins/pkg/app/meta/cloud.go @@ -0,0 +1,131 @@ +package meta + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "time" + + "github.com/grafana/grafana-app-sdk/logging" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" +) + +const ( + defaultCloudTTL = 1 * time.Hour +) + +// CloudProvider retrieves plugin metadata from the grafana.com API. +type CloudProvider struct { + httpClient *http.Client + grafanaComAPIURL string + log logging.Logger + ttl time.Duration +} + +// NewCloudProvider creates a new CloudProvider that fetches metadata from grafana.com. +func NewCloudProvider(grafanaComAPIURL string) *CloudProvider { + return NewCloudProviderWithTTL(grafanaComAPIURL, defaultCloudTTL) +} + +// NewCloudProviderWithTTL creates a new CloudProvider with a custom TTL. +func NewCloudProviderWithTTL(grafanaComAPIURL string, ttl time.Duration) *CloudProvider { + if grafanaComAPIURL == "" { + grafanaComAPIURL = "https://grafana.com/api/plugins" + } + + return &CloudProvider{ + httpClient: &http.Client{ + Timeout: 10 * time.Second, + }, + grafanaComAPIURL: grafanaComAPIURL, + log: logging.DefaultLogger, + ttl: ttl, + } +} + +// GetMeta fetches plugin metadata from grafana.com API endpoint: +// GET /api/plugins/{pluginId}/versions/{version} +func (p *CloudProvider) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) { + u, err := url.Parse(p.grafanaComAPIURL) + if err != nil { + return nil, fmt.Errorf("invalid grafana.com API URL: %w", err) + } + + u.Path = path.Join(u.Path, pluginID, "versions", version) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", "grafana-plugins-app") + + resp, err := p.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch plugin metadata: %w", err) + } + defer func() { + if err = resp.Body.Close(); err != nil { + p.log.Warn("Failed to close response body", "error", err) + } + }() + + if resp.StatusCode == http.StatusNotFound { + return nil, ErrMetaNotFound + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code %d from grafana.com API", resp.StatusCode) + } + + var gcomMeta grafanaComPluginVersionMeta + if err = json.NewDecoder(resp.Body).Decode(&gcomMeta); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &Result{ + Meta: gcomMeta.JSON, + TTL: p.ttl, + }, nil +} + +// grafanaComPluginVersionMeta represents the response from grafana.com API +// GET /api/plugins/{pluginId}/versions/{version} +type grafanaComPluginVersionMeta struct { + PluginID string `json:"pluginSlug"` + Version string `json:"version"` + URL string `json:"url"` + Commit string `json:"commit"` + Description string `json:"description"` + Keywords []string `json:"keywords"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + JSON pluginsv0alpha1.PluginMetaJSONData `json:"json"` + Readme string `json:"readme"` + Downloads int `json:"downloads"` + Verified bool `json:"verified"` + Status string `json:"status"` + StatusContext string `json:"statusContext"` + DownloadSlug string `json:"downloadSlug"` + SignatureType string `json:"signatureType"` + SignedByOrg string `json:"signedByOrg"` + SignedByOrgName string `json:"signedByOrgName"` + Packages struct { + Any struct { + Md5 string `json:"md5"` + Sha256 string `json:"sha256"` + PackageName string `json:"packageName"` + DownloadURL string `json:"downloadUrl"` + } `json:"any"` + } `json:"packages"` + Links []struct { + Rel string `json:"rel"` + Href string `json:"href"` + } `json:"links"` + AngularDetected bool `json:"angularDetected"` + Scopes []string `json:"scopes"` +} diff --git a/apps/plugins/pkg/app/meta/cloud_test.go b/apps/plugins/pkg/app/meta/cloud_test.go new file mode 100644 index 00000000000..bef2d1a5041 --- /dev/null +++ b/apps/plugins/pkg/app/meta/cloud_test.go @@ -0,0 +1,186 @@ +package meta + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" +) + +func TestCloudProvider_GetMeta(t *testing.T) { + ctx := context.Background() + + t.Run("successfully fetches plugin metadata", func(t *testing.T) { + expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/api/plugins/test-plugin/versions/1.0.0", r.URL.Path) + assert.Equal(t, "application/json", r.Header.Get("Accept")) + assert.Equal(t, "grafana-plugins-app", r.Header.Get("User-Agent")) + + response := grafanaComPluginVersionMeta{ + PluginID: "test-plugin", + Version: "1.0.0", + JSON: expectedMeta, + Description: "Test description", + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + require.NoError(t, json.NewEncoder(w).Encode(response)) + })) + defer server.Close() + + provider := NewCloudProvider(server.URL + "/api/plugins") + result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, expectedMeta, result.Meta) + assert.Equal(t, defaultCloudTTL, result.TTL) + }) + + t.Run("returns ErrMetaNotFound for 404 status", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + provider := NewCloudProvider(server.URL + "/api/plugins") + result, err := provider.GetMeta(ctx, "nonexistent-plugin", "1.0.0") + + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrMetaNotFound)) + assert.Nil(t, result) + }) + + t.Run("returns error for non-200 status codes", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + provider := NewCloudProvider(server.URL + "/api/plugins") + result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "unexpected status code 500") + assert.Nil(t, result) + }) + + t.Run("returns error for invalid JSON response", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("invalid json")) + })) + defer server.Close() + + provider := NewCloudProvider(server.URL + "/api/plugins") + result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to decode response") + assert.Nil(t, result) + }) + + t.Run("returns error for invalid API URL", func(t *testing.T) { + provider := NewCloudProvider("://invalid-url") + result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid grafana.com API URL") + assert.Nil(t, result) + }) + + t.Run("uses custom TTL when provided", func(t *testing.T) { + customTTL := 2 * time.Hour + expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := grafanaComPluginVersionMeta{ + PluginID: "test-plugin", + Version: "1.0.0", + JSON: expectedMeta, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + require.NoError(t, json.NewEncoder(w).Encode(response)) + })) + defer server.Close() + + provider := NewCloudProviderWithTTL(server.URL+"/api/plugins", customTTL) + result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, customTTL, result.TTL) + }) + + t.Run("handles context cancellation", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(100 * time.Millisecond) + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + provider := NewCloudProvider(server.URL + "/api/plugins") + result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") + + assert.Error(t, err) + assert.Nil(t, result) + }) +} + +func TestNewCloudProvider(t *testing.T) { + t.Run("creates provider with default TTL", func(t *testing.T) { + provider := NewCloudProvider("https://grafana.com/api/plugins") + assert.Equal(t, defaultCloudTTL, provider.ttl) + assert.NotNil(t, provider.httpClient) + assert.Equal(t, "https://grafana.com/api/plugins", provider.grafanaComAPIURL) + }) + + t.Run("uses default URL when empty", func(t *testing.T) { + provider := NewCloudProvider("") + assert.Equal(t, "https://grafana.com/api/plugins", provider.grafanaComAPIURL) + }) +} + +func TestNewCloudProviderWithTTL(t *testing.T) { + t.Run("creates provider with custom TTL", func(t *testing.T) { + customTTL := 2 * time.Hour + provider := NewCloudProviderWithTTL("https://grafana.com/api/plugins", customTTL) + assert.Equal(t, customTTL, provider.ttl) + }) + + t.Run("accepts zero TTL", func(t *testing.T) { + provider := NewCloudProviderWithTTL("https://grafana.com/api/plugins", 0) + assert.Equal(t, time.Duration(0), provider.ttl) + }) + + t.Run("uses default URL when empty", func(t *testing.T) { + provider := NewCloudProviderWithTTL("", defaultCloudTTL) + assert.Equal(t, "https://grafana.com/api/plugins", provider.grafanaComAPIURL) + }) +} diff --git a/apps/plugins/pkg/app/meta/core.go b/apps/plugins/pkg/app/meta/core.go new file mode 100644 index 00000000000..3452f95b24b --- /dev/null +++ b/apps/plugins/pkg/app/meta/core.go @@ -0,0 +1,603 @@ +package meta + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "sync" + "time" + + "github.com/grafana/grafana-app-sdk/logging" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/manager/sources" +) + +const ( + defaultCoreTTL = 24 * time.Hour +) + +// CoreProvider retrieves plugin metadata for core plugins. +type CoreProvider struct { + mu sync.RWMutex + loadedPlugins map[string]pluginsv0alpha1.PluginMetaJSONData + initialized bool + ttl time.Duration +} + +// NewCoreProvider creates a new CoreProvider for core plugins. +func NewCoreProvider() *CoreProvider { + return NewCoreProviderWithTTL(defaultCoreTTL) +} + +// NewCoreProviderWithTTL creates a new CoreProvider with a custom TTL. +func NewCoreProviderWithTTL(ttl time.Duration) *CoreProvider { + return &CoreProvider{ + loadedPlugins: make(map[string]pluginsv0alpha1.PluginMetaJSONData), + ttl: ttl, + } +} + +// GetMeta retrieves plugin metadata for core plugins. +func (p *CoreProvider) GetMeta(ctx context.Context, pluginID, _ string) (*Result, error) { + // Check cache first + p.mu.RLock() + if meta, found := p.loadedPlugins[pluginID]; found { + p.mu.RUnlock() + return &Result{ + Meta: meta, + TTL: p.ttl, + }, nil + } + p.mu.RUnlock() + + // Initialize cache if not already done + p.mu.Lock() + defer p.mu.Unlock() + + // Double-check after acquiring write lock + if meta, found := p.loadedPlugins[pluginID]; found { + return &Result{ + Meta: meta, + TTL: p.ttl, + }, nil + } + + if !p.initialized { + if err := p.loadPlugins(ctx); err != nil { + logging.DefaultLogger.Warn("CoreProvider: could not load core plugins, will return ErrMetaNotFound for all lookups", "error", err) + // Mark as initialized even on failure so we don't keep trying + p.initialized = true + return nil, ErrMetaNotFound + } + p.initialized = true + } + + if meta, found := p.loadedPlugins[pluginID]; found { + return &Result{ + Meta: meta, + TTL: p.ttl, + }, nil + } + + return nil, ErrMetaNotFound +} + +// loadPlugins discovers and caches all core plugins. +// Returns an error if the static root path cannot be found or if plugin discovery fails. +// This error will be handled gracefully by GetMeta, which will return ErrMetaNotFound +// to allow other providers to handle the request. +func (p *CoreProvider) loadPlugins(ctx context.Context) error { + var staticRootPath string + if wd, err := os.Getwd(); err == nil { + // Check if we're in the Grafana root + publicPath := filepath.Join(wd, "public", "app", "plugins") + if _, err = os.Stat(publicPath); err == nil { + staticRootPath = filepath.Join(wd, "public") + } + } + + if staticRootPath == "" { + return errors.New("could not find Grafana static root path") + } + + datasourcePath := filepath.Join(staticRootPath, "app", "plugins", "datasource") + panelPath := filepath.Join(staticRootPath, "app", "plugins", "panel") + + src := sources.NewLocalSource(plugins.ClassCore, []string{datasourcePath, panelPath}) + ps, err := src.Discover(ctx) + if err != nil { + return err + } + + if len(ps) == 0 { + logging.DefaultLogger.Warn("CoreProvider: no core plugins found during discovery") + return nil + } + + for _, bundle := range ps { + meta := jsonDataToPluginMetaJSONData(bundle.Primary.JSONData) + p.loadedPlugins[bundle.Primary.JSONData.ID] = meta + } + + return nil +} + +// jsonDataToPluginMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.PluginMetaJSONData. +// nolint:gocyclo +func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.PluginMetaJSONData { + meta := pluginsv0alpha1.PluginMetaJSONData{ + Id: jsonData.ID, + Name: jsonData.Name, + } + + // Map plugin type + switch jsonData.Type { + case plugins.TypeApp: + meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeApp + case plugins.TypeDataSource: + meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeDatasource + case plugins.TypePanel: + meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypePanel + case plugins.TypeRenderer: + meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeRenderer + } + + // Map Info + meta.Info = pluginsv0alpha1.PluginMetaInfo{ + Keywords: jsonData.Info.Keywords, + Logos: pluginsv0alpha1.PluginMetaV0alpha1InfoLogos{ + Small: jsonData.Info.Logos.Small, + Large: jsonData.Info.Logos.Large, + }, + Updated: jsonData.Info.Updated, + Version: jsonData.Info.Version, + } + + if jsonData.Info.Description != "" { + meta.Info.Description = &jsonData.Info.Description + } + + if jsonData.Info.Author.Name != "" || jsonData.Info.Author.URL != "" { + author := &pluginsv0alpha1.PluginMetaV0alpha1InfoAuthor{} + if jsonData.Info.Author.Name != "" { + author.Name = &jsonData.Info.Author.Name + } + if jsonData.Info.Author.URL != "" { + author.Url = &jsonData.Info.Author.URL + } + meta.Info.Author = author + } + + if len(jsonData.Info.Links) > 0 { + meta.Info.Links = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links)) + for _, link := range jsonData.Info.Links { + v0Link := pluginsv0alpha1.PluginMetaV0alpha1InfoLinks{} + if link.Name != "" { + v0Link.Name = &link.Name + } + if link.URL != "" { + v0Link.Url = &link.URL + } + meta.Info.Links = append(meta.Info.Links, v0Link) + } + } + + if len(jsonData.Info.Screenshots) > 0 { + meta.Info.Screenshots = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots)) + for _, screenshot := range jsonData.Info.Screenshots { + v0Screenshot := pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots{} + if screenshot.Name != "" { + v0Screenshot.Name = &screenshot.Name + } + if screenshot.Path != "" { + v0Screenshot.Path = &screenshot.Path + } + meta.Info.Screenshots = append(meta.Info.Screenshots, v0Screenshot) + } + } + + // Map Dependencies + meta.Dependencies = pluginsv0alpha1.PluginMetaDependencies{ + GrafanaDependency: jsonData.Dependencies.GrafanaDependency, + } + + if jsonData.Dependencies.GrafanaVersion != "" { + meta.Dependencies.GrafanaVersion = &jsonData.Dependencies.GrafanaVersion + } + + if len(jsonData.Dependencies.Plugins) > 0 { + meta.Dependencies.Plugins = make([]pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins)) + for _, dep := range jsonData.Dependencies.Plugins { + var depType pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsType + switch dep.Type { + case "app": + depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeApp + case "datasource": + depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeDatasource + case "panel": + depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypePanel + } + meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins{ + Id: dep.ID, + Type: depType, + Name: dep.Name, + }) + } + } + + if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 { + meta.Dependencies.Extensions = &pluginsv0alpha1.PluginMetaV0alpha1DependenciesExtensions{ + ExposedComponents: jsonData.Dependencies.Extensions.ExposedComponents, + } + } + + // Map optional boolean fields + if jsonData.Alerting { + meta.Alerting = &jsonData.Alerting + } + if jsonData.Annotations { + meta.Annotations = &jsonData.Annotations + } + if jsonData.AutoEnabled { + meta.AutoEnabled = &jsonData.AutoEnabled + } + if jsonData.Backend { + meta.Backend = &jsonData.Backend + } + if jsonData.BuiltIn { + meta.BuiltIn = &jsonData.BuiltIn + } + if jsonData.HideFromList { + meta.HideFromList = &jsonData.HideFromList + } + if jsonData.Logs { + meta.Logs = &jsonData.Logs + } + if jsonData.Metrics { + meta.Metrics = &jsonData.Metrics + } + if jsonData.MultiValueFilterOperators { + meta.MultiValueFilterOperators = &jsonData.MultiValueFilterOperators + } + if jsonData.Preload { + meta.Preload = &jsonData.Preload + } + if jsonData.SkipDataQuery { + meta.SkipDataQuery = &jsonData.SkipDataQuery + } + if jsonData.Streaming { + meta.Streaming = &jsonData.Streaming + } + if jsonData.Tracing { + meta.Tracing = &jsonData.Tracing + } + + // Map category + if jsonData.Category != "" { + var category pluginsv0alpha1.PluginMetaJSONDataCategory + switch jsonData.Category { + case "tsdb": + category = pluginsv0alpha1.PluginMetaJSONDataCategoryTsdb + case "logging": + category = pluginsv0alpha1.PluginMetaJSONDataCategoryLogging + case "cloud": + category = pluginsv0alpha1.PluginMetaJSONDataCategoryCloud + case "tracing": + category = pluginsv0alpha1.PluginMetaJSONDataCategoryTracing + case "profiling": + category = pluginsv0alpha1.PluginMetaJSONDataCategoryProfiling + case "sql": + category = pluginsv0alpha1.PluginMetaJSONDataCategorySql + case "enterprise": + category = pluginsv0alpha1.PluginMetaJSONDataCategoryEnterprise + case "iot": + category = pluginsv0alpha1.PluginMetaJSONDataCategoryIot + case "other": + category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther + default: + category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther + } + meta.Category = &category + } + + // Map state + if jsonData.State != "" { + var state pluginsv0alpha1.PluginMetaJSONDataState + switch jsonData.State { + case plugins.ReleaseStateAlpha: + state = pluginsv0alpha1.PluginMetaJSONDataStateAlpha + case plugins.ReleaseStateBeta: + state = pluginsv0alpha1.PluginMetaJSONDataStateBeta + default: + } + if state != "" { + meta.State = &state + } + } + + // Map executable + if jsonData.Executable != "" { + meta.Executable = &jsonData.Executable + } + + // Map QueryOptions + if len(jsonData.QueryOptions) > 0 { + queryOptions := &pluginsv0alpha1.PluginMetaQueryOptions{} + if val, ok := jsonData.QueryOptions["maxDataPoints"]; ok { + queryOptions.MaxDataPoints = &val + } + if val, ok := jsonData.QueryOptions["minInterval"]; ok { + queryOptions.MinInterval = &val + } + if val, ok := jsonData.QueryOptions["cacheTimeout"]; ok { + queryOptions.CacheTimeout = &val + } + meta.QueryOptions = queryOptions + } + + // Map Includes + if len(jsonData.Includes) > 0 { + meta.Includes = make([]pluginsv0alpha1.PluginMetaInclude, 0, len(jsonData.Includes)) + for _, include := range jsonData.Includes { + v0Include := pluginsv0alpha1.PluginMetaInclude{} + if include.UID != "" { + v0Include.Uid = &include.UID + } + if include.Type != "" { + var includeType pluginsv0alpha1.PluginMetaIncludeType + switch include.Type { + case "dashboard": + includeType = pluginsv0alpha1.PluginMetaIncludeTypeDashboard + case "page": + includeType = pluginsv0alpha1.PluginMetaIncludeTypePage + case "panel": + includeType = pluginsv0alpha1.PluginMetaIncludeTypePanel + case "datasource": + includeType = pluginsv0alpha1.PluginMetaIncludeTypeDatasource + } + v0Include.Type = &includeType + } + if include.Name != "" { + v0Include.Name = &include.Name + } + if include.Component != "" { + v0Include.Component = &include.Component + } + if include.Role != "" { + var role pluginsv0alpha1.PluginMetaIncludeRole + switch include.Role { + case "Admin": + role = pluginsv0alpha1.PluginMetaIncludeRoleAdmin + case "Editor": + role = pluginsv0alpha1.PluginMetaIncludeRoleEditor + case "Viewer": + role = pluginsv0alpha1.PluginMetaIncludeRoleViewer + } + v0Include.Role = &role + } + if include.Action != "" { + v0Include.Action = &include.Action + } + if include.Path != "" { + v0Include.Path = &include.Path + } + if include.AddToNav { + v0Include.AddToNav = &include.AddToNav + } + if include.DefaultNav { + v0Include.DefaultNav = &include.DefaultNav + } + if include.Icon != "" { + v0Include.Icon = &include.Icon + } + meta.Includes = append(meta.Includes, v0Include) + } + } + + // Map Routes + if len(jsonData.Routes) > 0 { + meta.Routes = make([]pluginsv0alpha1.PluginMetaRoute, 0, len(jsonData.Routes)) + for _, route := range jsonData.Routes { + v0Route := pluginsv0alpha1.PluginMetaRoute{} + if route.Path != "" { + v0Route.Path = &route.Path + } + if route.Method != "" { + v0Route.Method = &route.Method + } + if route.URL != "" { + v0Route.Url = &route.URL + } + if route.ReqRole != "" { + reqRole := string(route.ReqRole) + v0Route.ReqRole = &reqRole + } + if route.ReqAction != "" { + v0Route.ReqAction = &route.ReqAction + } + if len(route.Headers) > 0 { + headers := make([]string, 0, len(route.Headers)) + for _, header := range route.Headers { + headers = append(headers, header.Name+": "+header.Content) + } + v0Route.Headers = headers + } + if len(route.URLParams) > 0 { + v0Route.UrlParams = make([]pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams, 0, len(route.URLParams)) + for _, param := range route.URLParams { + v0Param := pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams{} + if param.Name != "" { + v0Param.Name = ¶m.Name + } + if param.Content != "" { + v0Param.Content = ¶m.Content + } + v0Route.UrlParams = append(v0Route.UrlParams, v0Param) + } + } + if route.TokenAuth != nil { + v0Route.TokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteTokenAuth{} + if route.TokenAuth.Url != "" { + v0Route.TokenAuth.Url = &route.TokenAuth.Url + } + if len(route.TokenAuth.Scopes) > 0 { + v0Route.TokenAuth.Scopes = route.TokenAuth.Scopes + } + if len(route.TokenAuth.Params) > 0 { + v0Route.TokenAuth.Params = make(map[string]interface{}) + for k, v := range route.TokenAuth.Params { + v0Route.TokenAuth.Params[k] = v + } + } + } + if route.JwtTokenAuth != nil { + v0Route.JwtTokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteJwtTokenAuth{} + if route.JwtTokenAuth.Url != "" { + v0Route.JwtTokenAuth.Url = &route.JwtTokenAuth.Url + } + if len(route.JwtTokenAuth.Scopes) > 0 { + v0Route.JwtTokenAuth.Scopes = route.JwtTokenAuth.Scopes + } + if len(route.JwtTokenAuth.Params) > 0 { + v0Route.JwtTokenAuth.Params = make(map[string]interface{}) + for k, v := range route.JwtTokenAuth.Params { + v0Route.JwtTokenAuth.Params[k] = v + } + } + } + if len(route.Body) > 0 { + var bodyMap map[string]interface{} + if err := json.Unmarshal(route.Body, &bodyMap); err == nil { + v0Route.Body = bodyMap + } + } + meta.Routes = append(meta.Routes, v0Route) + } + } + + // Map Extensions + if len(jsonData.Extensions.AddedLinks) > 0 || len(jsonData.Extensions.AddedComponents) > 0 || + len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 { + extensions := &pluginsv0alpha1.PluginMetaExtensions{} + + if len(jsonData.Extensions.AddedLinks) > 0 { + extensions.AddedLinks = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks)) + for _, link := range jsonData.Extensions.AddedLinks { + v0Link := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks{ + Targets: link.Targets, + Title: link.Title, + } + if link.Description != "" { + v0Link.Description = &link.Description + } + extensions.AddedLinks = append(extensions.AddedLinks, v0Link) + } + } + + if len(jsonData.Extensions.AddedComponents) > 0 { + extensions.AddedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents)) + for _, comp := range jsonData.Extensions.AddedComponents { + v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents{ + Targets: comp.Targets, + Title: comp.Title, + } + if comp.Description != "" { + v0Comp.Description = &comp.Description + } + extensions.AddedComponents = append(extensions.AddedComponents, v0Comp) + } + } + + if len(jsonData.Extensions.ExposedComponents) > 0 { + extensions.ExposedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents)) + for _, comp := range jsonData.Extensions.ExposedComponents { + v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents{ + Id: comp.Id, + } + if comp.Title != "" { + v0Comp.Title = &comp.Title + } + if comp.Description != "" { + v0Comp.Description = &comp.Description + } + extensions.ExposedComponents = append(extensions.ExposedComponents, v0Comp) + } + } + + if len(jsonData.Extensions.ExtensionPoints) > 0 { + extensions.ExtensionPoints = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints)) + for _, point := range jsonData.Extensions.ExtensionPoints { + v0Point := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints{ + Id: point.Id, + } + if point.Title != "" { + v0Point.Title = &point.Title + } + if point.Description != "" { + v0Point.Description = &point.Description + } + extensions.ExtensionPoints = append(extensions.ExtensionPoints, v0Point) + } + } + + meta.Extensions = extensions + } + + // Map Roles + if len(jsonData.Roles) > 0 { + meta.Roles = make([]pluginsv0alpha1.PluginMetaRole, 0, len(jsonData.Roles)) + for _, role := range jsonData.Roles { + v0Role := pluginsv0alpha1.PluginMetaRole{ + Grants: role.Grants, + } + if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 { + v0RoleRole := &pluginsv0alpha1.PluginMetaV0alpha1RoleRole{} + if role.Role.Name != "" { + v0RoleRole.Name = &role.Role.Name + } + if role.Role.Description != "" { + v0RoleRole.Description = &role.Role.Description + } + if len(role.Role.Permissions) > 0 { + v0RoleRole.Permissions = make([]pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions)) + for _, perm := range role.Role.Permissions { + v0Perm := pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions{} + if perm.Action != "" { + v0Perm.Action = &perm.Action + } + if perm.Scope != "" { + v0Perm.Scope = &perm.Scope + } + v0RoleRole.Permissions = append(v0RoleRole.Permissions, v0Perm) + } + } + v0Role.Role = v0RoleRole + } + meta.Roles = append(meta.Roles, v0Role) + } + } + + // Map IAM + if jsonData.IAM != nil && len(jsonData.IAM.Permissions) > 0 { + iam := &pluginsv0alpha1.PluginMetaIAM{ + Permissions: make([]pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)), + } + for _, perm := range jsonData.IAM.Permissions { + v0Perm := pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions{} + if perm.Action != "" { + v0Perm.Action = &perm.Action + } + if perm.Scope != "" { + v0Perm.Scope = &perm.Scope + } + iam.Permissions = append(iam.Permissions, v0Perm) + } + meta.Iam = iam + } + + return meta +} diff --git a/apps/plugins/pkg/app/meta/core_test.go b/apps/plugins/pkg/app/meta/core_test.go new file mode 100644 index 00000000000..a9d7103e4a2 --- /dev/null +++ b/apps/plugins/pkg/app/meta/core_test.go @@ -0,0 +1,302 @@ +package meta + +import ( + "context" + "errors" + "os" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" + "github.com/grafana/grafana/pkg/plugins" +) + +func TestCoreProvider_GetMeta(t *testing.T) { + ctx := context.Background() + + t.Run("returns cached plugin when available", func(t *testing.T) { + provider := NewCoreProvider() + + expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + + provider.mu.Lock() + provider.loadedPlugins["test-plugin"] = expectedMeta + provider.initialized = true + provider.mu.Unlock() + + result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, expectedMeta, result.Meta) + assert.Equal(t, defaultCoreTTL, result.TTL) + }) + + t.Run("returns ErrMetaNotFound for non-existent plugin", func(t *testing.T) { + provider := NewCoreProvider() + + provider.mu.Lock() + provider.initialized = true + provider.mu.Unlock() + + result, err := provider.GetMeta(ctx, "nonexistent-plugin", "1.0.0") + + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrMetaNotFound)) + assert.Nil(t, result) + }) + + t.Run("ignores version parameter", func(t *testing.T) { + provider := NewCoreProvider() + + expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + + provider.mu.Lock() + provider.loadedPlugins["test-plugin"] = expectedMeta + provider.initialized = true + provider.mu.Unlock() + + result1, err1 := provider.GetMeta(ctx, "test-plugin", "1.0.0") + result2, err2 := provider.GetMeta(ctx, "test-plugin", "2.0.0") + + require.NoError(t, err1) + require.NoError(t, err2) + assert.Equal(t, result1.Meta, result2.Meta) + }) + + t.Run("uses custom TTL when provided", func(t *testing.T) { + customTTL := 2 * time.Hour + provider := NewCoreProviderWithTTL(customTTL) + + expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + + provider.mu.Lock() + provider.loadedPlugins["test-plugin"] = expectedMeta + provider.initialized = true + provider.mu.Unlock() + + result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, customTTL, result.TTL) + }) + + t.Run("gracefully handles initialization failure and returns ErrMetaNotFound", func(t *testing.T) { + tempDir := t.TempDir() + + oldWd, err := os.Getwd() + require.NoError(t, err) + defer func() { + _ = os.Chdir(oldWd) + }() + + require.NoError(t, os.Chdir(tempDir)) + + provider := NewCoreProvider() + result, err := provider.GetMeta(ctx, "any-plugin", "1.0.0") + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrMetaNotFound)) + assert.Nil(t, result) + + initialized := provider.initialized + assert.True(t, initialized, "provider should be marked as initialized even after failure") + }) +} + +func TestCoreProvider_loadPlugins(t *testing.T) { + ctx := context.Background() + + t.Run("loads all core plugins", func(t *testing.T) { + _, filename, _, _ := runtime.Caller(0) + testDir := filepath.Dir(filename) + grafanaRoot := filepath.Join(testDir, "..", "..", "..", "..", "..") + grafanaRoot, err := filepath.Abs(grafanaRoot) + require.NoError(t, err) + + publicPath := filepath.Join(grafanaRoot, "public", "app", "plugins") + if _, err = os.Stat(publicPath); err != nil { + t.Skipf("Grafana root not found at %s, skipping integration test: %v", publicPath, err) + } + + require.NoError(t, os.Chdir(grafanaRoot)) + + provider := NewCoreProvider() + err = provider.loadPlugins(ctx) + require.NoError(t, err) + assert.Len(t, provider.loadedPlugins, 53) + }) + + t.Run("returns error when static root path not found", func(t *testing.T) { + tempDir := t.TempDir() + + oldWd, err := os.Getwd() + require.NoError(t, err) + defer func() { + _ = os.Chdir(oldWd) + }() + + require.NoError(t, os.Chdir(tempDir)) + + provider := NewCoreProvider() + err = provider.loadPlugins(ctx) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "could not find Grafana static root path") + }) + + t.Run("returns no error when no plugins found", func(t *testing.T) { + tempDir := t.TempDir() + publicPath := filepath.Join(tempDir, "public", "app", "plugins") + require.NoError(t, os.MkdirAll(publicPath, 0750)) + + oldWd, err := os.Getwd() + require.NoError(t, err) + defer func() { + _ = os.Chdir(oldWd) + }() + + require.NoError(t, os.Chdir(tempDir)) + + provider := NewCoreProvider() + err = provider.loadPlugins(ctx) + assert.NoError(t, err) + }) + + t.Run("successfully loads plugins when structure exists", func(t *testing.T) { + tempDir := t.TempDir() + publicPath := filepath.Join(tempDir, "public", "app", "plugins") + datasourcePath := filepath.Join(publicPath, "datasource") + panelPath := filepath.Join(publicPath, "panel") + + require.NoError(t, os.MkdirAll(datasourcePath, 0750)) + require.NoError(t, os.MkdirAll(panelPath, 0750)) + + pluginDir := filepath.Join(datasourcePath, "test-datasource") + require.NoError(t, os.MkdirAll(pluginDir, 0750)) + + pluginJSON := `{ + "id": "test-datasource", + "name": "Test Datasource", + "type": "datasource", + "info": { + "version": "1.0.0", + "description": "Test description" + } + }` + + require.NoError(t, os.WriteFile(filepath.Join(pluginDir, "plugin.json"), []byte(pluginJSON), 0644)) + + oldWd, err := os.Getwd() + require.NoError(t, err) + defer func() { + _ = os.Chdir(oldWd) + }() + + require.NoError(t, os.Chdir(tempDir)) + + provider := NewCoreProvider() + err = provider.loadPlugins(ctx) + + if err != nil { + require.NoError(t, err) + } + + provider.mu.RLock() + loaded := len(provider.loadedPlugins) > 0 + provider.mu.RUnlock() + + if loaded { + result, err := provider.GetMeta(ctx, "test-datasource", "1.0.0") + require.NoError(t, err) + assert.Equal(t, "test-datasource", result.Meta.Id) + assert.Equal(t, "Test Datasource", result.Meta.Name) + } + }) +} + +func TestNewCoreProvider(t *testing.T) { + t.Run("creates provider with default TTL", func(t *testing.T) { + provider := NewCoreProvider() + assert.Equal(t, defaultCoreTTL, provider.ttl) + assert.NotNil(t, provider.loadedPlugins) + assert.False(t, provider.initialized) + }) +} + +func TestNewCoreProviderWithTTL(t *testing.T) { + t.Run("creates provider with custom TTL", func(t *testing.T) { + customTTL := 2 * time.Hour + provider := NewCoreProviderWithTTL(customTTL) + assert.Equal(t, customTTL, provider.ttl) + }) + + t.Run("accepts zero TTL", func(t *testing.T) { + provider := NewCoreProviderWithTTL(0) + assert.Equal(t, time.Duration(0), provider.ttl) + }) +} + +func TestJsonDataToMeta(t *testing.T) { + t.Run("converts basic plugin JSON data", func(t *testing.T) { + jsonData := plugins.JSONData{ + ID: "test-plugin", + Name: "Test Plugin", + Type: plugins.TypeDataSource, + Info: plugins.Info{ + Version: "1.0.0", + Description: "Test description", + Keywords: []string{"test", "plugin"}, + Logos: plugins.Logos{ + Small: "small.png", + Large: "large.png", + }, + }, + } + + meta := jsonDataToPluginMetaJSONData(jsonData) + + assert.Equal(t, "test-plugin", meta.Id) + assert.Equal(t, "Test Plugin", meta.Name) + assert.Equal(t, pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, meta.Type) + assert.Equal(t, "1.0.0", meta.Info.Version) + assert.Equal(t, "Test description", *meta.Info.Description) + assert.Equal(t, []string{"test", "plugin"}, meta.Info.Keywords) + assert.Equal(t, "small.png", meta.Info.Logos.Small) + assert.Equal(t, "large.png", meta.Info.Logos.Large) + }) + + t.Run("handles optional fields", func(t *testing.T) { + jsonData := plugins.JSONData{ + ID: "test-plugin", + Name: "Test Plugin", + Type: plugins.TypePanel, + Info: plugins.Info{ + Version: "1.0.0", + }, + } + + meta := jsonDataToPluginMetaJSONData(jsonData) + + assert.Nil(t, meta.Info.Description) + assert.Nil(t, meta.Info.Author) + assert.Empty(t, meta.Info.Keywords) + }) +} diff --git a/apps/plugins/pkg/app/meta/manager.go b/apps/plugins/pkg/app/meta/manager.go new file mode 100644 index 00000000000..e694e88bba7 --- /dev/null +++ b/apps/plugins/pkg/app/meta/manager.go @@ -0,0 +1,131 @@ +package meta + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" +) + +const ( + defaultCleanupInterval = 10 * time.Minute +) + +// cachedMeta represents a cached metadata entry with expiration time +type cachedMeta struct { + meta pluginsv0alpha1.PluginMetaJSONData + ttl time.Duration + expiresAt time.Time +} + +// ProviderManager searches multiple providers for Plugin Meta in order until one succeeds, and caches +// results with per-provider TTLs. +// It implements app.Runnable to manage the cleanup goroutine lifecycle. +type ProviderManager struct { + providers []Provider + cache map[string]*cachedMeta + cacheMu sync.RWMutex +} + +// NewProviderManager creates a new ProviderManager that chains the given providers +// and caches results with per-provider TTLs. +func NewProviderManager(providers ...Provider) *ProviderManager { + if len(providers) == 0 { + panic("ProviderManager requires at least one provider") + } + + return &ProviderManager{ + providers: providers, + cache: make(map[string]*cachedMeta), + } +} + +// Run implements app.Runnable. It runs the cleanup loop until the context is cancelled. +// This method blocks until the context is cancelled (when the app shuts down). +func (pm *ProviderManager) Run(ctx context.Context) error { + ticker := time.NewTicker(defaultCleanupInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + pm.cleanupExpired() + } + } +} + +// GetMeta tries each provider in order until one succeeds, using cache when available. +// Returns ErrMetaNotFound only if all providers return ErrMetaNotFound. +// Otherwise, returns the last non-ErrMetaNotFound error if all providers fail. +func (pm *ProviderManager) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) { + cacheKey := pm.cacheKey(pluginID, version) + + // Check cache first + pm.cacheMu.RLock() + cached, exists := pm.cache[cacheKey] + pm.cacheMu.RUnlock() + + if exists && time.Now().Before(cached.expiresAt) { + return &Result{ + Meta: cached.meta, + TTL: cached.ttl, + }, nil + } + + // Try each provider in order until one succeeds + var lastErr error + for _, provider := range pm.providers { + result, err := provider.GetMeta(ctx, pluginID, version) + if err == nil { + // Don't cache results with a zero TTL + if result.TTL == 0 { + continue + } + + pm.cacheMu.Lock() + pm.cache[cacheKey] = &cachedMeta{ + meta: result.Meta, + ttl: result.TTL, + expiresAt: time.Now().Add(result.TTL), + } + pm.cacheMu.Unlock() + + return result, nil + } + + // If not found, try next provider + if errors.Is(err, ErrMetaNotFound) { + continue + } + + lastErr = err + } + + if lastErr != nil { + return nil, fmt.Errorf("failed to fetch plugin metadata from any provider: %w", lastErr) + } + + return nil, ErrMetaNotFound +} + +// cleanupExpired removes expired entries from the cache. +func (pm *ProviderManager) cleanupExpired() { + now := time.Now() + + pm.cacheMu.Lock() + defer pm.cacheMu.Unlock() + for key, entry := range pm.cache { + if now.After(entry.expiresAt) { + delete(pm.cache, key) + } + } +} + +func (pm *ProviderManager) cacheKey(pluginID, version string) string { + return fmt.Sprintf("%s:%s", pluginID, version) +} diff --git a/apps/plugins/pkg/app/meta/manager_test.go b/apps/plugins/pkg/app/meta/manager_test.go new file mode 100644 index 00000000000..3d3fe3c936c --- /dev/null +++ b/apps/plugins/pkg/app/meta/manager_test.go @@ -0,0 +1,434 @@ +package meta + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" +) + +func TestNewProviderManager(t *testing.T) { + t.Run("panics with no providers", func(t *testing.T) { + assert.Panics(t, func() { + NewProviderManager() + }) + }) + + t.Run("creates manager with providers", func(t *testing.T) { + provider1 := &mockProvider{} + provider2 := &mockProvider{} + + pm := NewProviderManager(provider1, provider2) + + require.NotNil(t, pm) + assert.Len(t, pm.providers, 2) + assert.NotNil(t, pm.cache) + }) +} + +func TestProviderManager_GetMeta(t *testing.T) { + ctx := context.Background() + + t.Run("returns cached result when available and not expired", func(t *testing.T) { + cachedMeta := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + + provider := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + return &Result{ + Meta: cachedMeta, + TTL: time.Hour, + }, nil + }, + } + + pm := NewProviderManager(provider) + + result1, err := pm.GetMeta(ctx, "test-plugin", "1.0.0") + require.NoError(t, err) + require.NotNil(t, result1) + assert.Equal(t, cachedMeta, result1.Meta) + assert.Equal(t, time.Hour, result1.TTL) + + provider.getMetaFunc = func(ctx context.Context, pluginID, version string) (*Result, error) { + return &Result{ + Meta: pluginsv0alpha1.PluginMetaJSONData{Id: "different"}, + TTL: time.Hour, + }, nil + } + + result2, err := pm.GetMeta(ctx, "test-plugin", "1.0.0") + require.NoError(t, err) + require.NotNil(t, result2) + assert.Equal(t, cachedMeta, result2.Meta) + assert.Equal(t, time.Hour, result2.TTL) + }) + + t.Run("fetches from provider when not cached", func(t *testing.T) { + expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + expectedTTL := 2 * time.Hour + + provider := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + return &Result{ + Meta: expectedMeta, + TTL: expectedTTL, + }, nil + }, + } + + pm := NewProviderManager(provider) + + result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0") + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, expectedMeta, result.Meta) + assert.Equal(t, expectedTTL, result.TTL) + + pm.cacheMu.RLock() + cached, exists := pm.cache["test-plugin:1.0.0"] + pm.cacheMu.RUnlock() + + assert.True(t, exists) + assert.Equal(t, expectedMeta, cached.meta) + assert.Equal(t, expectedTTL, cached.ttl) + }) + + t.Run("does not cache result with zero TTL and tries next provider", func(t *testing.T) { + zeroTTLMeta := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Zero TTL Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + + provider1 := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + return &Result{ + Meta: zeroTTLMeta, + TTL: 0, + }, nil + }, + } + provider2 := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + return &Result{ + Meta: expectedMeta, + TTL: time.Hour, + }, nil + }, + } + + pm := NewProviderManager(provider1, provider2) + + result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0") + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, expectedMeta, result.Meta) + + pm.cacheMu.RLock() + cached, exists := pm.cache["test-plugin:1.0.0"] + pm.cacheMu.RUnlock() + + assert.True(t, exists) + assert.Equal(t, expectedMeta, cached.meta) + assert.Equal(t, time.Hour, cached.ttl) + }) + + t.Run("tries next provider when first returns ErrMetaNotFound", func(t *testing.T) { + expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + + provider1 := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + return nil, ErrMetaNotFound + }, + } + provider2 := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + return &Result{ + Meta: expectedMeta, + TTL: time.Hour, + }, nil + }, + } + + pm := NewProviderManager(provider1, provider2) + + result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0") + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, expectedMeta, result.Meta) + }) + + t.Run("returns ErrMetaNotFound when all providers return ErrMetaNotFound", func(t *testing.T) { + provider1 := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + return nil, ErrMetaNotFound + }, + } + provider2 := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + return nil, ErrMetaNotFound + }, + } + + pm := NewProviderManager(provider1, provider2) + + result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0") + + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrMetaNotFound)) + assert.Nil(t, result) + }) + + t.Run("returns error when provider returns non-ErrMetaNotFound error", func(t *testing.T) { + expectedErr := errors.New("network error") + + provider1 := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + return nil, expectedErr + }, + } + provider2 := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + return nil, ErrMetaNotFound + }, + } + + pm := NewProviderManager(provider1, provider2) + + result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to fetch plugin metadata from any provider") + assert.ErrorIs(t, err, expectedErr) + assert.Nil(t, result) + }) + + t.Run("skips expired cache entries", func(t *testing.T) { + expiredMeta := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Expired Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + + callCount := 0 + provider := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + callCount++ + if callCount == 1 { + return &Result{ + Meta: expiredMeta, + TTL: time.Nanosecond, + }, nil + } + return &Result{ + Meta: expectedMeta, + TTL: time.Hour, + }, nil + }, + } + + pm := NewProviderManager(provider) + + result1, err := pm.GetMeta(ctx, "test-plugin", "1.0.0") + require.NoError(t, err) + assert.Equal(t, expiredMeta, result1.Meta) + + time.Sleep(2 * time.Nanosecond) + + result2, err := pm.GetMeta(ctx, "test-plugin", "1.0.0") + require.NoError(t, err) + assert.Equal(t, expectedMeta, result2.Meta) + assert.Equal(t, 2, callCount) + }) + + t.Run("uses first successful provider", func(t *testing.T) { + expectedMeta1 := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Provider 1 Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + expectedMeta2 := pluginsv0alpha1.PluginMetaJSONData{ + Id: "test-plugin", + Name: "Provider 2 Plugin", + Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + } + + provider1 := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + return &Result{ + Meta: expectedMeta1, + TTL: time.Hour, + }, nil + }, + } + provider2 := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + return &Result{ + Meta: expectedMeta2, + TTL: time.Hour, + }, nil + }, + } + + pm := NewProviderManager(provider1, provider2) + + result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0") + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, expectedMeta1, result.Meta) + }) +} + +func TestProviderManager_Run(t *testing.T) { + t.Run("runs cleanup loop until context cancelled", func(t *testing.T) { + pm := NewProviderManager(&mockProvider{}) + + ctx, cancel := context.WithCancel(context.Background()) + + done := make(chan error, 1) + go func() { + done <- pm.Run(ctx) + }() + + time.Sleep(10 * time.Millisecond) + cancel() + + err := <-done + require.NoError(t, err) + }) +} + +func TestProviderManager_cleanupExpired(t *testing.T) { + t.Run("removes expired entries", func(t *testing.T) { + validMeta := pluginsv0alpha1.PluginMetaJSONData{Id: "valid"} + expiredMeta1 := pluginsv0alpha1.PluginMetaJSONData{Id: "expired1"} + expiredMeta2 := pluginsv0alpha1.PluginMetaJSONData{Id: "expired2"} + + provider := &mockProvider{ + getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { + switch pluginID { + case "valid": + return &Result{Meta: validMeta, TTL: time.Hour}, nil + case "expired1": + return &Result{Meta: expiredMeta1, TTL: time.Nanosecond}, nil + case "expired2": + return &Result{Meta: expiredMeta2, TTL: time.Nanosecond}, nil + } + return nil, ErrMetaNotFound + }, + } + + pm := NewProviderManager(provider) + ctx := context.Background() + + _, err := pm.GetMeta(ctx, "expired1", "1.0.0") + require.NoError(t, err) + _, err = pm.GetMeta(ctx, "expired2", "1.0.0") + require.NoError(t, err) + _, err = pm.GetMeta(ctx, "valid", "1.0.0") + require.NoError(t, err) + + time.Sleep(2 * time.Nanosecond) + + pm.cleanupExpired() + + provider.getMetaFunc = func(ctx context.Context, pluginID, version string) (*Result, error) { + if pluginID == "valid" { + return &Result{Meta: validMeta, TTL: time.Hour}, nil + } + return nil, ErrMetaNotFound + } + + result, err := pm.GetMeta(ctx, "expired1", "1.0.0") + assert.Error(t, err) + assert.Nil(t, result) + + result, err = pm.GetMeta(ctx, "valid", "1.0.0") + require.NoError(t, err) + assert.Equal(t, validMeta, result.Meta) + }) + + t.Run("handles empty cache", func(t *testing.T) { + pm := NewProviderManager(&mockProvider{}) + pm.cleanupExpired() + }) +} + +func TestProviderManager_cacheKey(t *testing.T) { + pm := NewProviderManager(&mockProvider{}) + + tests := []struct { + name string + pluginID string + version string + expected string + }{ + { + name: "basic key", + pluginID: "test-plugin", + version: "1.0.0", + expected: "test-plugin:1.0.0", + }, + { + name: "empty version", + pluginID: "test-plugin", + version: "", + expected: "test-plugin:", + }, + { + name: "empty plugin ID", + pluginID: "", + version: "1.0.0", + expected: ":1.0.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key := pm.cacheKey(tt.pluginID, tt.version) + assert.Equal(t, tt.expected, key) + }) + } +} + +type mockProvider struct { + getMetaFunc func(ctx context.Context, pluginID, version string) (*Result, error) +} + +func (m *mockProvider) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) { + if m.getMetaFunc != nil { + return m.getMetaFunc(ctx, pluginID, version) + } + return nil, ErrMetaNotFound +} diff --git a/apps/plugins/pkg/app/meta/provider.go b/apps/plugins/pkg/app/meta/provider.go new file mode 100644 index 00000000000..8b5d8b0fcbd --- /dev/null +++ b/apps/plugins/pkg/app/meta/provider.go @@ -0,0 +1,27 @@ +package meta + +import ( + "context" + "errors" + "time" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" +) + +var ( + ErrMetaNotFound = errors.New("not found") +) + +// Result contains plugin metadata along with its recommended TTL. +type Result struct { + Meta pluginsv0alpha1.PluginMetaJSONData + TTL time.Duration +} + +// Provider is used for retrieving plugin metadata. +type Provider interface { + // GetMeta retrieves plugin metadata for the given plugin ID and version. + // Returns the Result containing the PluginMetaJSONData and its recommended TTL. + // If the plugin is not found, returns ErrMetaNotFound. + GetMeta(ctx context.Context, pluginID, version string) (*Result, error) +} diff --git a/apps/plugins/pkg/app/storage.go b/apps/plugins/pkg/app/storage.go new file mode 100644 index 00000000000..bd277978a91 --- /dev/null +++ b/apps/plugins/pkg/app/storage.go @@ -0,0 +1,200 @@ +package app + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/resource" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/rest" + + claims "github.com/grafana/authlib/types" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" + "github.com/grafana/grafana/apps/plugins/pkg/app/meta" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" +) + +var ( + _ rest.Scoper = (*PluginMetaStorage)(nil) + _ rest.SingularNameProvider = (*PluginMetaStorage)(nil) + _ rest.Getter = (*PluginMetaStorage)(nil) + _ rest.Lister = (*PluginMetaStorage)(nil) + _ rest.Storage = (*PluginMetaStorage)(nil) + _ rest.TableConvertor = (*PluginMetaStorage)(nil) +) + +type PluginMetaStorage struct { + metaManager *meta.ProviderManager + client *pluginsv0alpha1.PluginClient + clientFactory func(context.Context) (*pluginsv0alpha1.PluginClient, error) + clientErr error + clientOnce sync.Once + + gr schema.GroupResource + namespacer claims.NamespaceFormatter + tableConverter rest.TableConvertor +} + +func NewPluginMetaStorage( + metaManager *meta.ProviderManager, + clientFactory func(context.Context) (*pluginsv0alpha1.PluginClient, error), + namespacer claims.NamespaceFormatter, +) *PluginMetaStorage { + gr := schema.GroupResource{ + Group: pluginsv0alpha1.APIGroup, + Resource: strings.ToLower(pluginsv0alpha1.PluginMetaKind().Plural()), + } + + return &PluginMetaStorage{ + metaManager: metaManager, + clientFactory: clientFactory, + gr: gr, + namespacer: namespacer, + tableConverter: rest.NewDefaultTableConvertor(gr), + } +} + +func (s *PluginMetaStorage) getClient(ctx context.Context) (*pluginsv0alpha1.PluginClient, error) { + s.clientOnce.Do(func() { + client, err := s.clientFactory(ctx) + if err != nil { + s.clientErr = err + s.client = nil + return + } + s.client = client + }) + + return s.client, s.clientErr +} + +func (s *PluginMetaStorage) New() runtime.Object { + return pluginsv0alpha1.PluginMetaKind().ZeroValue() +} + +func (s *PluginMetaStorage) Destroy() {} + +func (s *PluginMetaStorage) NamespaceScoped() bool { + return true +} + +func (s *PluginMetaStorage) GetSingularName() string { + return strings.ToLower(pluginsv0alpha1.PluginMetaKind().Kind()) +} + +func (s *PluginMetaStorage) NewList() runtime.Object { + return pluginsv0alpha1.PluginMetaKind().ZeroListValue() +} + +func (s *PluginMetaStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.tableConverter.ConvertToTable(ctx, object, tableOptions) +} + +func (s *PluginMetaStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + pluginClient, err := s.getClient(ctx) + if err != nil { + return nil, apierrors.NewInternalError(fmt.Errorf("failed to get plugin client: %w", err)) + } + + plugins, err := pluginClient.ListAll(ctx, ns.Value, resource.ListOptions{}) + if err != nil { + logging.DefaultLogger.Error("Failed to list plugins", "namespace", ns.Value, "error", err) + return nil, apierrors.NewInternalError(fmt.Errorf("failed to list plugins: %w", err)) + } + + // Convert each Plugin to PluginMeta + metaItems := make([]pluginsv0alpha1.PluginMeta, 0, len(plugins.Items)) + for _, plugin := range plugins.Items { + result, err := s.metaManager.GetMeta(ctx, plugin.Spec.Id, plugin.Spec.Version) + if err != nil { + // Log error but continue with other plugins + logging.DefaultLogger.Warn("Failed to fetch metadata for plugin", "pluginId", plugin.Spec.Id, "version", plugin.Spec.Version, "error", err) + continue + } + + pluginMeta := createPluginMetaFromPluginMetaJSONData(result.Meta, plugin.Name, plugin.Namespace) + metaItems = append(metaItems, *pluginMeta) + } + + list := &pluginsv0alpha1.PluginMetaList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: pluginsv0alpha1.APIGroup + "/" + pluginsv0alpha1.APIVersion, + Kind: pluginsv0alpha1.PluginMetaKind().Kind() + "List", + }, + Items: metaItems, + } + + return list, nil +} + +func (s *PluginMetaStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + pluginClient, err := s.getClient(ctx) + if err != nil { + return nil, apierrors.NewInternalError(fmt.Errorf("failed to get plugin client: %w", err)) + } + + plugin, err := pluginClient.Get(ctx, resource.Identifier{ + Namespace: ns.Value, + Name: name, + }) + if err != nil { + return nil, err + } + + result, err := s.metaManager.GetMeta(ctx, plugin.Spec.Id, plugin.Spec.Version) + if err != nil { + if errors.Is(err, meta.ErrMetaNotFound) { + gr := schema.GroupResource{ + Group: pluginsv0alpha1.APIGroup, + Resource: name, + } + return nil, apierrors.NewNotFound(gr, plugin.Spec.Id) + } + + logging.DefaultLogger.Error("Failed to fetch plugin metadata", "pluginId", plugin.Spec.Id, "version", plugin.Spec.Version, "error", err) + return nil, apierrors.NewInternalError(fmt.Errorf("failed to fetch plugin metadata: %w", err)) + } + + return createPluginMetaFromPluginMetaJSONData(result.Meta, name, ns.Value), nil +} + +// createPluginMetaFromPluginMetaJSONData creates a PluginMeta k8s object from PluginMetaJSONData and plugin metadata. +func createPluginMetaFromPluginMetaJSONData(pluginJSON pluginsv0alpha1.PluginMetaJSONData, name, namespace string) *pluginsv0alpha1.PluginMeta { + pluginMeta := &pluginsv0alpha1.PluginMeta{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: pluginsv0alpha1.PluginMetaSpec{ + PluginJSON: pluginJSON, + }, + } + + // Set the GroupVersionKind + pluginMeta.SetGroupVersionKind(schema.GroupVersionKind{ + Group: pluginsv0alpha1.APIGroup, + Version: pluginsv0alpha1.APIVersion, + Kind: pluginsv0alpha1.PluginMetaKind().Kind(), + }) + + return pluginMeta +} diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index e546d23b95b..c1ba032196c 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -238,6 +238,7 @@ type ReleaseState string const ( ReleaseStateAlpha ReleaseState = "alpha" + ReleaseStateBeta ReleaseState = "beta" ) type SignatureType string diff --git a/pkg/registry/apps/apps.go b/pkg/registry/apps/apps.go index 32afa56be46..983605dedbc 100644 --- a/pkg/registry/apps/apps.go +++ b/pkg/registry/apps/apps.go @@ -33,7 +33,7 @@ import ( func ProvideAppInstallers( features featuremgmt.FeatureToggles, playlistAppInstaller *playlist.PlaylistAppInstaller, - pluginsApplInstaller *plugins.PluginsAppInstaller, + pluginsApplInstaller *plugins.AppInstaller, shorturlAppInstaller *shorturl.ShortURLAppInstaller, rulesAppInstaller *rules.AlertingRulesAppInstaller, correlationsAppInstaller *correlations.AppInstaller, diff --git a/pkg/registry/apps/apps_test.go b/pkg/registry/apps/apps_test.go index 5f46fd349be..b7f6c2565e6 100644 --- a/pkg/registry/apps/apps_test.go +++ b/pkg/registry/apps/apps_test.go @@ -18,7 +18,7 @@ import ( func TestProvideAppInstallers_Table(t *testing.T) { playlistInstaller := &playlist.PlaylistAppInstaller{} - pluginsInstaller := &plugins.PluginsAppInstaller{} + pluginsInstaller := &plugins.AppInstaller{} rulesInstaller := &rules.AlertingRulesAppInstaller{} correlationsAppInstaller := &correlations.AppInstaller{} notificationsAppInstaller := ¬ifications.AlertingNotificationsAppInstaller{} diff --git a/pkg/registry/apps/plugins/register.go b/pkg/registry/apps/plugins/register.go index e95b120cd66..bcfb6561ee8 100644 --- a/pkg/registry/apps/plugins/register.go +++ b/pkg/registry/apps/plugins/register.go @@ -1,34 +1,59 @@ package plugins import ( + "context" + "fmt" + "os" + + "github.com/grafana/grafana-app-sdk/k8s" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/registry/rest" restclient "k8s.io/client-go/rest" "github.com/grafana/grafana-app-sdk/app" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" "github.com/grafana/grafana-app-sdk/simple" + pluginsappapis "github.com/grafana/grafana/apps/plugins/pkg/apis" + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app" + "github.com/grafana/grafana/apps/plugins/pkg/app/meta" + "github.com/grafana/grafana/pkg/configprovider" + "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" ) var ( - _ appsdkapiserver.AppInstaller = (*PluginsAppInstaller)(nil) - _ appinstaller.AuthorizerProvider = (*PluginsAppInstaller)(nil) + _ appsdkapiserver.AppInstaller = (*AppInstaller)(nil) + _ appinstaller.AuthorizerProvider = (*AppInstaller)(nil) ) -type PluginsAppInstaller struct { +type AppInstaller struct { + metaManager *meta.ProviderManager + cfgProvider configprovider.ConfigProvider + restConfigProvider apiserver.RestConfigProvider + appsdkapiserver.AppInstaller } func RegisterAppInstaller( - cfg *setting.Cfg, - features featuremgmt.FeatureToggles, -) (*PluginsAppInstaller, error) { - installer := &PluginsAppInstaller{} - specificConfig := any(nil) + cfgProvider configprovider.ConfigProvider, + restConfigProvider apiserver.RestConfigProvider, +) (*AppInstaller, error) { + grafanaComAPIURL := os.Getenv("GRAFANA_COM_API_URL") + if grafanaComAPIURL == "" { + grafanaComAPIURL = "https://grafana.com/api/plugins" + } + + coreProvider := meta.NewCoreProvider() + cloudProvider := meta.NewCloudProvider(grafanaComAPIURL) + metaProviderManager := meta.NewProviderManager(coreProvider, cloudProvider) + specificConfig := &pluginsapp.PluginAppConfig{ + MetaProviderManager: metaProviderManager, + } provider := simple.NewAppProvider(pluginsappapis.LocalManifest(), specificConfig, pluginsapp.New) appConfig := app.Config{ KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method @@ -39,11 +64,54 @@ func RegisterAppInstaller( if err != nil { return nil, err } - installer.AppInstaller = i - return installer, nil + + return &AppInstaller{ + metaManager: metaProviderManager, + cfgProvider: cfgProvider, + restConfigProvider: restConfigProvider, + AppInstaller: i, + }, nil +} + +func (p *AppInstaller) InstallAPIs( + server appsdkapiserver.GenericAPIServer, + restOptsGetter generic.RESTOptionsGetter, +) error { + ctx := context.Background() + cfg, err := p.cfgProvider.Get(ctx) + if err != nil { + return err + } + + // Create a client factory function that will be called lazily when the client is needed. + // This avoids deadlock issues since the restConfigProvider/API server will not be ready during API installation. + clientFactory := func(ctx context.Context) (*pluginsv0alpha1.PluginClient, error) { + kubeConfig, err := p.restConfigProvider.GetRestConfig(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get rest config: %w", err) + } + + clientGenerator := k8s.NewClientRegistry(*kubeConfig, k8s.DefaultClientConfig()) + client, err := pluginsv0alpha1.NewPluginClientFromGenerator(clientGenerator) + if err != nil { + return nil, fmt.Errorf("failed to create plugin client: %w", err) + } + + return client, nil + } + + pluginMetaGVR := pluginsv0alpha1.PluginMetaKind().GroupVersionResource() + replacedStorage := map[schema.GroupVersionResource]rest.Storage{ + pluginMetaGVR: pluginsapp.NewPluginMetaStorage(p.metaManager, clientFactory, request.GetNamespaceMapper(cfg)), + } + wrappedServer := &customStorageWrapper{ + wrapped: server, + replace: replacedStorage, + } + return p.AppInstaller.InstallAPIs(wrappedServer, restOptsGetter) } // GetAuthorizer returns the authorizer for the plugins app. -func (p *PluginsAppInstaller) GetAuthorizer() authorizer.Authorizer { +func (p *AppInstaller) GetAuthorizer() authorizer.Authorizer { return pluginsapp.GetAuthorizer() } diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index eb448ba41a8..4dc7523cbb6 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -786,7 +786,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - pluginsAppInstaller, err := plugins.RegisterAppInstaller(cfg, featureToggles) + appInstaller, err := plugins.RegisterAppInstaller(configProvider, eventualRestConfigProvider) if err != nil { return nil, err } @@ -798,7 +798,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - appInstaller, err := correlations2.RegisterAppInstaller(cfg, featureToggles, correlationsService) + correlationsAppInstaller, err := correlations2.RegisterAppInstaller(cfg, featureToggles, correlationsService) if err != nil { return nil, err } @@ -823,7 +823,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller) + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller) builderMetrics := builder.ProvideBuilderMetrics(registerer) apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics) if err != nil { @@ -1435,7 +1435,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - pluginsAppInstaller, err := plugins.RegisterAppInstaller(cfg, featureToggles) + appInstaller, err := plugins.RegisterAppInstaller(configProvider, eventualRestConfigProvider) if err != nil { return nil, err } @@ -1447,7 +1447,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - appInstaller, err := correlations2.RegisterAppInstaller(cfg, featureToggles, correlationsService) + correlationsAppInstaller, err := correlations2.RegisterAppInstaller(cfg, featureToggles, correlationsService) if err != nil { return nil, err } @@ -1472,7 +1472,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller) + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller) builderMetrics := builder.ProvideBuilderMetrics(registerer) apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics) if err != nil { diff --git a/pkg/services/pluginsintegration/plugininstaller/service_test.go b/pkg/services/pluginsintegration/plugininstaller/service_test.go index f0d34cfc660..d2d525a3c82 100644 --- a/pkg/services/pluginsintegration/plugininstaller/service_test.go +++ b/pkg/services/pluginsintegration/plugininstaller/service_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" @@ -208,7 +209,9 @@ func TestService_Run(t *testing.T) { t.Cleanup(func() { s.StopAsync() - err := s.AwaitTerminated(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + err := s.AwaitTerminated(ctx) if tt.shouldThrowError { require.ErrorContains(t, err, "Failed to install plugin") return @@ -225,6 +228,8 @@ func TestService_Run(t *testing.T) { } require.NoError(t, err) + <-s.installComplete + if tt.shouldInstall { expectedInstalled := 0 expectedInstalledFromURL := 0 @@ -246,7 +251,6 @@ func TestService_Run(t *testing.T) { expectedInstalled++ } } - <-s.installComplete require.Equal(t, expectedInstalled, installed) require.Equal(t, expectedInstalledFromURL, installedFromURL) } diff --git a/pkg/tests/apis/plugins/discovery_test.go b/pkg/tests/apis/plugins/discovery_test.go index 168aa259dad..8f0d0cc5ef5 100644 --- a/pkg/tests/apis/plugins/discovery_test.go +++ b/pkg/tests/apis/plugins/discovery_test.go @@ -19,6 +19,35 @@ func TestIntegrationPluginsIntegrationDiscovery(t *testing.T) { "version": "v0alpha1", "freshness": "Current", "resources": [ + { + "resource": "pluginmetas", + "responseKind": { + "group": "", + "kind": "PluginMeta", + "version": "" + }, + "scope": "Namespaced", + "singularResource": "pluginmeta", + "subresources": [ + { + "responseKind": { + "group": "", + "kind": "PluginMeta", + "version": "" + }, + "subresource": "status", + "verbs": [ + "get", + "patch", + "update" + ] + } + ], + "verbs": [ + "get", + "list" + ] + }, { "resource": "plugins", "responseKind": { @@ -29,17 +58,6 @@ func TestIntegrationPluginsIntegrationDiscovery(t *testing.T) { "scope": "Namespaced", "singularResource": "plugins", "subresources": [ - { - "responseKind": { - "group": "", - "kind": "ResourceCallOptions", - "version": "" - }, - "subresource": "meta", - "verbs": [ - "get" - ] - }, { "responseKind": { "group": "", diff --git a/pkg/tests/apis/plugins/pluginmeta_test.go b/pkg/tests/apis/plugins/pluginmeta_test.go new file mode 100644 index 00000000000..af70f02a6f8 --- /dev/null +++ b/pkg/tests/apis/plugins/pluginmeta_test.go @@ -0,0 +1,95 @@ +package plugins + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationPluginMeta(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + t.Run("get plugin meta", func(t *testing.T) { + helper := setupHelper(t) + ctx := context.Background() + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvrPlugins, + }) + + pluginName := "test-plugin-meta" + plugin := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + "apiVersion": "plugins.grafana.app/v0alpha1", + "kind": "Plugin", + "metadata": {"name": "%s"}, + "spec": {"id": "grafana-piechart-panel", "version": "1.0.0"} + }`, pluginName)) + _, err := client.Resource.Create(ctx, plugin, metav1.CreateOptions{}) + require.NoError(t, err) + + namespace := helper.Org1.Admin.Identity.GetNamespace() + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas/%s", namespace, pluginName) + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &pluginsv0alpha1.PluginMeta{}) + + require.NotNil(t, response.Result) + require.NotNil(t, response.Result.Spec.PluginJSON) + require.Equal(t, "grafana-piechart-panel", response.Result.Spec.PluginJSON.Id) + require.NotEmpty(t, response.Result.Spec.PluginJSON.Name) + require.NotEmpty(t, response.Result.Spec.PluginJSON.Type) + }) + + t.Run("get plugin meta for non-existent plugin", func(t *testing.T) { + helper := setupHelper(t) + namespace := helper.Org1.Admin.Identity.GetNamespace() + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas/non-existent-plugin", namespace) + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &pluginsv0alpha1.PluginMeta{}) + + require.NotNil(t, response.Status) + require.Equal(t, int32(404), response.Status.Code) + }) + + t.Run("get plugin meta for plugin with non-existent metadata", func(t *testing.T) { + helper := setupHelper(t) + ctx := context.Background() + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvrPlugins, + }) + + pluginName := "test-plugin-meta-not-found" + plugin := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + "apiVersion": "plugins.grafana.app/v0alpha1", + "kind": "Plugin", + "metadata": {"name": "%s"}, + "spec": {"id": "non-existent-plugin-id", "version": "1.0.0"} + }`, pluginName)) + _, err := client.Resource.Create(ctx, plugin, metav1.CreateOptions{}) + require.NoError(t, err) + + namespace := helper.Org1.Admin.Identity.GetNamespace() + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas/%s", namespace, pluginName) + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &pluginsv0alpha1.PluginMeta{}) + + require.NotNil(t, response.Status) + require.Equal(t, int32(404), response.Status.Code) + }) +} diff --git a/pkg/tests/apis/plugins/pluginmetas_test.go b/pkg/tests/apis/plugins/pluginmetas_test.go new file mode 100644 index 00000000000..1ffe8d35a50 --- /dev/null +++ b/pkg/tests/apis/plugins/pluginmetas_test.go @@ -0,0 +1,85 @@ +package plugins + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationPluginMetas(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + t.Run("list plugin metas", func(t *testing.T) { + helper := setupHelper(t) + ctx := context.Background() + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvrPlugins, + }) + + plugin1Name := "test-plugin-metas-1" + plugin1 := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + "apiVersion": "plugins.grafana.app/v0alpha1", + "kind": "Plugin", + "metadata": {"name": "%s"}, + "spec": {"id": "grafana-piechart-panel", "version": "1.0.0"} + }`, plugin1Name)) + _, err := client.Resource.Create(ctx, plugin1, metav1.CreateOptions{}) + require.NoError(t, err) + + plugin2Name := "test-plugin-metas-2" + plugin2 := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + "apiVersion": "plugins.grafana.app/v0alpha1", + "kind": "Plugin", + "metadata": {"name": "%s"}, + "spec": {"id": "grafana-clock-panel", "version": "1.0.0"} + }`, plugin2Name)) + _, err = client.Resource.Create(ctx, plugin2, metav1.CreateOptions{}) + require.NoError(t, err) + + namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()) + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas", namespace) + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &pluginsv0alpha1.PluginMetaList{}) + + require.NotNil(t, response.Result) + require.NotNil(t, response.Result.Items) + require.GreaterOrEqual(t, len(response.Result.Items), 2) + + foundIDs := make(map[string]bool) + for _, item := range response.Result.Items { + require.NotNil(t, item.Spec.PluginJSON) + foundIDs[item.Spec.PluginJSON.Id] = true + require.NotEmpty(t, item.Spec.PluginJSON.Id) + require.NotEmpty(t, item.Spec.PluginJSON.Type) + require.NotEmpty(t, item.Spec.PluginJSON.Name) + } + require.True(t, foundIDs["grafana-piechart-panel"]) + require.True(t, foundIDs["grafana-clock-panel"]) + }) + + t.Run("list plugin metas with no plugins", func(t *testing.T) { + helper := setupHelper(t) + namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()) + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas", namespace) + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &pluginsv0alpha1.PluginMetaList{}) + + require.NotNil(t, response.Result) + require.NotNil(t, response.Result.Items) + require.GreaterOrEqual(t, len(response.Result.Items), 0) + }) +} From 0c2707bbc41b60124d2217547f607e08a2a3f013 Mon Sep 17 00:00:00 2001 From: Charandas <542168+charandas@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:09:49 -0800 Subject: [PATCH 085/378] K8s: read resource configs from API Enablement for API Builders (#114329) --- pkg/registry/apis/ofrep/register.go | 4 - pkg/services/apiserver/builder/helper.go | 106 +++++++++++++----- pkg/services/apiserver/builder/openapi.go | 19 +++- .../apiserver/builder/request_handler.go | 10 +- pkg/services/apiserver/service.go | 8 +- 5 files changed, 111 insertions(+), 36 deletions(-) diff --git a/pkg/registry/apis/ofrep/register.go b/pkg/registry/apis/ofrep/register.go index cd6f8f0eb29..d6a443cfee7 100644 --- a/pkg/registry/apis/ofrep/register.go +++ b/pkg/registry/apis/ofrep/register.go @@ -64,10 +64,6 @@ func NewAPIBuilder(providerType string, url *url.URL, insecure bool, caFile stri } func RegisterAPIService(apiregistration builder.APIRegistrar, cfg *setting.Cfg) (*APIBuilder, error) { - if !cfg.OpenFeature.APIEnabled { - return nil, nil - } - var staticEvaluator featuremgmt.StaticFlagEvaluator // No static evaluator needed for non-static provider var err error if cfg.OpenFeature.ProviderType == setting.StaticProviderType { diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index 603c49f3cb2..ae90295c01d 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -22,6 +22,7 @@ import ( k8srequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/generic" genericapiserver "k8s.io/apiserver/pkg/server" + serverstorage "k8s.io/apiserver/pkg/server/storage" "k8s.io/apiserver/pkg/util/openapi" k8sscheme "k8s.io/client-go/kubernetes/scheme" k8stracing "k8s.io/component-base/tracing" @@ -72,13 +73,36 @@ var PathRewriters = []filters.PathRewriter{ }, } +// GetDefaultBuildHandlerChainFuncForAggregator is a replica of GetDefaultBuildHandlerChainFunc except it skips custom routes handling +func GetDefaultBuildHandlerChainFuncForAggregator() BuildHandlerChainFunc { + return func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler { + // filters.WithRequester needs to be after the K8s chain because it depends on the K8s user in context + handler := filters.WithRequester(delegateHandler) + + // Call DefaultBuildHandlerChain on the main entrypoint http.Handler + // See https://github.com/kubernetes/apiserver/blob/v0.28.0/pkg/server/config.go#L906 + // DefaultBuildHandlerChain provides many things, notably CORS, HSTS, cache-control, authz and latency tracking + handler = genericapiserver.DefaultBuildHandlerChain(handler, c) + + handler = filters.WithAcceptHeader(handler) + handler = filters.WithPathRewriters(handler, PathRewriters) + handler = k8stracing.WithTracing(handler, c.TracerProvider, "KubernetesAPI") + handler = filters.WithExtractJaegerTrace(handler) + // Configure filters.WithPanicRecovery to not crash on panic + utilruntime.ReallyCrash = false + + return handler + } +} func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus.Registerer) BuildHandlerChainFunc { return func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler { requestHandler, err := GetCustomRoutesHandler( delegateHandler, c.LoopbackClientConfig, builders, - reg) + reg, + c.MergedResourceConfig, + ) if err != nil { panic(fmt.Sprintf("could not build the request handler for specified API builders: %s", err.Error())) } @@ -105,6 +129,8 @@ func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus. } } +// SetupConfig sets up the server config for the API server +// specify isAggregator=true, if the chain is being constructed for kube-aggregator func SetupConfig( scheme *runtime.Scheme, serverConfig *genericapiserver.RecommendedConfig, @@ -114,6 +140,7 @@ func SetupConfig( gvs []schema.GroupVersion, additionalOpenAPIDefGetters []common.GetOpenAPIDefinitions, reg prometheus.Registerer, + apiResourceConfig *serverstorage.ResourceConfig, ) error { serverConfig.AdmissionControl = NewAdmissionFromBuilders(builders) defsGetter := GetOpenAPIDefinitions(builders, additionalOpenAPIDefGetters...) @@ -126,7 +153,7 @@ func SetupConfig( openapinamer.NewDefinitionNamer(scheme, k8sscheme.Scheme)) // Add the custom routes to service discovery - serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs) + serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs, apiResourceConfig) serverConfig.OpenAPIV3Config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) { meta := r.Metadata() kind := "" @@ -287,6 +314,7 @@ func InstallAPIs( features featuremgmt.FeatureToggles, dualWriterMetrics *grafanarest.DualWriterMetrics, builderMetrics *BuilderMetrics, + apiResourceConfig *serverstorage.ResourceConfig, ) error { // dual writing is only enabled when the storage type is not legacy. // this is needed to support setting a default RESTOptionsGetter for new APIs that don't @@ -401,34 +429,9 @@ func InstallAPIs( for group, buildersForGroup := range buildersGroupMap { g := genericapiserver.NewDefaultAPIGroupInfo(group, scheme, metav1.ParameterCodec, codecs) for _, b := range buildersForGroup { - if err := b.UpdateAPIGroupInfo(&g, APIGroupOptions{ - Scheme: scheme, - OptsGetter: optsGetter, - DualWriteBuilder: dualWrite, - MetricsRegister: reg, - StorageOptsRegister: optsregister, - StorageOpts: storageOpts, - }); err != nil { + if err := installAPIGroupsForBuilder(&g, group, b, apiResourceConfig, scheme, optsGetter, dualWrite, reg, optsregister, storageOpts, features); err != nil { return err } - if len(g.PrioritizedVersions) < 1 { - continue - } - - // if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed - //nolint:staticcheck // not yet migrated to OpenFeature - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok { - for name := range resources { - if !allowRegisteringResourceByInfo(b.AllowedV0Alpha1Resources(), name) { - delete(resources, name) - } - } - if len(resources) == 0 { - delete(g.VersionedResourcesStorageMap, "v0alpha1") - } - } - } } // skip installing the group if there are no resources left after filtering @@ -445,6 +448,53 @@ func InstallAPIs( return nil } +func installAPIGroupsForBuilder(g *genericapiserver.APIGroupInfo, group string, b APIGroupBuilder, apiResourceConfig *serverstorage.ResourceConfig, scheme *runtime.Scheme, + optsGetter generic.RESTOptionsGetter, dualWrite grafanarest.DualWriteBuilder, reg prometheus.Registerer, optsregister apistore.StorageOptionsRegister, + storageOpts *options.StorageOptions, features featuremgmt.FeatureToggles) error { + if err := b.UpdateAPIGroupInfo(g, APIGroupOptions{ + Scheme: scheme, + OptsGetter: optsGetter, + DualWriteBuilder: dualWrite, + MetricsRegister: reg, + StorageOptsRegister: optsregister, + StorageOpts: storageOpts, + }); err != nil { + return err + } + if len(g.PrioritizedVersions) < 1 { + return nil + } + + // filter out api groups that are disabled in APIEnablementOptions + for version := range g.VersionedResourcesStorageMap { + gvr := schema.GroupVersionResource{ + Group: group, + Version: version, + } + if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) { + klog.InfoS("Skipping storage for disabled resource", "gvr", gvr.String()) + delete(g.VersionedResourcesStorageMap, version) + } + } + + // if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed + //nolint:staticcheck // not yet migrated to OpenFeature + if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { + if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok { + for name := range resources { + if !allowRegisteringResourceByInfo(b.AllowedV0Alpha1Resources(), name) { + delete(resources, name) + } + } + if len(resources) == 0 { + delete(g.VersionedResourcesStorageMap, "v0alpha1") + } + } + } + + return nil +} + // AddPostStartHooks adds post start hooks to a generic API server config func AddPostStartHooks( config *genericapiserver.RecommendedConfig, diff --git a/pkg/services/apiserver/builder/openapi.go b/pkg/services/apiserver/builder/openapi.go index 9cb7cc17f7c..6d3b33f4baf 100644 --- a/pkg/services/apiserver/builder/openapi.go +++ b/pkg/services/apiserver/builder/openapi.go @@ -9,6 +9,8 @@ import ( apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime/schema" + serverstorage "k8s.io/apiserver/pkg/server/storage" + "k8s.io/klog/v2" openapi "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" spec "k8s.io/kube-openapi/pkg/validation/spec" @@ -76,6 +78,7 @@ func addBuilderRoutes( targetGroupVersion schema.GroupVersion, openAPISpec *spec3.OpenAPI, apiGroupBuilders []APIGroupBuilder, + apiResourceConfig *serverstorage.ResourceConfig, ) (*spec3.OpenAPI, error) { for _, apiGroupBuilder := range apiGroupBuilders { // Optionally include raw http handlers for all builders @@ -107,12 +110,24 @@ func addBuilderRoutes( } } } + + // filter out api groups that are disabled in APIEnablementOptions + for path := range openAPISpec.Paths.Paths { + if strings.HasPrefix(path, "/apis/"+targetGroupVersion.String()+"/") { + gv := targetGroupVersion.WithResource("") + if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gv) { + klog.InfoS("removing openapi routes for disabled resource", "gv", gv.String()) + delete(openAPISpec.Paths.Paths, path) + } + } + } + return openAPISpec, nil } // Modify the OpenAPI spec to include the additional routes. // nolint:gocyclo -func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) { +func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion, apiResourceConfig *serverstorage.ResourceConfig) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) { return func(s *spec3.OpenAPI) (*spec3.OpenAPI, error) { if s.Paths == nil { return s, nil @@ -227,7 +242,7 @@ func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []s } } } - return addBuilderRoutes(gv, ©, builders) + return addBuilderRoutes(gv, ©, builders, apiResourceConfig) } } return s, nil diff --git a/pkg/services/apiserver/builder/request_handler.go b/pkg/services/apiserver/builder/request_handler.go index 80fbf6d0eaa..50761f1d42c 100644 --- a/pkg/services/apiserver/builder/request_handler.go +++ b/pkg/services/apiserver/builder/request_handler.go @@ -6,7 +6,9 @@ import ( "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" + serverstorage "k8s.io/apiserver/pkg/server/storage" restclient "k8s.io/client-go/rest" + klog "k8s.io/klog/v2" "k8s.io/kube-openapi/pkg/spec3" ) @@ -14,7 +16,7 @@ type requestHandler struct { router *mux.Router } -func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer) (http.Handler, error) { +func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer, apiResourceConfig *serverstorage.ResourceConfig) (http.Handler, error) { useful := false // only true if any routes exist anywhere router := mux.NewRouter() @@ -27,6 +29,12 @@ func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient } for _, gv := range GetGroupVersions(builder) { + // filter out api groups that are disabled in APIEnablementOptions + gvr := gv.WithResource("") + if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) { + klog.InfoS("Skipping custom route handler for disabled group version", "gv", gv.String()) + continue + } routes := provider.GetAPIRoutes(gv) if routes == nil { continue diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 28da416536c..605016c112d 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -316,7 +316,11 @@ func (s *service) start(ctx context.Context) error { s.cfg.BuildBranch, ) - if err := o.APIEnablementOptions.ApplyTo(&serverConfig.Config, appinstaller.NewAPIResourceConfig(s.appInstallers), s.scheme); err != nil { + apiResourceConfig := appinstaller.NewAPIResourceConfig(s.appInstallers) + // add the builder group versions to the api resource config + apiResourceConfig.EnableVersions(groupVersions...) + + if err := o.APIEnablementOptions.ApplyTo(&serverConfig.Config, apiResourceConfig, s.scheme); err != nil { return err } @@ -359,6 +363,7 @@ func (s *service) start(ctx context.Context) error { groupVersions, defGetters, s.metrics, + apiResourceConfig, ) if err != nil { return err @@ -400,6 +405,7 @@ func (s *service) start(ctx context.Context) error { s.features, s.dualWriterMetrics, s.builderMetrics, + apiResourceConfig, ) if err != nil { return err From 2c18526a78d44e8dc7c9fe75aba7b0ba134af25c Mon Sep 17 00:00:00 2001 From: William Wernert Date: Mon, 24 Nov 2025 14:58:43 -0500 Subject: [PATCH 086/378] Alerting: Collate rule_group column as binary (#114365) * fix: collate rule_group column as utf8mb4_bin This ensures this column is sorted identically by the database and golang, which should eliminate issues related to a mismatch in sort order. * chore: make test stricter * fix: also add binary collation for postgres --- pkg/services/ngalert/store/alert_rule_test.go | 8 +++----- pkg/services/sqlstore/migrations/migrations.go | 2 ++ .../ualert/alert_rule_group_collation_bin.go | 10 ++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 pkg/services/sqlstore/migrations/ualert/alert_rule_group_collation_bin.go diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 9a491120c8c..b0b7592c2d1 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -1516,11 +1516,9 @@ func TestIntegrationListAlertRulesByGroupCaseSensitiveOrdering(t *testing.T) { } // Verify case-sensitive alphabetical ordering - // different databases may sort uppercase before lowercase or vice versa depending on character set, the important part is that the order is consistent and case-sensitive - expectedOrder := []string{"test", "Test", "TEST"} - alternateExpectedOrder := []string{"TEST", "Test", "test"} - if !slices.Equal(groupOrder, expectedOrder) && !slices.Equal(groupOrder, alternateExpectedOrder) { - t.Fatalf("groups are not ordered case-sensitively as expected. got: %v, want: %v or %v", groupOrder, expectedOrder, alternateExpectedOrder) + expectedOrder := []string{"TEST", "Test", "test"} + if !slices.Equal(groupOrder, expectedOrder) { + t.Fatalf("groups are not ordered case-sensitively as expected. got: %v, want: %v", groupOrder, expectedOrder) } // Verify each group contains the correct rules diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 279839a4ae4..4fc4e1df131 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -162,4 +162,6 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) { ualert.ExpandAlertRuleUpdatedByMigration(mg) ualert.AddAlertRuleGroupIndexMigration(mg) + + ualert.CollateBinAlertRuleGroup(mg) } diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_group_collation_bin.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_group_collation_bin.go new file mode 100644 index 00000000000..4464d6669d3 --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule_group_collation_bin.go @@ -0,0 +1,10 @@ +package ualert + +import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +// CollateBinAlertRuleGroup ensures that rule_group column collates in the same way go sorts strings. +func CollateBinAlertRuleGroup(mg *migrator.Migrator) { + mg.AddMigration("ensure rule_group column sorts the same way as golang", migrator.NewRawSQLMigration(""). + Mysql("ALTER TABLE alert_rule MODIFY rule_group VARCHAR(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL;"). + Postgres(`ALTER TABLE alert_rule ALTER COLUMN rule_group SET DATA TYPE varchar(190) COLLATE "C";`)) +} From 93362c90243f556cdc7e055267245d146df87d3f Mon Sep 17 00:00:00 2001 From: Luminessa Starlight Date: Mon, 24 Nov 2025 15:39:09 -0500 Subject: [PATCH 087/378] Chore: Bump i18next-cli to 1.24.22 (#114183) * bump i18next-cli to latest * make it 1.24.22 while we're here * Update extracted phrases after i18n-extract update * yarn dedupe --------- Co-authored-by: joshhunt --- package.json | 2 +- packages/grafana-alerting/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- .../src/locales/en-US/grafana-prometheus.json | 2 +- packages/grafana-sql/package.json | 2 +- .../src/locales/en-US/grafana-sql.json | 2 +- .../grafana-azure-monitor-datasource.json | 4 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/mssql/locales/en-US/mssql.json | 4 +- .../app/plugins/datasource/mssql/package.json | 2 +- public/locales/en-US/grafana.json | 76 +-- yarn.lock | 630 ++++++++---------- 12 files changed, 327 insertions(+), 403 deletions(-) diff --git a/package.json b/package.json index 1ab0dc7a811..2e6fcba94f2 100644 --- a/package.json +++ b/package.json @@ -210,7 +210,7 @@ "html-loader": "5.1.0", "html-webpack-plugin": "5.6.3", "http-server": "14.1.1", - "i18next-cli": "1.11.12", + "i18next-cli": "^1.24.22", "ini": "^5.0.0", "jest": "29.7.0", "jest-canvas-mock": "2.5.2", diff --git a/packages/grafana-alerting/package.json b/packages/grafana-alerting/package.json index da13aa6fc0f..1c3ab2bf9b4 100644 --- a/packages/grafana-alerting/package.json +++ b/packages/grafana-alerting/package.json @@ -72,7 +72,7 @@ "@types/react-dom": "18.3.5", "@types/tinycolor2": "^1", "i18next": "^25.5.2", - "i18next-cli": "1.11.12", + "i18next-cli": "^1.24.22", "react": "18.3.1", "react-dom": "18.3.1", "react-redux": "^9.2.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index a3a1fc78408..8ed4bd95d5a 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -91,7 +91,7 @@ "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", "esbuild": "0.25.8", - "i18next-cli": "1.11.12", + "i18next-cli": "^1.24.22", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "react": "18.3.1", diff --git a/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json index d21b1c2d693..65289da4d2d 100644 --- a/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Scrape interval", "label-series-limit": "Series limit", "label-use-series-endpoint": "Use series endpoint", - "more-info": "For more information on configuring prometheus type and version in data sources, see the <2>provisioning documentation .", + "more-info": "For more information on configuring prometheus type and version in data sources, see the <2>provisioning documentation.", "placeholder-example-maxsourceresolutionmtimeout": "Example: {{example}}", "title-interval-behaviour": "Interval behaviour", "title-other": "Other", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 554eac97254..b184683aff2 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -48,7 +48,7 @@ "@types/react-virtualized-auto-sizer": "1.0.8", "@types/systemjs": "6.15.3", "@types/uuid": "10.0.0", - "i18next-cli": "1.11.12", + "i18next-cli": "^1.24.22", "jest": "^29.6.4", "ts-jest": "29.4.0", "ts-node": "10.9.2", diff --git a/packages/grafana-sql/src/locales/en-US/grafana-sql.json b/packages/grafana-sql/src/locales/en-US/grafana-sql.json index f2a06d8b983..f9572756abd 100644 --- a/packages/grafana-sql/src/locales/en-US/grafana-sql.json +++ b/packages/grafana-sql/src/locales/en-US/grafana-sql.json @@ -54,7 +54,7 @@ } }, "query-header": { - "content-invalid-query": "Your query is invalid. Check below for details.
However, you can still run this query.", + "content-invalid-query": "Your query is invalid. Check below for details.
However, you can still run this query.", "editor-modes": { "label-builder": "Builder", "label-code": "Code" diff --git a/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json index 1c84f68f612..47d60ee13f7 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Subscription", "placeholder-resource-name": "name", "tooltip-region": "The code region of the resource. Optional for one resource but mandatory when selecting multiple ones.", - "tooltip-resource-uri": "Manually edit the <2>resource uri . Supports the use of multiple template variables (ex: /subscriptions/$subId/resourceGroups/$rg)" + "tooltip-resource-uri": "Manually edit the <2>resource uri. Supports the use of multiple template variables (ex: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Aggregate function", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Basic Logs", - "description-basic-logs": "Enabling this feature incurs Azure Monitor per-query costs on dashboard panels that query tables configured for <2>Basic Logs .", + "description-basic-logs": "Enabling this feature incurs Azure Monitor per-query costs on dashboard panels that query tables configured for <2>Basic Logs.", "label-enable-basic-logs": "Enable Basic Logs" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 8e3a153c85f..70d873551d4 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -38,7 +38,7 @@ "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", - "i18next-cli": "1.11.12", + "i18next-cli": "^1.24.22", "jest": "29.7.0", "react-select-event": "5.5.1", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/mssql/locales/en-US/mssql.json b/public/app/plugins/datasource/mssql/locales/en-US/mssql.json index 4e20b5d10c1..c495e66b179 100644 --- a/public/app/plugins/datasource/mssql/locales/en-US/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/en-US/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "{{encryptionValue}} - Data sent between client and server is not encrypted beyond the login packet. (default)", "description-encrypt-older-version": "If you're using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to disable encryption to be able to connect.", "description-encrypt-true": "{{encryptionValue}} - Data sent between client and server is encrypted.", - "description-min-interval": "A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example <1>{{exampleInterval}} if your data is written every minute.", + "description-min-interval": "A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example<1>{{exampleInterval}} if your data is written every minute.", "description-tls-cert": "Path to file containing the public key certificate of the CA that signed the SQL Server certificate. Needed when the server certificate is self signed.", "label-auth-settings": "Azure Authentication Settings", "label-auth-type": "Authentication Type", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Indicate whether DNS `SRV` records should be used to locate the KDCs and other servers for a realm. The default is <1>{{default}}.", - "description-krb5-config-file-path": "The path to the configuration file for the <2>MIT krb5 package . The default is <4>{{default}}.", + "description-krb5-config-file-path": "The path to the configuration file for the <2>MIT krb5 package. The default is <4>{{default}}.", "description-udp-preference-limit": "The default is <1>{{default}} and means always use TCP and is optional.", "label-dns-lookup-kdc": "DNS Lookup KDC", "label-krb5-config-file-path": "krb5 config file path", diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index cb766af057f..48b210ca0f5 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -26,7 +26,7 @@ "@types/lodash": "4.17.20", "@types/node": "24.10.1", "@types/react": "18.3.18", - "i18next-cli": "1.11.12", + "i18next-cli": "^1.24.22", "ts-node": "10.9.2", "typescript": "5.9.2", "webpack": "5.101.0" diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 1da6f55fa78..474fde1a97d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -209,7 +209,7 @@ "title-delete": "Delete" }, "orgs": { - "delete-body": "Are you sure you want to delete '{{deleteOrgName}}'?
<5>All dashboards for this organization will be removed!", + "delete-body": "Are you sure you want to delete '{{deleteOrgName}}'?
<5>All dashboards for this organization will be removed!", "id-header": "ID", "name-header": "Name", "new-org-button": "New org" @@ -1145,7 +1145,7 @@ "title-something-wrong-trying-fetch-group-details": "Something went wrong when trying to fetch group details" }, "evaluation-interval-limit-exceeded": { - "body-minimum-interval": "A minimum evaluation interval of {{minInterval}} has been configured in Grafana.
Please contact the administrator to configure a lower interval.", + "body-minimum-interval": "A minimum evaluation interval of {{minInterval}} has been configured in Grafana.
Please contact the administrator to configure a lower interval.", "title-global-evaluation-interval-limit-exceeded": "Global evaluation interval limit exceeded" }, "existing-rule-editor": { @@ -1417,7 +1417,7 @@ "title-add-folder-and-labels": "Add folder and labels" }, "grafana-managed-rule-type": { - "description": "Supports multiple data sources of any kind.
Transform data with expressions." + "description": "Supports multiple data sources of any kind.
Transform data with expressions." }, "grafana-modify-export": { "body-invalid-rule-id": "The rule UID in the page URL is invalid. Please check the URL and try again.", @@ -1848,7 +1848,7 @@ "aria-label-new": "new" }, "mimir-flavored-type": { - "description": "Use a Mimir, Loki or Cortex datasource.
Expressions are not supported." + "description": "Use a Mimir, Loki or Cortex datasource.
Expressions are not supported." }, "min-interval-option": { "label-interval": "Interval", @@ -2081,7 +2081,7 @@ "warning-1": "Deleting this notification policy will permanently remove it.", "warning-2": "Are you sure you want to delete this policy?" }, - "filter-description": "Filter notification policies by using a comma separated list of matchers, e.g.: <1>severity=critical, region=EMEA", + "filter-description": "Filter notification policies by using a comma separated list of matchers, e.g.:<1>severity=critical, region=EMEA", "generated-policies": "Auto-generated policies", "matchers": "Matchers", "metadata": { @@ -2244,7 +2244,7 @@ "error-no-query-editor": "Could not load query editor due to: {{errorMessage}}" }, "recording-rule-type": { - "description": "Precompute expressions.
Should be combined with an alert rule." + "description": "Precompute expressions.
Should be combined with an alert rule." }, "recording-rules": { "description-target-data-source": "The Prometheus data source to store recording rules in", @@ -2808,7 +2808,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Alert template data", - "only-in-alert": "Available only when in the context of an Alert (e.g. inside .Alerts loop)" + "only-in-alert": "Available only when in the context of an Alert (e.g. inside.Alerts loop)" }, "available-context-notification": "Available in the context of a notification.", "notification-template-data": "Notification template data" @@ -3093,7 +3093,7 @@ "title-notification-policies": "Notification policies" }, "yaml-content-info": { - "body": "The YAML content in the editor only contains alert rule configuration
To configure Prometheus, you need to provide the rest of the <4>configuration file content." + "body": "The YAML content in the editor only contains alert rule configuration
To configure Prometheus, you need to provide the rest of the <4>configuration file content." } }, "alertlist": { @@ -3169,7 +3169,7 @@ "no-annotations-found": "No annotations found" }, "annotation-list-item": { - "tooltip-created-by": "Created by:
{{email}}" + "tooltip-created-by": "Created by:
{{email}}" }, "category-annotation-query": "Annotation query", "category-display": "Display", @@ -3207,7 +3207,7 @@ }, "empty-state": { "button-title": "Add annotation query", - "info-box-content": "

Annotations provide a way to integrate event data into your graphs. They are visualized as vertical lines and icons on all graph panels. When you hover over an annotation icon you can get event text & tags for the event. You can add annotation events directly from grafana by holding CTRL or CMD + click on graph (or drag region). These will be stored in Grafana's annotation database.

", + "info-box-content": "

Annotations provide a way to integrate event data into your graphs. They are visualized as vertical lines and icons on all graph panels. When you hover over an annotation icon you can get event text & tags for the event. You can add annotation events directly from grafana by holding CTRL or CMD + click on graph (or drag region). These will be stored in Grafana's annotation database.

", "info-box-content-2": "Checkout the <2>Annotations documentation for more information.", "title": "There are no custom annotation queries added yet" }, @@ -3245,7 +3245,7 @@ "auth-settings": "Auth settings" }, "auth-drawer-unconneced": { - "subtitle": "Configure auth settings. Find out more in our <2>documentation ." + "subtitle": "Configure auth settings. Find out more in our <2>documentation." }, "auth-drawer-unconnected": { "advanced-auth": "Advanced Auth", @@ -3279,7 +3279,7 @@ "allowed-organizations-description": "List of comma- or space-separated organizations. The user should be a member \nof at least one organization to log in.", "allowed-organizations-label": "Allowed organizations", "allowed-organizations-placeholder": "Enter organizations (my-team, myteam...) and press Enter to add", - "api-url-description": "The user information endpoint of your OAuth2 provider. Information returned by this endpoint must be compatible with <2>OpenID UserInfo .", + "api-url-description": "The user information endpoint of your OAuth2 provider. Information returned by this endpoint must be compatible with <2>OpenID UserInfo.", "api-url-required": "This field must be a valid URL if set.", "auth-style-description": "It determines how \"{{ clientIDLabel }}\" and \"{{ clientSecretLabel }}\" are sent to Oauth2 provider. Default is AutoDetect.", "auth-style-label": "Auth style", @@ -3424,7 +3424,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "Manage your auth settings and configure single sign-on. Find out more in our <2>documentation ." + "subtitle": "Manage your auth settings and configure single sign-on. Find out more in our <2>documentation." }, "bar-chart": { "warn": { @@ -4290,7 +4290,7 @@ "okay": "Okay" }, "not-found-datasource": { - "body": "Maybe you mistyped the URL or the plugin with the id <1> is unavailable.
To see a list of available datasources please <5>click here." + "body": "Maybe you mistyped the URL or the plugin with the id <1> is unavailable.
To see a list of available datasources please <5>click here." }, "oss": { "connections-home-page": { @@ -4403,7 +4403,7 @@ }, "source-form": { "control-required": "This field is required.", - "description": "A data point needs to provide values to all variables as fields or as transformations output to make the correlation button appear in the visualization.
Note: Not every variable needs to be explicitly defined below. A transformation such as <4>logfmt will create variables for every key/value pair.", + "description": "A data point needs to provide values to all variables as fields or as transformations output to make the correlation button appear in the visualization.
Note: Not every variable needs to be explicitly defined below. A transformation such as <4>logfmt will create variables for every key/value pair.", "description-external-pre": "You have used following variables in the target URL:", "description-query-pre": "You have used following variables in the target query:", "external-title": "Configure the data source that will use the URL (Step 3 of 3)", @@ -4415,12 +4415,12 @@ "results-required": "This field is required.", "source-description": "Results from selected source data source have links displayed in the panel", "source-label": "Source", - "sub-text": "

Define what data source will display the correlation, and what data will replace previously defined variables.

" + "sub-text": "

Define what data source will display the correlation, and what data will replace previously defined variables.

" }, "sub-title": "Define how data living in different data sources relates to each other. Read more in the <2>documentation", "target-form": { "control-rules": "This field is required.", - "sub-text": "

Define what the correlation will link to. With the query type, a query will run when the correlation is clicked. With the external type, clicking the correlation will open a URL.

", + "sub-text": "

Define what the correlation will link to. With the query type, a query will run when the correlation is clicked. With the external type, clicking the correlation will open a URL.

", "target-description-external": "Specify the URL that will open when the link is clicked", "target-description-query": "Specify which data source is queried when the link is clicked", "target-label": "Target", @@ -4627,7 +4627,7 @@ } }, "confirm-plugin-dashboard-save-modal": { - "body-plugin-dashboard": "Your changes will be lost when you update the plugin.
<2>Use Save As to create custom version.", + "body-plugin-dashboard": "Your changes will be lost when you update the plugin.
<2>Use Save As to create custom version.", "cancel": "Cancel", "overwrite": "Overwrite", "title-plugin-dashboard": "Plugin dashboard" @@ -4844,7 +4844,7 @@ "add-visualization-body": "Select a data source and then query and visualize your data with charts, stats and tables or create lists, markdowns and other widgets.", "add-visualization-button": "Add visualization", "add-visualization-header": "Start your new dashboard by adding a visualization", - "import-a-dashboard-body": "Import dashboards from files or <2>grafana.com .", + "import-a-dashboard-body": "Import dashboards from files or <2>grafana.com.", "import-a-dashboard-header": "Import a dashboard", "import-dashboard-button": "Import dashboard", "show-less-dashboards": "Show less", @@ -5319,8 +5319,8 @@ "title-provisioned": "Provisioned dashboard" }, "save-dashboard-error-proxy": { - "body-name-exists": "A dashboard with the same name in selected folder already exists.
<2>Would you still like to save this dashboard?", - "body-version-mismatch": "Someone else has updated this dashboard
<2>Would you still like to save this dashboard?", + "body-name-exists": "A dashboard with the same name in selected folder already exists.
<2>Would you still like to save this dashboard?", + "body-version-mismatch": "Someone else has updated this dashboard
<2>Would you still like to save this dashboard?", "confirmText-save-and-overwrite": "Save and overwrite", "title-name-exists": "Conflict", "title-version-mismatch": "Conflict" @@ -6199,7 +6199,7 @@ "query": "Query" }, "query-variable-editor-form": { - "description-examples": "Named capture groups can be used to separate the display text and value ( <1>see examples ).", + "description-examples": "Named capture groups can be used to separate the display text and value (<1>see examples ).", "description-optional": "Optional, if you want to extract part of a series name or metric node segment.", "label-data-source": "Data source", "label-static-options-sort": "Static options sort", @@ -6623,7 +6623,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Click <2>here to learn more about this error.", - "success-more-details-links": "Next, you can start to visualize data by <2>building a dashboard , or by querying data in the <5>Explore view ." + "success-more-details-links": "Next, you can start to visualize data by <2>building a dashboard, or by querying data in the <5>Explore view." }, "data-sources": { "datasource-add-button": { @@ -6719,7 +6719,7 @@ "test": "Test" }, "cloud-info-box": { - "body-alert": "Or skip the effort and get {{mainDS}} (and {{extraDS}}) as fully-managed, scalable, and hosted data sources from Grafana Labs with the <6>free-forever Grafana Cloud plan .", + "body-alert": "Or skip the effort and get {{mainDS}} (and {{extraDS}}) as fully-managed, scalable, and hosted data sources from Grafana Labs with the <6>free-forever Grafana Cloud plan.", "title-alert": "Configure your {{mainDS}} data source below" }, "dashboards-table": { @@ -7703,8 +7703,8 @@ }, "math": { "available-math-functions": "Available math functions", - "run-math-operations": "Run math operations on one or more queries. You reference the query by {{refExample}} ie. {{ref1}}, {{ref2}}, {{ref3}} etc.
Example: <12>{{example}}", - "tooltip-footer": "See our additional documentation on <2>Math expressions .", + "run-math-operations": "Run math operations on one or more queries. You reference the query by {{refExample}} ie. {{ref1}}, {{ref2}}, {{ref3}}etc.
Example: <12>{{example}}", + "tooltip-footer": "See our additional documentation on <2>Math expressions.", "tooltip-title": "Math operator", "tooltip-trigger": "Expression" }, @@ -9545,7 +9545,7 @@ "label": "Search base DNS", "placeholder": "example: dc=grafana,dc=org" }, - "subtitle": "The LDAP integration in Grafana allows your Grafana users to log in with their LDAP credentials. Find out more in our <2><0>documentation .", + "subtitle": "The LDAP integration in Grafana allows your Grafana users to log in with their LDAP credentials. Find out more in our <2><1>documentation.", "title": "Basic Settings" }, "library-panel": { @@ -10941,10 +10941,10 @@ "placeholder-optional": "(optional)", "role": "Role", "submit": "Submit", - "tooltip": "You can now select the \"No basic role\" option and add permissions to your custom needs. You can find more information in <1>our documentation ." + "tooltip": "You can now select the \"No basic role\" option and add permissions to your custom needs. You can find more information in <1>our documentation." }, "user-invite-page": { - "sub-title": "Send invitation or add existing Grafana user to the organization. <1> {{orgName}}", + "sub-title": "Send invitation or add existing Grafana user to the organization.<1> {{orgName}}", "text": { "invite-user": "Invite user" } @@ -11034,7 +11034,7 @@ "switch-to-table": "Switch to table" }, "panel-plugin-error": { - "text-load-error": "Check the server startup logs for more information.
If this plugin was loaded from Git, then make sure it was compiled.", + "text-load-error": "Check the server startup logs for more information.
If this plugin was loaded from Git, then make sure it was compiled.", "title-load-error": "Error loading: {{panelId}}", "title-not-found": "Panel plugin not found: {{id}}" }, @@ -11335,7 +11335,7 @@ "message": "All plugins are up to date" }, "not-found-plugin": { - "body-plugin-not-found": "That plugin cannot be found. Please check the url is correct or
go to the <3>plugin catalog.", + "body-plugin-not-found": "That plugin cannot be found. Please check the url is correct or
go to the <3>plugin catalog.", "title-plugin-not-found": "Plugin not found" }, "plugin-actions": { @@ -11650,7 +11650,7 @@ "actions": { "set-up-required-feature-toggles": "Set up required feature toggles" }, - "learn-more-documentation": "Want to learn more? See our <2>documentation .", + "learn-more-documentation": "Want to learn more? See our <2>documentation.", "manage-dashboards-provision-updates-automatically": "Manage dashboards as code in Git and provision updates automatically", "manage-your-dashboards-with-remote-provisioning": "Get started with Git Sync", "store-dashboards-in-version-controlled-storage": "Store dashboards in version-controlled storage for better organization and history tracking" @@ -12396,7 +12396,7 @@ }, "menu": { "clear-button": "Clear all", - "tooltip": "You can now select the \"No basic role\" option and add permissions to your custom needs. You can find more information in <1>our documentation ." + "tooltip": "You can now select the \"No basic role\" option and add permissions to your custom needs. You can find more information in <1>our documentation." }, "menu-aria-label": "Role picker menu", "menu-group-option-aria-label": "Role picker option", @@ -12406,7 +12406,7 @@ }, "sub-menu-aria-label": "Role picker submenu", "title": { - "description": "Assign roles to users to ensure granular control over access to Grafana‘s features and resources. Find out more in our <2>documentation ." + "description": "Assign roles to users to ensure granular control over access to Grafana‘s features and resources. Find out more in our <2>documentation." } }, "role-picker-drawer": { @@ -12722,7 +12722,7 @@ "info-text": "Create a direct link to this dashboard or panel, customized with the options below.", "link-url": "Link URL", "render-alert": "Image renderer plugin not installed", - "render-instructions": "To render an image, you must install the <2>Grafana image renderer plugin . Please contact your Grafana administrator to install the plugin.", + "render-instructions": "To render an image, you must install the <2>Grafana image renderer plugin. Please contact your Grafana administrator to install the plugin.", "rendered-image": "Direct link rendered image", "save-alert": "Dashboard is not saved", "save-dashboard": "To render a panel image, you must save the dashboard first.", @@ -13368,7 +13368,7 @@ "forwards-time-aria-label": "Move time range forwards", "to": "to", "zoom-out-button": "Zoom out time range", - "zoom-out-tooltip": "Time range zoom out
CTRL+Z" + "zoom-out-tooltip": "Time range zoom out
CTRL+Z" }, "time-range": { "apply": "Apply time range", @@ -13496,7 +13496,7 @@ }, "transformations": { "empty": { - "add-transformation-body": "Transformations allow data to be changed in various ways before your visualization is shown.
This includes joining data together, renaming fields, making calculations, formatting data for display, and more.", + "add-transformation-body": "Transformations allow data to be changed in various ways before your visualization is shown.
This includes joining data together, renaming fields, making calculations, formatting data for display, and more.", "add-transformation-header": "Start transforming data" } }, @@ -13763,7 +13763,7 @@ "label-format": "Format", "label-set-timezone": "Set timezone", "label-time-field": "Time field", - "tooltip-format": "The output format for the field specified as a <2>Moment.js format string .", + "tooltip-format": "The output format for the field specified as a <2>Moment.js format string.", "tooltip-timezone-manually": "Set the timezone of the date manually" }, "format-time-transformer-editor": { diff --git a/yarn.lock b/yarn.lock index 778173eec14..131a46fe1fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2454,7 +2454,7 @@ __metadata: "@types/react-dom": "npm:18.3.5" fast-deep-equal: "npm:^3.1.3" i18next: "npm:^25.0.0" - i18next-cli: "npm:1.11.12" + i18next-cli: "npm:^1.24.22" immer: "npm:10.2.0" jest: "npm:29.7.0" lodash: "npm:4.17.21" @@ -2739,7 +2739,7 @@ __metadata: "@types/lodash": "npm:4.17.20" "@types/node": "npm:24.10.1" "@types/react": "npm:18.3.18" - i18next-cli: "npm:1.11.12" + i18next-cli: "npm:^1.24.22" lodash: "npm:4.17.21" react: "npm:18.3.1" rxjs: "npm:7.8.2" @@ -3019,7 +3019,7 @@ __metadata: "@types/tinycolor2": "npm:^1" fishery: "npm:^2.3.1" i18next: "npm:^25.5.2" - i18next-cli: "npm:1.11.12" + i18next-cli: "npm:^1.24.22" lodash: "npm:^4.17.21" react: "npm:18.3.1" react-dom: "npm:18.3.1" @@ -3521,7 +3521,7 @@ __metadata: "@types/uuid": "npm:10.0.0" debounce-promise: "npm:3.1.2" esbuild: "npm:0.25.8" - i18next-cli: "npm:1.11.12" + i18next-cli: "npm:^1.24.22" jest: "npm:29.7.0" jest-environment-jsdom: "npm:29.7.0" lodash: "npm:4.17.21" @@ -3709,7 +3709,7 @@ __metadata: "@types/react-virtualized-auto-sizer": "npm:1.0.8" "@types/systemjs": "npm:6.15.3" "@types/uuid": "npm:10.0.0" - i18next-cli: "npm:1.11.12" + i18next-cli: "npm:^1.24.22" immutable: "npm:5.1.4" jest: "npm:^29.6.4" lodash: "npm:4.17.21" @@ -4010,257 +4010,250 @@ __metadata: languageName: node linkType: hard -"@inquirer/ansi@npm:^1.0.0, @inquirer/ansi@npm:^1.0.1": - version: 1.0.1 - resolution: "@inquirer/ansi@npm:1.0.1" - checksum: 10/0dda65720736f3e730715f3778e0e90f039ebd1382c277495a4d1cdbd2b2863095aa7291cd8ea7d3c0618bdee04a375db6e10a7bae5fb904df0b632a1c7774f9 +"@inquirer/ansi@npm:^1.0.1, @inquirer/ansi@npm:^1.0.2": + version: 1.0.2 + resolution: "@inquirer/ansi@npm:1.0.2" + checksum: 10/d1496e573a63ee6752bcf3fc93375cdabc55b0d60f0588fe7902282c710b223252ad318ff600ee904e48555634663b53fda517f5b29ce9fbda90bfae18592fbc languageName: node linkType: hard -"@inquirer/checkbox@npm:^4.3.0": - version: 4.3.0 - resolution: "@inquirer/checkbox@npm:4.3.0" +"@inquirer/checkbox@npm:^4.3.2": + version: 4.3.2 + resolution: "@inquirer/checkbox@npm:4.3.2" dependencies: - "@inquirer/ansi": "npm:^1.0.1" - "@inquirer/core": "npm:^10.3.0" - "@inquirer/figures": "npm:^1.0.14" - "@inquirer/type": "npm:^3.0.9" - yoctocolors-cjs: "npm:^2.1.2" + "@inquirer/ansi": "npm:^1.0.2" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/figures": "npm:^1.0.15" + "@inquirer/type": "npm:^3.0.10" + yoctocolors-cjs: "npm:^2.1.3" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/aa7ddf0816bc1718afdc38d7ed1e16207bbf944164a5a3ac0d87072a5a1b51cf3417617c72128a78d66b2e40a45f4a5658bdfc4bb546e92a208e30c8006674e6 + checksum: 10/4ac5dd2679981e23f066c51c605cb1c63ccda9ea6e1ad895e675eb26702aaf6cf961bf5ca3acd832efba5edcf9883b6742002c801673d2b35c123a7fa7db7b23 languageName: node linkType: hard -"@inquirer/confirm@npm:^5.0.0, @inquirer/confirm@npm:^5.1.19": - version: 5.1.19 - resolution: "@inquirer/confirm@npm:5.1.19" +"@inquirer/confirm@npm:^5.0.0, @inquirer/confirm@npm:^5.1.21": + version: 5.1.21 + resolution: "@inquirer/confirm@npm:5.1.21" dependencies: - "@inquirer/core": "npm:^10.3.0" - "@inquirer/type": "npm:^3.0.9" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/type": "npm:^3.0.10" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/d65e0addf80c146d71a74057d77048bd78a4a80d74a9e0d774b759ff1adf38a33cde6c06a6d6ef802bb61ef9158770315dec3931f89b3624c0e63c595c0473c1 + checksum: 10/a107aa0073965ea510affb9e5b55baf40333503d600970c458c07770cd4e0eee01efc4caba66f0409b0fadc9550d127329622efb543cffcabff3ad0e7f865372 languageName: node linkType: hard -"@inquirer/core@npm:^10.2.2, @inquirer/core@npm:^10.3.0": - version: 10.3.0 - resolution: "@inquirer/core@npm:10.3.0" +"@inquirer/core@npm:^10.3.0, @inquirer/core@npm:^10.3.2": + version: 10.3.2 + resolution: "@inquirer/core@npm:10.3.2" dependencies: - "@inquirer/ansi": "npm:^1.0.1" - "@inquirer/figures": "npm:^1.0.14" - "@inquirer/type": "npm:^3.0.9" + "@inquirer/ansi": "npm:^1.0.2" + "@inquirer/figures": "npm:^1.0.15" + "@inquirer/type": "npm:^3.0.10" cli-width: "npm:^4.1.0" mute-stream: "npm:^2.0.0" signal-exit: "npm:^4.1.0" wrap-ansi: "npm:^6.2.0" - yoctocolors-cjs: "npm:^2.1.2" + yoctocolors-cjs: "npm:^2.1.3" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/41392e38dc3253b3dbebda9cca344ca5cfd72c05e1c5b8460cb00e0ce7fa665a1970a2c4af89dcb951d75fedfdf775cdd7ac3be0748dbe3dec47db5d899d6963 + checksum: 10/eb434bdf0ae7d904367003c772bcd80cbf679f79c087c99a4949fd7288e9a2f713ec3ea63381b9a001f52389ab56a77fcd88d64d81a03b1195193410ce8971c2 languageName: node linkType: hard -"@inquirer/editor@npm:^4.2.21": - version: 4.2.21 - resolution: "@inquirer/editor@npm:4.2.21" +"@inquirer/editor@npm:^4.2.23": + version: 4.2.23 + resolution: "@inquirer/editor@npm:4.2.23" dependencies: - "@inquirer/core": "npm:^10.3.0" - "@inquirer/external-editor": "npm:^1.0.2" - "@inquirer/type": "npm:^3.0.9" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/external-editor": "npm:^1.0.3" + "@inquirer/type": "npm:^3.0.10" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/cf2d4237dc86abb2143ba6e99a4a60368ee992ba56b0072457e4dd472e7ac99ebffcbd0895bf36d5f694f3e327ff0dbb70265952b03ec8a0dbf4abd1db306991 + checksum: 10/f91b9aadba6ea28a0f4ea5f075af421e076262aebbd737e1b9779f086fa9d559d064e9942a581544645d1dcf56d6b685e8063fe46677880fbca73f6de4e4e7c5 languageName: node linkType: hard -"@inquirer/expand@npm:^4.0.21": - version: 4.0.21 - resolution: "@inquirer/expand@npm:4.0.21" +"@inquirer/expand@npm:^4.0.23": + version: 4.0.23 + resolution: "@inquirer/expand@npm:4.0.23" dependencies: - "@inquirer/core": "npm:^10.3.0" - "@inquirer/type": "npm:^3.0.9" - yoctocolors-cjs: "npm:^2.1.2" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/type": "npm:^3.0.10" + yoctocolors-cjs: "npm:^2.1.3" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/eb1900c443895377c03652c3e2b6ca29c572fe6ee2682e264572957b9b4a596d3d55c9ea271934846fb05d5cc5195cca0dffde1386e41358ac5c308698320e93 + checksum: 10/73ad1d6376e5efe2a452c33494d6d16ee2670c638ae470a795fdff4acb59a8e032e38e141f87b603b6e96320977519b375dac6471d86d5e3087a9c1db40e3111 languageName: node linkType: hard -"@inquirer/external-editor@npm:^1.0.2": - version: 1.0.2 - resolution: "@inquirer/external-editor@npm:1.0.2" +"@inquirer/external-editor@npm:^1.0.3": + version: 1.0.3 + resolution: "@inquirer/external-editor@npm:1.0.3" dependencies: - chardet: "npm:^2.1.0" + chardet: "npm:^2.1.1" iconv-lite: "npm:^0.7.0" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/d0c5c73249b8153f4cf872c4fba01c57a7653142a4cad496f17ed03ef3769330a4b3c519b68d70af69d4bb33003d2599b66b2242be85411c0b027ff383619666 + checksum: 10/c95d7237a885b32031715089f92820525731d4d3c2bd7afdb826307dc296cc2b39e7a644b0bb265441963348cca42e7785feb29c3aaf18fd2b63131769bf6587 languageName: node linkType: hard -"@inquirer/figures@npm:^1.0.14": - version: 1.0.14 - resolution: "@inquirer/figures@npm:1.0.14" - checksum: 10/39df361eb607cea5a020d457e25f9c6aee3a1de8975c6295a4b3bfe86ba7e7f7bfbefa6a52b145b1790f2690e5c8f10eb822e5bc764aff7ba00a6cd24eec5a25 +"@inquirer/figures@npm:^1.0.15, @inquirer/figures@npm:^1.0.3": + version: 1.0.15 + resolution: "@inquirer/figures@npm:1.0.15" + checksum: 10/3f858807f361ca29f41ec1076bbece4098cc140d86a06159d42c6e3f6e4d9bec9e10871ccfcbbaa367d6a8462b01dff89f2b1b157d9de6e8726bec85533f525c languageName: node linkType: hard -"@inquirer/figures@npm:^1.0.3": - version: 1.0.11 - resolution: "@inquirer/figures@npm:1.0.11" - checksum: 10/357ddd2e83718bc3c9189d518b93fd69099af9c860354df9a5ac0ec024cb5df1228ae4608d2de7625624d2adcd047db813f29426a610eaae7b9e449f8c753c6b - languageName: node - linkType: hard - -"@inquirer/input@npm:^4.2.5": - version: 4.2.5 - resolution: "@inquirer/input@npm:4.2.5" +"@inquirer/input@npm:^4.3.1": + version: 4.3.1 + resolution: "@inquirer/input@npm:4.3.1" dependencies: - "@inquirer/core": "npm:^10.3.0" - "@inquirer/type": "npm:^3.0.9" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/type": "npm:^3.0.10" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/150dee6d6094663439a9941e5df5f1dadb819cd5ac68a7b5be0e0aceb4b74db0c8fa9a5fa0d21879928df6a53c84c9214b699f3fddbb6c617dd9c253a9d95155 + checksum: 10/713aaa4c94263299fbd7adfd65378f788cac1b5047f2b7e1ea349ca669db6c7c91b69ab6e2f6660cdbc28c7f7888c5c77ab4433bd149931597e43976d1ba5f34 languageName: node linkType: hard -"@inquirer/number@npm:^3.0.21": - version: 3.0.21 - resolution: "@inquirer/number@npm:3.0.21" +"@inquirer/number@npm:^3.0.23": + version: 3.0.23 + resolution: "@inquirer/number@npm:3.0.23" dependencies: - "@inquirer/core": "npm:^10.3.0" - "@inquirer/type": "npm:^3.0.9" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/type": "npm:^3.0.10" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/7b254cb3947c78a83593d82347bf93929b0af714cffa20f9f4503ec338004b3c0d6813945e3e3d7062b5d50006412b181ed833ac88c7da4df538df8bb15775d0 + checksum: 10/50694807b71746e15ed69d100aae3c8014d83c90aa660e8a179fe0db1046f26d727947542f64e24cc8b969a61659cb89fe36208cc2b59c1816382b598e686dd2 languageName: node linkType: hard -"@inquirer/password@npm:^4.0.21": - version: 4.0.21 - resolution: "@inquirer/password@npm:4.0.21" +"@inquirer/password@npm:^4.0.23": + version: 4.0.23 + resolution: "@inquirer/password@npm:4.0.23" dependencies: - "@inquirer/ansi": "npm:^1.0.1" - "@inquirer/core": "npm:^10.3.0" - "@inquirer/type": "npm:^3.0.9" + "@inquirer/ansi": "npm:^1.0.2" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/type": "npm:^3.0.10" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/07fb1527ea2d44a81b79d9263f59713e66977e21fbf44efedb6bf08d27d617900ef481c49c91b0a749caf1d282f2b5e19fe6b7474acc98db3edd174eb5d45416 + checksum: 10/97364970b01c85946a4a50ad876c53ef0c1857a9144e24fad65e5dfa4b4e5dd42564fbcdfa2b49bb049a25d127efbe0882cb18afcdd47b166ebd01c6c4b5e825 languageName: node linkType: hard -"@inquirer/prompts@npm:^7.8.6": - version: 7.9.0 - resolution: "@inquirer/prompts@npm:7.9.0" +"@inquirer/prompts@npm:^7.9.0": + version: 7.10.1 + resolution: "@inquirer/prompts@npm:7.10.1" dependencies: - "@inquirer/checkbox": "npm:^4.3.0" - "@inquirer/confirm": "npm:^5.1.19" - "@inquirer/editor": "npm:^4.2.21" - "@inquirer/expand": "npm:^4.0.21" - "@inquirer/input": "npm:^4.2.5" - "@inquirer/number": "npm:^3.0.21" - "@inquirer/password": "npm:^4.0.21" - "@inquirer/rawlist": "npm:^4.1.9" - "@inquirer/search": "npm:^3.2.0" - "@inquirer/select": "npm:^4.4.0" + "@inquirer/checkbox": "npm:^4.3.2" + "@inquirer/confirm": "npm:^5.1.21" + "@inquirer/editor": "npm:^4.2.23" + "@inquirer/expand": "npm:^4.0.23" + "@inquirer/input": "npm:^4.3.1" + "@inquirer/number": "npm:^3.0.23" + "@inquirer/password": "npm:^4.0.23" + "@inquirer/rawlist": "npm:^4.1.11" + "@inquirer/search": "npm:^3.2.2" + "@inquirer/select": "npm:^4.4.2" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/ca58889261018be39a58e93c03c542d47d25dd7f5bb93e98bde7e3bf63eb70f8d5243d32a3fd53797bb474471682490513d99ec5179323862e5a15cdeb35f4b5 + checksum: 10/b3e3386edd255e4e91c7908050674f8a2e69b043883c00feec2f87d697be37bc6e8cd4a360e7e3233a9825ae7ea044a2ac63d5700926d27f9959013d8566f890 languageName: node linkType: hard -"@inquirer/rawlist@npm:^4.1.9": - version: 4.1.9 - resolution: "@inquirer/rawlist@npm:4.1.9" +"@inquirer/rawlist@npm:^4.1.11": + version: 4.1.11 + resolution: "@inquirer/rawlist@npm:4.1.11" dependencies: - "@inquirer/core": "npm:^10.3.0" - "@inquirer/type": "npm:^3.0.9" - yoctocolors-cjs: "npm:^2.1.2" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/type": "npm:^3.0.10" + yoctocolors-cjs: "npm:^2.1.3" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/ec23e087bfa9497b36d51b53e8da18c837e4a0c5c091bce7d1a6b52d9664035d7e22c3753993dd3c7c9ebfd5e9b71f1738873f2c25422668733ddb28d74bf26b + checksum: 10/0d8f6484cfc20749190e95eecfb2d034bafb3644ec4907b84b1673646f5dd71730e38e35565ea98dfd240d8851e3cff653edafcc4e0af617054b127b407e3229 languageName: node linkType: hard -"@inquirer/search@npm:^3.2.0": - version: 3.2.0 - resolution: "@inquirer/search@npm:3.2.0" +"@inquirer/search@npm:^3.2.2": + version: 3.2.2 + resolution: "@inquirer/search@npm:3.2.2" dependencies: - "@inquirer/core": "npm:^10.3.0" - "@inquirer/figures": "npm:^1.0.14" - "@inquirer/type": "npm:^3.0.9" - yoctocolors-cjs: "npm:^2.1.2" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/figures": "npm:^1.0.15" + "@inquirer/type": "npm:^3.0.10" + yoctocolors-cjs: "npm:^2.1.3" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/b01a53d6f72090f45cda39b75dd2e613e2d7fa0f454c0781f846e543b833eed4da831fec8d9e5325ebd4383684a69a74c5a38520b7ee0734b1090f71cf5dd372 + checksum: 10/abaed2df7763633ff4414b58d1c87233b69ed3cd2ac77629f0d54b72b8b585dc4806c7a2a8261daba58af5b0a2147e586d079fdc82060b6bcf56b75d3d03f3a7 languageName: node linkType: hard -"@inquirer/select@npm:^4.4.0": - version: 4.4.0 - resolution: "@inquirer/select@npm:4.4.0" +"@inquirer/select@npm:^4.4.2": + version: 4.4.2 + resolution: "@inquirer/select@npm:4.4.2" dependencies: - "@inquirer/ansi": "npm:^1.0.1" - "@inquirer/core": "npm:^10.3.0" - "@inquirer/figures": "npm:^1.0.14" - "@inquirer/type": "npm:^3.0.9" - yoctocolors-cjs: "npm:^2.1.2" + "@inquirer/ansi": "npm:^1.0.2" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/figures": "npm:^1.0.15" + "@inquirer/type": "npm:^3.0.10" + yoctocolors-cjs: "npm:^2.1.3" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/97f1f167d640c668ea5f137fca76fd9096215546f0890732ab8ed0c58ac3d9f01db5d1877e877e74ef6877a139ab8970ee683c6c3f2c08f2e11522f5e0bfd61e + checksum: 10/795ec0ac77d575f20bd6a12fb1c040093e62217ac0c80194829a8d3c3d1e09f70ad738e9a9dd6095cc8358fff4e13882209c09bdf8eb0864a86dcabef5b0a6a6 languageName: node linkType: hard -"@inquirer/type@npm:^3.0.8, @inquirer/type@npm:^3.0.9": - version: 3.0.9 - resolution: "@inquirer/type@npm:3.0.9" +"@inquirer/type@npm:^3.0.10, @inquirer/type@npm:^3.0.9": + version: 3.0.10 + resolution: "@inquirer/type@npm:3.0.10" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/960ba4737405f70bac17e7cdc4696c60064b06c8dd13a4b3d0783763ba1714bdadbd598b88d537ab9415b7d5d61e011ac042cfbd1438b2a35298e2868724b853 + checksum: 10/57d113a9db7abc73326491e29bedc88ef362e53779f9f58a1b61225e0be068ce0c54e33cd65f4a13ca46131676fb72c3ef488463c4c9af0aa89680684c55d74c languageName: node linkType: hard @@ -7114,7 +7107,7 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:2.10.1": +"@reduxjs/toolkit@npm:2.10.1, @reduxjs/toolkit@npm:^2.9.0": version: 2.10.1 resolution: "@reduxjs/toolkit@npm:2.10.1" dependencies: @@ -7136,28 +7129,6 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:^2.9.0": - version: 2.9.2 - resolution: "@reduxjs/toolkit@npm:2.9.2" - dependencies: - "@standard-schema/spec": "npm:^1.0.0" - "@standard-schema/utils": "npm:^0.3.0" - immer: "npm:^10.0.3" - redux: "npm:^5.0.1" - redux-thunk: "npm:^3.1.0" - reselect: "npm:^5.1.0" - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 || ^19 - react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - checksum: 10/7a8fa57086bc44dafe2f04428d6557edad46ee6eb46b2fe80468ebf934a441a7d4d750b50a6d41bc360d9ab61baadb1dddb5a8f556cfac68977f56e832f509a2 - languageName: node - linkType: hard - "@remix-run/router@npm:1.19.1": version: 1.19.1 resolution: "@remix-run/router@npm:1.19.1" @@ -8896,13 +8867,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.13.19": - version: 1.13.19 - resolution: "@swc/core-darwin-arm64@npm:1.13.19" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@swc/core-darwin-arm64@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-darwin-arm64@npm:1.13.3" @@ -8910,10 +8874,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.13.19": - version: 1.13.19 - resolution: "@swc/core-darwin-x64@npm:1.13.19" - conditions: os=darwin & cpu=x64 +"@swc/core-darwin-arm64@npm:1.15.2": + version: 1.15.2 + resolution: "@swc/core-darwin-arm64@npm:1.15.2" + conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -8924,10 +8888,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.13.19": - version: 1.13.19 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.19" - conditions: os=linux & cpu=arm +"@swc/core-darwin-x64@npm:1.15.2": + version: 1.15.2 + resolution: "@swc/core-darwin-x64@npm:1.15.2" + conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -8938,10 +8902,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.13.19": - version: 1.13.19 - resolution: "@swc/core-linux-arm64-gnu@npm:1.13.19" - conditions: os=linux & cpu=arm64 & libc=glibc +"@swc/core-linux-arm-gnueabihf@npm:1.15.2": + version: 1.15.2 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.15.2" + conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -8952,10 +8916,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.13.19": - version: 1.13.19 - resolution: "@swc/core-linux-arm64-musl@npm:1.13.19" - conditions: os=linux & cpu=arm64 & libc=musl +"@swc/core-linux-arm64-gnu@npm:1.15.2": + version: 1.15.2 + resolution: "@swc/core-linux-arm64-gnu@npm:1.15.2" + conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard @@ -8966,10 +8930,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.13.19": - version: 1.13.19 - resolution: "@swc/core-linux-x64-gnu@npm:1.13.19" - conditions: os=linux & cpu=x64 & libc=glibc +"@swc/core-linux-arm64-musl@npm:1.15.2": + version: 1.15.2 + resolution: "@swc/core-linux-arm64-musl@npm:1.15.2" + conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard @@ -8980,10 +8944,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.13.19": - version: 1.13.19 - resolution: "@swc/core-linux-x64-musl@npm:1.13.19" - conditions: os=linux & cpu=x64 & libc=musl +"@swc/core-linux-x64-gnu@npm:1.15.2": + version: 1.15.2 + resolution: "@swc/core-linux-x64-gnu@npm:1.15.2" + conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard @@ -8994,10 +8958,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.13.19": - version: 1.13.19 - resolution: "@swc/core-win32-arm64-msvc@npm:1.13.19" - conditions: os=win32 & cpu=arm64 +"@swc/core-linux-x64-musl@npm:1.15.2": + version: 1.15.2 + resolution: "@swc/core-linux-x64-musl@npm:1.15.2" + conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard @@ -9008,10 +8972,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.13.19": - version: 1.13.19 - resolution: "@swc/core-win32-ia32-msvc@npm:1.13.19" - conditions: os=win32 & cpu=ia32 +"@swc/core-win32-arm64-msvc@npm:1.15.2": + version: 1.15.2 + resolution: "@swc/core-win32-arm64-msvc@npm:1.15.2" + conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -9022,10 +8986,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.13.19": - version: 1.13.19 - resolution: "@swc/core-win32-x64-msvc@npm:1.13.19" - conditions: os=win32 & cpu=x64 +"@swc/core-win32-ia32-msvc@npm:1.15.2": + version: 1.15.2 + resolution: "@swc/core-win32-ia32-msvc@npm:1.15.2" + conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -9036,53 +9000,14 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.13.19": - version: 1.13.19 - resolution: "@swc/core@npm:1.13.19" - dependencies: - "@swc/core-darwin-arm64": "npm:1.13.19" - "@swc/core-darwin-x64": "npm:1.13.19" - "@swc/core-linux-arm-gnueabihf": "npm:1.13.19" - "@swc/core-linux-arm64-gnu": "npm:1.13.19" - "@swc/core-linux-arm64-musl": "npm:1.13.19" - "@swc/core-linux-x64-gnu": "npm:1.13.19" - "@swc/core-linux-x64-musl": "npm:1.13.19" - "@swc/core-win32-arm64-msvc": "npm:1.13.19" - "@swc/core-win32-ia32-msvc": "npm:1.13.19" - "@swc/core-win32-x64-msvc": "npm:1.13.19" - "@swc/counter": "npm:^0.1.3" - "@swc/types": "npm:^0.1.25" - peerDependencies: - "@swc/helpers": ">=0.5.17" - dependenciesMeta: - "@swc/core-darwin-arm64": - optional: true - "@swc/core-darwin-x64": - optional: true - "@swc/core-linux-arm-gnueabihf": - optional: true - "@swc/core-linux-arm64-gnu": - optional: true - "@swc/core-linux-arm64-musl": - optional: true - "@swc/core-linux-x64-gnu": - optional: true - "@swc/core-linux-x64-musl": - optional: true - "@swc/core-win32-arm64-msvc": - optional: true - "@swc/core-win32-ia32-msvc": - optional: true - "@swc/core-win32-x64-msvc": - optional: true - peerDependenciesMeta: - "@swc/helpers": - optional: true - checksum: 10/07d0f72d8c82070a1ec2b439964780f0ba7aec61b17fa6518b1c372373bbe208ea9ce68f70f606d42df71a33149e4f704eabe07c6197e15d133e375546915f57 +"@swc/core-win32-x64-msvc@npm:1.15.2": + version: 1.15.2 + resolution: "@swc/core-win32-x64-msvc@npm:1.15.2" + conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/core@npm:1.13.3, @swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22": +"@swc/core@npm:1.13.3": version: 1.13.3 resolution: "@swc/core@npm:1.13.3" dependencies: @@ -9128,6 +9053,52 @@ __metadata: languageName: node linkType: hard +"@swc/core@npm:1.15.2, @swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22": + version: 1.15.2 + resolution: "@swc/core@npm:1.15.2" + dependencies: + "@swc/core-darwin-arm64": "npm:1.15.2" + "@swc/core-darwin-x64": "npm:1.15.2" + "@swc/core-linux-arm-gnueabihf": "npm:1.15.2" + "@swc/core-linux-arm64-gnu": "npm:1.15.2" + "@swc/core-linux-arm64-musl": "npm:1.15.2" + "@swc/core-linux-x64-gnu": "npm:1.15.2" + "@swc/core-linux-x64-musl": "npm:1.15.2" + "@swc/core-win32-arm64-msvc": "npm:1.15.2" + "@swc/core-win32-ia32-msvc": "npm:1.15.2" + "@swc/core-win32-x64-msvc": "npm:1.15.2" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.25" + peerDependencies: + "@swc/helpers": ">=0.5.17" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10/55c112b2d3ff098efd1d2d941ec8fc8ddbcd81c53ede67bcaa28d176a711fb39a250a63a627104f0c2f6f764e41f78eeb72edb5300c6d98cbbaf97cce84927a9 + languageName: node + linkType: hard + "@swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -12316,18 +12287,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1, axios@npm:^1.10.0, axios@npm:^1.6.1, axios@npm:^1.8.3": - version: 1.12.2 - resolution: "axios@npm:1.12.2" - dependencies: - follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.4" - proxy-from-env: "npm:^1.1.0" - checksum: 10/886a79770594eaad76493fecf90344b567bd956240609b5dcd09bd0afe8d3e6f1ad6d3257a93a483b6192b409d4b673d9515a34619e3e3ed1b2c0ec2a83b20ba - languageName: node - linkType: hard - -"axios@npm:^1.12.2": +"axios@npm:^1, axios@npm:^1.10.0, axios@npm:^1.12.2, axios@npm:^1.6.1, axios@npm:^1.8.3": version: 1.13.2 resolution: "axios@npm:1.13.2" dependencies: @@ -13251,7 +13211,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:5.6.2, chalk@npm:^5.6.2": +"chalk@npm:5.6.2, chalk@npm:^5.2.0, chalk@npm:^5.3.0, chalk@npm:^5.4.1, chalk@npm:^5.6.2": version: 5.6.2 resolution: "chalk@npm:5.6.2" checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0 @@ -13289,13 +13249,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.2.0, chalk@npm:^5.3.0, chalk@npm:^5.4.1": - version: 5.4.1 - resolution: "chalk@npm:5.4.1" - checksum: 10/29df3ffcdf25656fed6e95962e2ef86d14dfe03cd50e7074b06bad9ffbbf6089adbb40f75c00744d843685c8d008adaf3aed31476780312553caf07fa86e5bc7 - languageName: node - linkType: hard - "chance@npm:^1.1.13": version: 1.1.13 resolution: "chance@npm:1.1.13" @@ -13365,10 +13318,10 @@ __metadata: languageName: node linkType: hard -"chardet@npm:^2.1.0": - version: 2.1.0 - resolution: "chardet@npm:2.1.0" - checksum: 10/8085fd8e5b1234fafacb279b4dab84dc127f512f953441daf09fc71ade70106af0dff28e86bfda00bab0de61fb475fa9003c87f82cbad3da02a4f299bfd427da +"chardet@npm:^2.1.1": + version: 2.1.1 + resolution: "chardet@npm:2.1.1" + checksum: 10/d56913b65e45c5c86f331988e2ef6264c131bfeadaae098ee719bf6610546c77740e37221ffec802dde56b5e4466613a4c754786f4da6b5f6c5477243454d324 languageName: node linkType: hard @@ -13433,7 +13386,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:4.0.3, chokidar@npm:^4.0.0, chokidar@npm:^4.0.1": +"chokidar@npm:4.0.3, chokidar@npm:^4.0.0, chokidar@npm:^4.0.1, chokidar@npm:^4.0.3": version: 4.0.3 resolution: "chokidar@npm:4.0.3" dependencies: @@ -13899,10 +13852,10 @@ __metadata: languageName: node linkType: hard -"commander@npm:14.0.1": - version: 14.0.1 - resolution: "commander@npm:14.0.1" - checksum: 10/783115e9403caeca29c0fcbd4e0358f70c67760e4e4933f3453fcdd5ddba2ec44173c8da5213d7ce5e404f51c7e71203a42c548164dbe27b668b32a8981577f1 +"commander@npm:14.0.2, commander@npm:~14.0.0": + version: 14.0.2 + resolution: "commander@npm:14.0.2" + checksum: 10/2d202db5e5f9bb770112a3c1579b893d17ac6f6d932183077308bdd96d0f87f0bbe6a68b5b9ed2cf3b2514be6bb7de637480703c0e2db9741ee1b383237deb26 languageName: node linkType: hard @@ -13976,13 +13929,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:~14.0.0": - version: 14.0.0 - resolution: "commander@npm:14.0.0" - checksum: 10/c05418bfc35a3e8b5c67bd9f75f5b773f386f9b85f83e70e7c926047f270929cb06cf13cd68f387dd6e7e23c6157de8171b28ba606abd3e6256028f1f789becf - languageName: node - linkType: hard - "comment-parser@npm:1.4.1": version: 1.4.1 resolution: "comment-parser@npm:1.4.1" @@ -16230,16 +16176,7 @@ __metadata: languageName: node linkType: hard -"enquirer@npm:^2.3.6, enquirer@npm:~2.3.6": - version: 2.3.6 - resolution: "enquirer@npm:2.3.6" - dependencies: - ansi-colors: "npm:^4.1.1" - checksum: 10/751d14f037eb7683997e696fb8d5fe2675e0b0cde91182c128cf598acf3f5bd9005f35f7c2a9109e291140af496ebec237b6dac86067d59a9b44f3688107f426 - languageName: node - linkType: hard - -"enquirer@npm:^2.4.1": +"enquirer@npm:^2.3.6, enquirer@npm:^2.4.1": version: 2.4.1 resolution: "enquirer@npm:2.4.1" dependencies: @@ -16249,6 +16186,15 @@ __metadata: languageName: node linkType: hard +"enquirer@npm:~2.3.6": + version: 2.3.6 + resolution: "enquirer@npm:2.3.6" + dependencies: + ansi-colors: "npm:^4.1.1" + checksum: 10/751d14f037eb7683997e696fb8d5fe2675e0b0cde91182c128cf598acf3f5bd9005f35f7c2a9109e291140af496ebec237b6dac86067d59a9b44f3688107f426 + languageName: node + linkType: hard + "ensure-posix-path@npm:^1.1.0": version: 1.1.1 resolution: "ensure-posix-path@npm:1.1.1" @@ -18658,19 +18604,19 @@ __metadata: languageName: node linkType: hard -"glob@npm:*, glob@npm:11.0.3": - version: 11.0.3 - resolution: "glob@npm:11.0.3" +"glob@npm:*, glob@npm:12.0.0": + version: 12.0.0 + resolution: "glob@npm:12.0.0" dependencies: foreground-child: "npm:^3.3.1" jackspeak: "npm:^4.1.1" - minimatch: "npm:^10.0.3" + minimatch: "npm:^10.1.1" minipass: "npm:^7.1.2" package-json-from-dist: "npm:^1.0.0" path-scurry: "npm:^2.0.0" bin: glob: dist/esm/bin.mjs - checksum: 10/2ae536c1360c0266b523b2bfa6aadc10144a8b7e08869b088e37ac3c27cd30774f82e4bfb291cde796776e878f9e13200c7ff44010eb7054e00f46f649397893 + checksum: 10/6e21b3f1f1fa635836d45e54bbe50704884cc3e310e0cc011cfb5429db65a030e12936d99b07e66236370efe45dc8c8b26fa5334dbf555d6f8709e0315c77c30 languageName: node linkType: hard @@ -19137,7 +19083,7 @@ __metadata: http-server: "npm:14.1.1" i18next: "npm:^25.0.0" i18next-browser-languagedetector: "npm:^8.0.0" - i18next-cli: "npm:1.11.12" + i18next-cli: "npm:^1.24.22" i18next-pseudo: "npm:^2.2.1" immer: "npm:10.2.0" immutable: "npm:5.1.4" @@ -20000,25 +19946,26 @@ __metadata: languageName: node linkType: hard -"i18next-cli@npm:1.11.12": - version: 1.11.12 - resolution: "i18next-cli@npm:1.11.12" +"i18next-cli@npm:^1.24.22": + version: 1.24.22 + resolution: "i18next-cli@npm:1.24.22" dependencies: - "@swc/core": "npm:1.13.19" + "@swc/core": "npm:1.15.2" chalk: "npm:5.6.2" chokidar: "npm:4.0.3" - commander: "npm:14.0.1" + commander: "npm:14.0.2" execa: "npm:9.6.0" - glob: "npm:11.0.3" - i18next-resources-for-ts: "npm:1.7.4" - inquirer: "npm:12.9.6" + glob: "npm:12.0.0" + i18next-resources-for-ts: "npm:1.8.0" + inquirer: "npm:12.10.0" jiti: "npm:2.6.1" jsonc-parser: "npm:3.3.1" + minimatch: "npm:10.1.1" ora: "npm:9.0.0" - swc-walk: "npm:1.0.0" + swc-walk: "npm:1.0.1" bin: i18next-cli: dist/esm/cli.js - checksum: 10/b714cddb1fc5a611541a83f54320a84f4df5bd8308cd5b0c7abf6222d6d8e8cdac1e41fd9ecbb4d2930cf994e98d60235201f22e4f15baff0defb1759b2f3541 + checksum: 10/09dc8c551155c0a59fe793ba685d75a4a7810eb047113df3b43234ec5abdd26ffacb9e4f3c78b637f906518053b4aa8ea3e8af9e52fa0bab43e6379a81d44693 languageName: node linkType: hard @@ -20058,15 +20005,16 @@ __metadata: languageName: node linkType: hard -"i18next-resources-for-ts@npm:1.7.4": - version: 1.7.4 - resolution: "i18next-resources-for-ts@npm:1.7.4" +"i18next-resources-for-ts@npm:1.8.0": + version: 1.8.0 + resolution: "i18next-resources-for-ts@npm:1.8.0" dependencies: - "@babel/runtime": "npm:^7.27.0" - yaml: "npm:^2.7.1" + "@babel/runtime": "npm:^7.28.4" + chokidar: "npm:^4.0.3" + yaml: "npm:^2.8.1" bin: i18next-resources-for-ts: bin/i18next-resources-for-ts.js - checksum: 10/02ce71939dc6f38672d3b33281367ad7f2c52623371fd88fc1d0033eac5a6385ef45fed49c7a49e5bac0f9a8a97cdc95ec3ee1c68f26cc75dcbdcb5b525ca6cc + checksum: 10/217a6f4d978c36b83ed5e3bc9c550f0845e4e06378257be4a88f34a4f246278c677384d5481ae7936e8dd762dd68f0a86e136364f15ae2d887cef3704d4e3958 languageName: node linkType: hard @@ -20207,7 +20155,7 @@ __metadata: languageName: node linkType: hard -"immer@npm:10.2.0, immer@npm:^10.0.3, immer@npm:^10.2.0": +"immer@npm:10.2.0, immer@npm:^10.2.0": version: 10.2.0 resolution: "immer@npm:10.2.0" checksum: 10/d73e218c8f8ffbb39f9290dfafa478b94af73403dcf26b5672eef35233bb30f09ffe231f8a78a6c9cb442968510edd89e851776ec90a5ddfa82cee6db6b35137 @@ -20385,14 +20333,14 @@ __metadata: languageName: node linkType: hard -"inquirer@npm:12.9.6": - version: 12.9.6 - resolution: "inquirer@npm:12.9.6" +"inquirer@npm:12.10.0": + version: 12.10.0 + resolution: "inquirer@npm:12.10.0" dependencies: - "@inquirer/ansi": "npm:^1.0.0" - "@inquirer/core": "npm:^10.2.2" - "@inquirer/prompts": "npm:^7.8.6" - "@inquirer/type": "npm:^3.0.8" + "@inquirer/ansi": "npm:^1.0.1" + "@inquirer/core": "npm:^10.3.0" + "@inquirer/prompts": "npm:^7.9.0" + "@inquirer/type": "npm:^3.0.9" mute-stream: "npm:^2.0.0" run-async: "npm:^4.0.5" rxjs: "npm:^7.8.2" @@ -20401,7 +20349,7 @@ __metadata: peerDependenciesMeta: "@types/node": optional: true - checksum: 10/bcac231b3eba055aa16dbdb60ba6d7bfe66109be654bfb19f92095f703af07fc01528f716e86ec62f7bf7bd17b4e21ad4bb32b677cf42075dee04568afe9686b + checksum: 10/ed24f3fa202cd8b8fa05c56c8f42c202be7c24d621d968937ea2e298e1d06192979568706587fd243144e1ed55d248ba5c6a27b2c7ee6926864faad532d8572f languageName: node linkType: hard @@ -23807,6 +23755,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:10.1.1, minimatch@npm:^10.1.1": + version: 10.1.1 + resolution: "minimatch@npm:10.1.1" + dependencies: + "@isaacs/brace-expansion": "npm:^5.0.0" + checksum: 10/110f38921ea527022e90f7a5f43721838ac740d0a0c26881c03b57c261354fb9a0430e40b2c56dfcea2ef3c773768f27210d1106f1f2be19cde3eea93f26f45e + languageName: node + linkType: hard + "minimatch@npm:3.0.5": version: 3.0.5 resolution: "minimatch@npm:3.0.5" @@ -23825,15 +23782,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.0.3, minimatch@npm:^10.1.1": - version: 10.1.1 - resolution: "minimatch@npm:10.1.1" - dependencies: - "@isaacs/brace-expansion": "npm:^5.0.0" - checksum: 10/110f38921ea527022e90f7a5f43721838ac740d0a0c26881c03b57c261354fb9a0430e40b2c56dfcea2ef3c773768f27210d1106f1f2be19cde3eea93f26f45e - languageName: node - linkType: hard - "minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -26498,15 +26446,6 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.54.1": - version: 1.54.1 - resolution: "playwright-core@npm:1.54.1" - bin: - playwright-core: cli.js - checksum: 10/c0acfb7ecb48e9fb6c22f71298b966244bd4f8659093a798cc7f9a509deee22109560e44f2d507124e7718779640e0b4cb05a05c068b034405418d8740ec31ff - languageName: node - linkType: hard - "playwright-core@npm:1.56.1, playwright-core@npm:>=1.2.0": version: 1.56.1 resolution: "playwright-core@npm:1.56.1" @@ -26516,7 +26455,7 @@ __metadata: languageName: node linkType: hard -"playwright@npm:1.56.1": +"playwright@npm:1.56.1, playwright@npm:^1.14.0": version: 1.56.1 resolution: "playwright@npm:1.56.1" dependencies: @@ -26531,21 +26470,6 @@ __metadata: languageName: node linkType: hard -"playwright@npm:^1.14.0": - version: 1.54.1 - resolution: "playwright@npm:1.54.1" - dependencies: - fsevents: "npm:2.3.2" - playwright-core: "npm:1.54.1" - dependenciesMeta: - fsevents: - optional: true - bin: - playwright: cli.js - checksum: 10/ebad2924b589a4cfedf48288e67b4afc81047222e607321c9452a37610025e6e249bdfeff010c1c70cf9a9d35c0b9e5cb7934a58986f67928dd469d9fde1a035 - languageName: node - linkType: hard - "plop@npm:^4.0.1": version: 4.0.1 resolution: "plop@npm:4.0.1" @@ -31588,12 +31512,12 @@ __metadata: languageName: node linkType: hard -"swc-walk@npm:1.0.0": - version: 1.0.0 - resolution: "swc-walk@npm:1.0.0" +"swc-walk@npm:1.0.1": + version: 1.0.1 + resolution: "swc-walk@npm:1.0.1" dependencies: acorn-walk: "npm:^8.3.4" - checksum: 10/f6dc27eef421033956acbe12061e5f43a18b28747fd914734589f25d4144b0ef7de53bb1bf24fcf3a2355f305a3eb0593d0eaa006a43316113c79672b0c070bd + checksum: 10/7e1c727a094e8de7692280f4cfa16e9ee42b8f2b9a32b8c39fdfe47401edc392187c3cbb1e20a2e840d214096791019653392c0d6b469caf7b471fab1d268a64 languageName: node linkType: hard @@ -34276,7 +34200,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.0.0, yaml@npm:^2.3.4, yaml@npm:^2.6.0, yaml@npm:^2.7.1": +"yaml@npm:^2.0.0, yaml@npm:^2.3.4, yaml@npm:^2.6.0, yaml@npm:^2.8.1": version: 2.8.1 resolution: "yaml@npm:2.8.1" bin: @@ -34403,10 +34327,10 @@ __metadata: languageName: node linkType: hard -"yoctocolors-cjs@npm:^2.1.2": - version: 2.1.2 - resolution: "yoctocolors-cjs@npm:2.1.2" - checksum: 10/d731e3ba776a0ee19021d909787942933a6c2eafb2bbe85541f0c59aa5c7d475ce86fcb860d5803105e32244c3dd5ba875b87c4c6bf2d6f297da416aa54e556f +"yoctocolors-cjs@npm:^2.1.2, yoctocolors-cjs@npm:^2.1.3": + version: 2.1.3 + resolution: "yoctocolors-cjs@npm:2.1.3" + checksum: 10/b2144b38807673a4254dae06fe1a212729550609e606289c305e45c585b36fab1dbba44fe6cde90db9b28be465ec63f4c2a50867aeec6672f6bc36b6c9a361a0 languageName: node linkType: hard From fe367b570be1a97c7361e417789ce8630683a957 Mon Sep 17 00:00:00 2001 From: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> Date: Mon, 24 Nov 2025 23:08:02 +0100 Subject: [PATCH 088/378] Alerting: Align OpenAPI definition with notification settings header (#114363) fix: Align OpenAPI definition with notification settings header Signed-off-by: Alexander Soelberg Heidarsson <89837986+alex5517@users.noreply.github.com> --- .../src/clients/rtkq/legacy/endpoints.gen.ts | 8 ++-- pkg/services/ngalert/api/tooling/api.json | 42 ++++++++++++++++++- .../definitions/convert_prometheus_api.go | 2 +- pkg/services/ngalert/api/tooling/post.json | 42 ++++++++++++++++++- pkg/services/ngalert/api/tooling/spec.json | 42 ++++++++++++++++++- public/api-merged.json | 42 ++++++++++++++++++- public/openapi3.json | 42 ++++++++++++++++++- 7 files changed, 205 insertions(+), 15 deletions(-) diff --git a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts index 058aa2b057c..8d2e821668b 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts @@ -555,7 +555,7 @@ const injectedRtkApi = api 'x-grafana-alerting-alert-rules-paused': queryArg['x-grafana-alerting-alert-rules-paused'], 'x-grafana-alerting-target-datasource-uid': queryArg['x-grafana-alerting-target-datasource-uid'], 'x-grafana-alerting-folder-uid': queryArg['x-grafana-alerting-folder-uid'], - 'x-grafana-alerting-notification-receiver': queryArg['x-grafana-alerting-notification-receiver'], + 'x-grafana-alerting-notification-settings': queryArg['x-grafana-alerting-notification-settings'], }, }), invalidatesTags: ['convert_prometheus'], @@ -622,7 +622,7 @@ const injectedRtkApi = api 'x-grafana-alerting-alert-rules-paused': queryArg['x-grafana-alerting-alert-rules-paused'], 'x-grafana-alerting-target-datasource-uid': queryArg['x-grafana-alerting-target-datasource-uid'], 'x-grafana-alerting-folder-uid': queryArg['x-grafana-alerting-folder-uid'], - 'x-grafana-alerting-notification-receiver': queryArg['x-grafana-alerting-notification-receiver'], + 'x-grafana-alerting-notification-settings': queryArg['x-grafana-alerting-notification-settings'], }, }), invalidatesTags: ['convert_prometheus'], @@ -2498,7 +2498,7 @@ export type RouteConvertPrometheusCortexPostRuleGroupApiArg = { 'x-grafana-alerting-alert-rules-paused'?: boolean; 'x-grafana-alerting-target-datasource-uid'?: string; 'x-grafana-alerting-folder-uid'?: string; - 'x-grafana-alerting-notification-receiver'?: string; + 'x-grafana-alerting-notification-settings'?: string; prometheusRuleGroup: PrometheusRuleGroup; }; export type RouteConvertPrometheusCortexDeleteRuleGroupApiResponse = @@ -2535,7 +2535,7 @@ export type RouteConvertPrometheusPostRuleGroupApiArg = { 'x-grafana-alerting-alert-rules-paused'?: boolean; 'x-grafana-alerting-target-datasource-uid'?: string; 'x-grafana-alerting-folder-uid'?: string; - 'x-grafana-alerting-notification-receiver'?: string; + 'x-grafana-alerting-notification-settings'?: string; prometheusRuleGroup: PrometheusRuleGroup; }; export type RouteConvertPrometheusDeleteRuleGroupApiResponse = diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 1a003d00365..af37efc58d7 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -1707,6 +1707,10 @@ "is_paused": { "type": "boolean" }, + "message": { + "description": "Field is only populated when listing alert rule versions.", + "type": "string" + }, "metadata": { "$ref": "#/definitions/AlertRuleMetadata" }, @@ -2544,6 +2548,28 @@ "TLSConfig": { "$ref": "#/definitions/TLSConfig" }, + "audience": { + "description": "Audience optionally specifies the intended audience of the\nrequest. If empty, the value of TokenURL is used as the\nintended audience. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "string" + }, + "claims": { + "additionalProperties": {}, + "description": "Claims is a map of claims to be added to the JWT token. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "object" + }, + "client_certificate_key": { + "$ref": "#/definitions/Secret" + }, + "client_certificate_key_file": { + "type": "string" + }, + "client_certificate_key_id": { + "type": "string" + }, + "client_certificate_key_ref": { + "description": "ClientCertificateKeyRef is the name of the secret within the secret manager to use as the client\nsecret.", + "type": "string" + }, "client_id": { "type": "string" }, @@ -2563,6 +2589,14 @@ }, "type": "object" }, + "grant_type": { + "description": "GrantType is the OAuth2 grant type to use. It can be one of\n\"client_credentials\" or \"urn:ietf:params:oauth:grant-type:jwt-bearer\" (RFC 7523).\nDefault value is \"client_credentials\"", + "type": "string" + }, + "iss": { + "description": "Iss is the OAuth client identifier used when communicating with\nthe configured OAuth provider. Default value is client_id. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "string" + }, "no_proxy": { "description": "NoProxy contains addresses that should not use a proxy.", "type": "string" @@ -2583,6 +2617,10 @@ }, "type": "array" }, + "signature_algorithm": { + "description": "SignatureAlgorithm is the RSA algorithm used to sign JWT token. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".\nDefault value is RS256 and valid values RS256, RS384, RS512", + "type": "string" + }, "token_url": { "type": "string" } @@ -5781,7 +5819,7 @@ }, { "in": "header", - "name": "x-grafana-alerting-notification-receiver", + "name": "x-grafana-alerting-notification-settings", "type": "string" }, { @@ -6071,7 +6109,7 @@ }, { "in": "header", - "name": "x-grafana-alerting-notification-receiver", + "name": "x-grafana-alerting-notification-settings", "type": "string" }, { diff --git a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go index b3c2db6fe7a..3b672ad6881 100644 --- a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go +++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go @@ -259,7 +259,7 @@ type RouteConvertPrometheusPostRuleGroupParams struct { // in: header FolderUID string `json:"x-grafana-alerting-folder-uid"` // in: header - NotificationReceiver string `json:"x-grafana-alerting-notification-receiver"` + NotificationSettings string `json:"x-grafana-alerting-notification-settings"` // in:body Body PrometheusRuleGroup } diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index a73d76c2151..d9d38ce03b0 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -1707,6 +1707,10 @@ "is_paused": { "type": "boolean" }, + "message": { + "description": "Field is only populated when listing alert rule versions.", + "type": "string" + }, "metadata": { "$ref": "#/definitions/AlertRuleMetadata" }, @@ -2544,6 +2548,28 @@ "TLSConfig": { "$ref": "#/definitions/TLSConfig" }, + "audience": { + "description": "Audience optionally specifies the intended audience of the\nrequest. If empty, the value of TokenURL is used as the\nintended audience. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "string" + }, + "claims": { + "additionalProperties": {}, + "description": "Claims is a map of claims to be added to the JWT token. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "object" + }, + "client_certificate_key": { + "$ref": "#/definitions/Secret" + }, + "client_certificate_key_file": { + "type": "string" + }, + "client_certificate_key_id": { + "type": "string" + }, + "client_certificate_key_ref": { + "description": "ClientCertificateKeyRef is the name of the secret within the secret manager to use as the client\nsecret.", + "type": "string" + }, "client_id": { "type": "string" }, @@ -2563,6 +2589,14 @@ }, "type": "object" }, + "grant_type": { + "description": "GrantType is the OAuth2 grant type to use. It can be one of\n\"client_credentials\" or \"urn:ietf:params:oauth:grant-type:jwt-bearer\" (RFC 7523).\nDefault value is \"client_credentials\"", + "type": "string" + }, + "iss": { + "description": "Iss is the OAuth client identifier used when communicating with\nthe configured OAuth provider. Default value is client_id. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "string" + }, "no_proxy": { "description": "NoProxy contains addresses that should not use a proxy.", "type": "string" @@ -2583,6 +2617,10 @@ }, "type": "array" }, + "signature_algorithm": { + "description": "SignatureAlgorithm is the RSA algorithm used to sign JWT token. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".\nDefault value is RS256 and valid values RS256, RS384, RS512", + "type": "string" + }, "token_url": { "type": "string" } @@ -6834,7 +6872,7 @@ }, { "in": "header", - "name": "x-grafana-alerting-notification-receiver", + "name": "x-grafana-alerting-notification-settings", "type": "string" }, { @@ -7241,7 +7279,7 @@ }, { "in": "header", - "name": "x-grafana-alerting-notification-receiver", + "name": "x-grafana-alerting-notification-settings", "type": "string" }, { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index b590bb3edd7..adb65e92ad9 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1220,7 +1220,7 @@ }, { "type": "string", - "name": "x-grafana-alerting-notification-receiver", + "name": "x-grafana-alerting-notification-settings", "in": "header" }, { @@ -1634,7 +1634,7 @@ }, { "type": "string", - "name": "x-grafana-alerting-notification-receiver", + "name": "x-grafana-alerting-notification-settings", "in": "header" }, { @@ -6124,6 +6124,10 @@ "is_paused": { "type": "boolean" }, + "message": { + "description": "Field is only populated when listing alert rule versions.", + "type": "string" + }, "metadata": { "$ref": "#/definitions/AlertRuleMetadata" }, @@ -6963,6 +6967,28 @@ "TLSConfig": { "$ref": "#/definitions/TLSConfig" }, + "audience": { + "description": "Audience optionally specifies the intended audience of the\nrequest. If empty, the value of TokenURL is used as the\nintended audience. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "string" + }, + "claims": { + "description": "Claims is a map of claims to be added to the JWT token. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "object", + "additionalProperties": {} + }, + "client_certificate_key": { + "$ref": "#/definitions/Secret" + }, + "client_certificate_key_file": { + "type": "string" + }, + "client_certificate_key_id": { + "type": "string" + }, + "client_certificate_key_ref": { + "description": "ClientCertificateKeyRef is the name of the secret within the secret manager to use as the client\nsecret.", + "type": "string" + }, "client_id": { "type": "string" }, @@ -6982,6 +7008,14 @@ "type": "string" } }, + "grant_type": { + "description": "GrantType is the OAuth2 grant type to use. It can be one of\n\"client_credentials\" or \"urn:ietf:params:oauth:grant-type:jwt-bearer\" (RFC 7523).\nDefault value is \"client_credentials\"", + "type": "string" + }, + "iss": { + "description": "Iss is the OAuth client identifier used when communicating with\nthe configured OAuth provider. Default value is client_id. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "string" + }, "no_proxy": { "description": "NoProxy contains addresses that should not use a proxy.", "type": "string" @@ -7002,6 +7036,10 @@ "type": "string" } }, + "signature_algorithm": { + "description": "SignatureAlgorithm is the RSA algorithm used to sign JWT token. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".\nDefault value is RS256 and valid values RS256, RS384, RS512", + "type": "string" + }, "token_url": { "type": "string" } diff --git a/public/api-merged.json b/public/api-merged.json index 589d1f2a222..0c977043122 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -2885,7 +2885,7 @@ }, { "type": "string", - "name": "x-grafana-alerting-notification-receiver", + "name": "x-grafana-alerting-notification-settings", "in": "header" }, { @@ -3175,7 +3175,7 @@ }, { "type": "string", - "name": "x-grafana-alerting-notification-receiver", + "name": "x-grafana-alerting-notification-settings", "in": "header" }, { @@ -16720,6 +16720,10 @@ "is_paused": { "type": "boolean" }, + "message": { + "description": "Field is only populated when listing alert rule versions.", + "type": "string" + }, "metadata": { "$ref": "#/definitions/AlertRuleMetadata" }, @@ -18273,6 +18277,28 @@ "TLSConfig": { "$ref": "#/definitions/TLSConfig" }, + "audience": { + "description": "Audience optionally specifies the intended audience of the\nrequest. If empty, the value of TokenURL is used as the\nintended audience. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "string" + }, + "claims": { + "description": "Claims is a map of claims to be added to the JWT token. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "object", + "additionalProperties": {} + }, + "client_certificate_key": { + "$ref": "#/definitions/Secret" + }, + "client_certificate_key_file": { + "type": "string" + }, + "client_certificate_key_id": { + "type": "string" + }, + "client_certificate_key_ref": { + "description": "ClientCertificateKeyRef is the name of the secret within the secret manager to use as the client\nsecret.", + "type": "string" + }, "client_id": { "type": "string" }, @@ -18292,6 +18318,14 @@ "type": "string" } }, + "grant_type": { + "description": "GrantType is the OAuth2 grant type to use. It can be one of\n\"client_credentials\" or \"urn:ietf:params:oauth:grant-type:jwt-bearer\" (RFC 7523).\nDefault value is \"client_credentials\"", + "type": "string" + }, + "iss": { + "description": "Iss is the OAuth client identifier used when communicating with\nthe configured OAuth provider. Default value is client_id. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "string" + }, "no_proxy": { "description": "NoProxy contains addresses that should not use a proxy.", "type": "string" @@ -18312,6 +18346,10 @@ "type": "string" } }, + "signature_algorithm": { + "description": "SignatureAlgorithm is the RSA algorithm used to sign JWT token. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".\nDefault value is RS256 and valid values RS256, RS384, RS512", + "type": "string" + }, "token_url": { "type": "string" } diff --git a/public/openapi3.json b/public/openapi3.json index 706b89abb32..00336f4b49f 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -6207,6 +6207,10 @@ "is_paused": { "type": "boolean" }, + "message": { + "description": "Field is only populated when listing alert rule versions.", + "type": "string" + }, "metadata": { "$ref": "#/components/schemas/AlertRuleMetadata" }, @@ -7759,6 +7763,28 @@ "TLSConfig": { "$ref": "#/components/schemas/TLSConfig" }, + "audience": { + "description": "Audience optionally specifies the intended audience of the\nrequest. If empty, the value of TokenURL is used as the\nintended audience. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "string" + }, + "claims": { + "additionalProperties": {}, + "description": "Claims is a map of claims to be added to the JWT token. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "object" + }, + "client_certificate_key": { + "$ref": "#/components/schemas/Secret" + }, + "client_certificate_key_file": { + "type": "string" + }, + "client_certificate_key_id": { + "type": "string" + }, + "client_certificate_key_ref": { + "description": "ClientCertificateKeyRef is the name of the secret within the secret manager to use as the client\nsecret.", + "type": "string" + }, "client_id": { "type": "string" }, @@ -7778,6 +7804,14 @@ }, "type": "object" }, + "grant_type": { + "description": "GrantType is the OAuth2 grant type to use. It can be one of\n\"client_credentials\" or \"urn:ietf:params:oauth:grant-type:jwt-bearer\" (RFC 7523).\nDefault value is \"client_credentials\"", + "type": "string" + }, + "iss": { + "description": "Iss is the OAuth client identifier used when communicating with\nthe configured OAuth provider. Default value is client_id. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".", + "type": "string" + }, "no_proxy": { "description": "NoProxy contains addresses that should not use a proxy.", "type": "string" @@ -7798,6 +7832,10 @@ }, "type": "array" }, + "signature_algorithm": { + "description": "SignatureAlgorithm is the RSA algorithm used to sign JWT token. Only used if\nGrantType is set to \"urn:ietf:params:oauth:grant-type:jwt-bearer\".\nDefault value is RS256 and valid values RS256, RS384, RS512", + "type": "string" + }, "token_url": { "type": "string" } @@ -17019,7 +17057,7 @@ }, { "in": "header", - "name": "x-grafana-alerting-notification-receiver", + "name": "x-grafana-alerting-notification-settings", "schema": { "type": "string" } @@ -17378,7 +17416,7 @@ }, { "in": "header", - "name": "x-grafana-alerting-notification-receiver", + "name": "x-grafana-alerting-notification-settings", "schema": { "type": "string" } From fb05096d4809a7741385ec5d6f21188718b809a0 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 00:41:15 +0000 Subject: [PATCH 089/378] I18n: Download translations from Crowdin (#114379) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../src/locales/cs-CZ/grafana-prometheus.json | 2 +- .../src/locales/de-DE/grafana-prometheus.json | 2 +- .../src/locales/es-ES/grafana-prometheus.json | 2 +- .../src/locales/fr-FR/grafana-prometheus.json | 2 +- .../src/locales/hu-HU/grafana-prometheus.json | 2 +- .../src/locales/id-ID/grafana-prometheus.json | 2 +- .../src/locales/it-IT/grafana-prometheus.json | 2 +- .../src/locales/ja-JP/grafana-prometheus.json | 2 +- .../src/locales/ko-KR/grafana-prometheus.json | 2 +- .../src/locales/nl-NL/grafana-prometheus.json | 2 +- .../src/locales/pl-PL/grafana-prometheus.json | 2 +- .../src/locales/pt-BR/grafana-prometheus.json | 2 +- .../src/locales/pt-PT/grafana-prometheus.json | 2 +- .../src/locales/ru-RU/grafana-prometheus.json | 2 +- .../src/locales/sv-SE/grafana-prometheus.json | 2 +- .../src/locales/tr-TR/grafana-prometheus.json | 2 +- .../locales/zh-Hans/grafana-prometheus.json | 2 +- .../locales/zh-Hant/grafana-prometheus.json | 2 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../grafana-azure-monitor-datasource.json | 4 +- .../datasource/mssql/locales/cs-CZ/mssql.json | 4 +- .../datasource/mssql/locales/de-DE/mssql.json | 4 +- .../datasource/mssql/locales/es-ES/mssql.json | 4 +- .../datasource/mssql/locales/fr-FR/mssql.json | 4 +- .../datasource/mssql/locales/hu-HU/mssql.json | 4 +- .../datasource/mssql/locales/id-ID/mssql.json | 4 +- .../datasource/mssql/locales/it-IT/mssql.json | 4 +- .../datasource/mssql/locales/ja-JP/mssql.json | 4 +- .../datasource/mssql/locales/ko-KR/mssql.json | 4 +- .../datasource/mssql/locales/nl-NL/mssql.json | 4 +- .../datasource/mssql/locales/pl-PL/mssql.json | 4 +- .../datasource/mssql/locales/pt-BR/mssql.json | 4 +- .../datasource/mssql/locales/pt-PT/mssql.json | 4 +- .../datasource/mssql/locales/ru-RU/mssql.json | 4 +- .../datasource/mssql/locales/sv-SE/mssql.json | 4 +- .../datasource/mssql/locales/tr-TR/mssql.json | 4 +- .../mssql/locales/zh-Hans/mssql.json | 4 +- .../mssql/locales/zh-Hant/mssql.json | 4 +- public/locales/cs-CZ/grafana.json | 41 ++++++++++++------- public/locales/de-DE/grafana.json | 41 ++++++++++++------- public/locales/es-ES/grafana.json | 41 ++++++++++++------- public/locales/fr-FR/grafana.json | 41 ++++++++++++------- public/locales/hu-HU/grafana.json | 41 ++++++++++++------- public/locales/id-ID/grafana.json | 41 ++++++++++++------- public/locales/it-IT/grafana.json | 41 ++++++++++++------- public/locales/ja-JP/grafana.json | 41 ++++++++++++------- public/locales/ko-KR/grafana.json | 41 ++++++++++++------- public/locales/nl-NL/grafana.json | 41 ++++++++++++------- public/locales/pl-PL/grafana.json | 41 ++++++++++++------- public/locales/pt-BR/grafana.json | 41 ++++++++++++------- public/locales/pt-PT/grafana.json | 41 ++++++++++++------- public/locales/ru-RU/grafana.json | 41 ++++++++++++------- public/locales/sv-SE/grafana.json | 41 ++++++++++++------- public/locales/tr-TR/grafana.json | 41 ++++++++++++------- public/locales/zh-Hans/grafana.json | 41 ++++++++++++------- public/locales/zh-Hant/grafana.json | 41 ++++++++++++------- 72 files changed, 576 insertions(+), 342 deletions(-) diff --git a/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json index 2379436aa18..d266dc1b4d9 100644 --- a/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Interval scrapingu", "label-series-limit": "Limit řady", "label-use-series-endpoint": "Použít koncový bod řady", - "more-info": "", + "more-info": "Další informace o konfiguraci typu a verze Prometheus ve zdrojích dat najdete v <2>dokumentaci k zajišťování.", "placeholder-example-maxsourceresolutionmtimeout": "Příklad: {{example}}", "title-interval-behaviour": "Chování intervalu", "title-other": "Ostatní", diff --git a/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json index 3b63c9cfc00..bc98703299a 100644 --- a/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Scrape-Intervall", "label-series-limit": "Reihengrenze", "label-use-series-endpoint": "Reihenendpunkt verwenden", - "more-info": "", + "more-info": "Weitere Informationen zur Konfiguration des Typs und der Version von Prometheus in Datenquellen finden Sie in der <2>Bereitstellungsdokumentation.", "placeholder-example-maxsourceresolutionmtimeout": "Beispiel: {{example}}", "title-interval-behaviour": "Intervallverhalten", "title-other": "Sonstiges", diff --git a/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json index 80739214809..076b607d879 100644 --- a/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Intervalo de recuperación de datos", "label-series-limit": "Límite de series", "label-use-series-endpoint": "Utilizar punto de conexión de la serie", - "more-info": "", + "more-info": "Para obtener más información sobre cómo configurar el tipo y la versión de Prometheus en las fuentes de datos, consulte la <2>documentación de aprovisionamiento.", "placeholder-example-maxsourceresolutionmtimeout": "Ejemplo: {{example}}", "title-interval-behaviour": "Comportamiento del intervalo", "title-other": "Otro", diff --git a/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json index 86169b30725..f9e81ff13f8 100644 --- a/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Intervalle de scraping", "label-series-limit": "Limite de séries", "label-use-series-endpoint": "Utiliser le point de terminaison de la série", - "more-info": "", + "more-info": "Pour en savoir plus sur la configuration du type et de la version de Prometheus dans les sources de données, consultez la <2>documentation sur le provisionnement.", "placeholder-example-maxsourceresolutionmtimeout": "Exemple : {{example}}", "title-interval-behaviour": "Comportement de l’intervalle", "title-other": "Autre", diff --git a/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json index bceeabefdc1..5a3a67a6bc0 100644 --- a/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Adatgyűjtési intervallum", "label-series-limit": "Sorozatkorlát", "label-use-series-endpoint": "Sorozat végpontjának használata", - "more-info": "", + "more-info": "A Prometheus-típus és -verzió adatforrásokban történő konfigurálásával kapcsolatos további információkért tekintse meg a <2>kiépítési dokumentációt.", "placeholder-example-maxsourceresolutionmtimeout": "Példa: {{example}}", "title-interval-behaviour": "Intervallum viselkedése", "title-other": "Egyéb", diff --git a/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json index 74b9ed50d74..de3d66304f9 100644 --- a/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Interval scrape", "label-series-limit": "Batas data seri", "label-use-series-endpoint": "Gunakan titik akhir data seri", - "more-info": "", + "more-info": "Untuk informasi lengkap tentang mengonfigurasi jenis dan versi prometheus dalam sumber data, lihat <2>dokumentasi penyediaan.", "placeholder-example-maxsourceresolutionmtimeout": "Contoh: {{example}}", "title-interval-behaviour": "Perilaku interval", "title-other": "Lainnya", diff --git a/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json index f3cf622ddd1..2e6509fdb30 100644 --- a/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Intervallo di raccolta", "label-series-limit": "Limite della serie", "label-use-series-endpoint": "Usa endpoint della serie", - "more-info": "", + "more-info": "Per ulteriori informazioni sulla configurazione del tipo e della versione di Prometheus nelle origini dati, consulta la <2>documentazione sul provisioning.", "placeholder-example-maxsourceresolutionmtimeout": "Esempio: {{example}}", "title-interval-behaviour": "Comportamento intervallo", "title-other": "Altro", diff --git a/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json index 8f6fcef6ab6..6642c9ba08e 100644 --- a/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "スクレイプ間隔", "label-series-limit": "系列制限", "label-use-series-endpoint": "系列エンドポイントを使用", - "more-info": "", + "more-info": "データソースでのPrometheusタイプとバージョンの設定の詳細については、<2>プロビジョニングドキュメントをご覧ください。", "placeholder-example-maxsourceresolutionmtimeout": "例: {{example}}", "title-interval-behaviour": "間隔の動作", "title-other": "その他", diff --git a/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json index 60576a8c7b6..03050db9014 100644 --- a/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "스크레이핑 간격", "label-series-limit": "계열 한도", "label-use-series-endpoint": "계열 엔드포인트 사용", - "more-info": "", + "more-info": "데이터 소스에서 prometheus 유형 및 버전을 구성하는 방법에 대한 자세한 내용은 <2>프로비저닝 문서를 참조하시기 바랍니다.", "placeholder-example-maxsourceresolutionmtimeout": "예: {{example}} ", "title-interval-behaviour": "간격 동작", "title-other": "기타", diff --git a/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json index feaca3639d9..721e72812f7 100644 --- a/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Scrape-interval", "label-series-limit": "Reekslimiet", "label-use-series-endpoint": "Serie-eindpunt gebruiken", - "more-info": "", + "more-info": "Zie de <2>provisioning-documentatie voor meer informatie over het configureren van het Prometheus-type en de versie in gegevensbronnen.", "placeholder-example-maxsourceresolutionmtimeout": "Voorbeeld: {{example}}", "title-interval-behaviour": "Intervalgedrag", "title-other": "Overige", diff --git a/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json index f0b3e694062..d5c06e28cfa 100644 --- a/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Odstęp czasowy pobierania danych", "label-series-limit": "Limit szeregu", "label-use-series-endpoint": "Użyj punktu końcowego szeregu", - "more-info": "", + "more-info": "Więcej informacji na temat konfigurowania typu i wersji systemu Prometheus w źródłach danych można znaleźć w <2>dokumentach dotyczących aprowizacji.", "placeholder-example-maxsourceresolutionmtimeout": "Przykład: {{example}}", "title-interval-behaviour": "Sposób działania odstępu czasu", "title-other": "Inne", diff --git a/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json index 07598624e45..8e54e6f7aa9 100644 --- a/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Intervalo de coleta", "label-series-limit": "Limite de série", "label-use-series-endpoint": "Usar endpoint da série", - "more-info": "", + "more-info": "Para mais informações sobre como configurar o tipo e a versão do Prometheus em fontes de dados, consulte a <2>documentação de provisionamento.", "placeholder-example-maxsourceresolutionmtimeout": "Exemplo: {{example}}", "title-interval-behaviour": "Comportamento de intervalo", "title-other": "Outro", diff --git a/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json index eb90445ed3e..6cec7bdbf3c 100644 --- a/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Intervalo de raspagem", "label-series-limit": "Limite de série", "label-use-series-endpoint": "Utilizar ponto final da série", - "more-info": "", + "more-info": "Para obter mais informações sobre como configurar o tipo e a versão do Prometheus em origens de dados, consulte a <2>documentação de aprovisionamento.", "placeholder-example-maxsourceresolutionmtimeout": "Exemplo: {{example}}", "title-interval-behaviour": "Comportamento de intervalo", "title-other": "Outro", diff --git a/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json index e4ff771a093..ee2ffbaa801 100644 --- a/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Интервал опроса", "label-series-limit": "Лимит рядов", "label-use-series-endpoint": "Использовать конечную точку ряда", - "more-info": "", + "more-info": "Более подробную информацию о настройке типа и версии Prometheus в источниках данных см. в <2>документации по подготовке.", "placeholder-example-maxsourceresolutionmtimeout": "Пример: {{example}}", "title-interval-behaviour": "Поведение интервалов", "title-other": "Прочее", diff --git a/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json index 8132d631384..ee4865d5b93 100644 --- a/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Skrapningsintervall", "label-series-limit": "Seriegräns", "label-use-series-endpoint": "Använd serieändpunkt", - "more-info": "", + "more-info": "För mer information om hur du konfigurerar prometheustyp och -version i datakällor, se <2>provisioneringsdokumentationen.", "placeholder-example-maxsourceresolutionmtimeout": "Exempel: {{example}}", "title-interval-behaviour": "Intervallbeteende", "title-other": "Annat", diff --git a/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json index 4b918d3c61d..cd3e0b6505f 100644 --- a/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "Kazıma aralığı", "label-series-limit": "Seri sınırı", "label-use-series-endpoint": "Seri uç noktasını kullan", - "more-info": "", + "more-info": "Prometheus türü ve sürümünü veri kaynaklarında yapılandırma hakkında daha fazla bilgi için <2>sağlama belgelerine bakın.", "placeholder-example-maxsourceresolutionmtimeout": "Örnek: {{example}}", "title-interval-behaviour": "Aralık davranışı", "title-other": "Diğer", diff --git a/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json index b8f110e9226..d93eaac9cad 100644 --- a/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "抓取间隔", "label-series-limit": "序列限制", "label-use-series-endpoint": "使用序列端点", - "more-info": "", + "more-info": "有关在数据源中配置 prometheus 类型和版本的更多信息,请参阅<2>预配文档。", "placeholder-example-maxsourceresolutionmtimeout": "示例:{{example}}", "title-interval-behaviour": "间隔行为", "title-other": "其他", diff --git a/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json index c9b5b8bdd99..3598db5f9f3 100644 --- a/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json @@ -175,7 +175,7 @@ "label-scrape-interval": "抓取間隔", "label-series-limit": "序列限制", "label-use-series-endpoint": "使用序列端點", - "more-info": "", + "more-info": "如需更多在資料來源中設定 Prometheus 類型與版本的相關資訊,請參閱<2>佈建文件。", "placeholder-example-maxsourceresolutionmtimeout": "範例:{{example}}", "title-interval-behaviour": "間隔行為", "title-other": "其他", diff --git a/public/app/plugins/datasource/azuremonitor/locales/cs-CZ/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/cs-CZ/grafana-azure-monitor-datasource.json index c894f2b6066..f81e1e00549 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/cs-CZ/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/cs-CZ/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Předplatné", "placeholder-resource-name": "název", "tooltip-region": "Oblast kódu zdroje. Volitelné pro jeden zdroj, ale povinné při výběru více zdrojů.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Ručně upravte <2>identifikátor URI zdroje. Podporuje použití více proměnných šablony (např.: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Funkce agregování", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Základní protokoly", - "description-basic-logs": "", + "description-basic-logs": "Povolení této funkce zapříčiní vznik nákladů Azure Monitor na dotaz na panelech nástěnek, které dotazují tabulky nakonfigurované pro <2>základní protokoly.", "label-enable-basic-logs": "Povolit základní protokoly" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/de-DE/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/de-DE/grafana-azure-monitor-datasource.json index 9c4d21c9645..180815453db 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/de-DE/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/de-DE/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Abonnement", "placeholder-resource-name": "Name", "tooltip-region": "Die Code-Region der Ressource. Optional für eine Ressource, aber zwingend notwendig bei der Auswahl mehrerer Ressourcen.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Bearbeiten Sie die <2>Ressourcen-URI manuell. Unterstützt die Verwendung mehrerer Vorlagenvariablen (Beispiel: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Aggregatfunktion", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Basis-Logs", - "description-basic-logs": "", + "description-basic-logs": "Wenn Sie diese Funktion aktivieren, fallen pro Abfrage Azure Monitor-Kosten für Dashboard-Panels an, die Tabellen abfragen, die für <2>Basis-Logs konfiguriert sind.", "label-enable-basic-logs": "Basis-Logs aktivieren" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/es-ES/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/es-ES/grafana-azure-monitor-datasource.json index 34239be4c38..dc8f0cd70b8 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/es-ES/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/es-ES/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Suscripción", "placeholder-resource-name": "nombre", "tooltip-region": "La región de código del recurso. Opcional para un recurso, pero obligatorio al seleccionar varios.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Edite manualmente el <2>URI del recurso. Admite el uso de múltiples variables de plantilla (por ejemplo: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Función de agregación", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Logs básicos", - "description-basic-logs": "", + "description-basic-logs": "Habilitar esta función conlleva costes por consulta de Azure Monitor en los paneles del dashboard que consultan tablas configuradas para <2>logs básicos.", "label-enable-basic-logs": "Habilitar logs básicos" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/fr-FR/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/fr-FR/grafana-azure-monitor-datasource.json index d5db7927d85..60617f22833 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/fr-FR/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/fr-FR/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Abonnement", "placeholder-resource-name": "nom", "tooltip-region": "La région de code de la ressource. Facultatif pour une ressource, mais obligatoire lors de la sélection de plusieurs ressources.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Modifiez manuellement l’<2>URI de la ressource. Prend en charge l’utilisation de plusieurs variables de modèle (par exemple : /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Fonction d’agrégation", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Journaux de base", - "description-basic-logs": "", + "description-basic-logs": "L’activation de cette fonctionnalité entraîne des coûts Azure Monitor par requête sur les panneaux du tableau de bord qui interrogent les tables configurées pour les <2>journaux de base.", "label-enable-basic-logs": "Activer les journaux de base" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/hu-HU/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/hu-HU/grafana-azure-monitor-datasource.json index 41afe3e235e..618f41bd45e 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/hu-HU/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/hu-HU/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Előfizetés", "placeholder-resource-name": "név", "tooltip-region": "Az erőforrás kódterülete. Egy erőforrás esetén nem kötelező, de több erőforrás kiválasztásakor kötelező.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Az <2>erőforrás-uri manuálisan szerkesztendő. Több sablonváltozó használatát támogatja (pl.: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Összesítési függvény", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Alapvető naplók", - "description-basic-logs": "", + "description-basic-logs": "A funkció engedélyezése esetén lekérdezésenkénti Azure Monitor-költségek merülnek fel azon irányítópult-paneleken, amelyek lekérdezik az <2>Alapvető naplókhoz konfigurált táblákat.", "label-enable-basic-logs": "Alapvető naplók engedélyezése" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/id-ID/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/id-ID/grafana-azure-monitor-datasource.json index b11a7098cc6..8f8211bfc3b 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/id-ID/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/id-ID/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Berlangganan", "placeholder-resource-name": "nama", "tooltip-region": "Wilayah kode sumber daya. Opsional untuk satu sumber daya tetapi wajib saat memilih beberapa sumber daya.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Edit <2>uri sumber daya secara manual. Mendukung penggunaan beberapa variabel templat (misalnya: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Fungsi agregat", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Log Dasar", - "description-basic-logs": "", + "description-basic-logs": "Jika Anda mengaktifkan fitur ini, Azure Monitor akan dikenakan biaya per kueri untuk panel dasbor yang melakukan kueri pada tabel yang dikonfigurasi untuk <2>Log Dasar.", "label-enable-basic-logs": "Aktifkan Log Dasar" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/it-IT/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/it-IT/grafana-azure-monitor-datasource.json index 0b5ffcdac4c..82bfd1f9ab9 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/it-IT/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/it-IT/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Abbonamento", "placeholder-resource-name": "nome", "tooltip-region": "La parte di codice della risorsa. Facoltativo per una risorsa ma obbligatorio quando se ne selezionano più di una.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Modifica manualmente l'<2>URI della risorsa. Supporta l'uso di più variabili di modello (ad esempio: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Funzione aggregata", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Registri di base", - "description-basic-logs": "", + "description-basic-logs": "L'abilitazione di questa funzione comporta i costi per query di Azure Monitor sui pannelli della dashboard che eseguono query su tabelle configurate per <2>Registri di base.", "label-enable-basic-logs": "Abilita registri di base" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/ja-JP/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/ja-JP/grafana-azure-monitor-datasource.json index cdd23e444af..d225f928c4e 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/ja-JP/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/ja-JP/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "サブスクリプション", "placeholder-resource-name": "名前", "tooltip-region": "リソースのコード領域。単一のリソースの場合はオプションですが、複数のリソースを選択する場合は必須です。", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "<2>リソースURIを手動で編集します。複数のテンプレート変数の使用をサポートしています(例:/subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "集計関数", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "基本ログ", - "description-basic-logs": "", + "description-basic-logs": "この機能を有効にすると、<2>基本ログ用に設定されたテーブルをクエリするダッシュボードパネルで、Azure Monitorのクエリごとのコストが発生します。", "label-enable-basic-logs": "基本ログを有効にする" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/ko-KR/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/ko-KR/grafana-azure-monitor-datasource.json index 8b855a130bd..e067ea85c39 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/ko-KR/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/ko-KR/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "구독", "placeholder-resource-name": "이름", "tooltip-region": "리소스의 코드 리전입니다. 하나의 리소스에 대해서는 선택 사항이지만 여러 리소스를 선택할 때는 필수입니다.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "<2>리소스 URI를 수동으로 편집합니다. 여러 템플릿 변수 사용 지원(예: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "집계 함수", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "기본 로그", - "description-basic-logs": "", + "description-basic-logs": "이 기능을 활성화하면 <2>기본 로그에 구성된 테이블을 쿼리하는 대시보드 패널에서 Azure Monitor 쿼리당 비용이 발생합니다.", "label-enable-basic-logs": "기본 로그 활성화" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/nl-NL/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/nl-NL/grafana-azure-monitor-datasource.json index ad53fde6477..022a78b5928 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/nl-NL/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/nl-NL/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Abonnement", "placeholder-resource-name": "naam", "tooltip-region": "Het codegebied van de bron. Optioneel voor één bron, maar verplicht bij het selecteren van meerdere bronnen.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Bewerk de <2>bron-uri handmatig. Ondersteunt het gebruik van meerdere sjabloonvariabelen (bijv.: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Totaal van de functie", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Basislogs", - "description-basic-logs": "", + "description-basic-logs": "Als je deze functie inschakelt, worden Azure Monitor-kosten per query gemaakt op dashboardpanelen die tabellen opvragen die zijn geconfigureerd voor <2>Basislogboeken.", "label-enable-basic-logs": "Basislogboeken inschakelen" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/pl-PL/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/pl-PL/grafana-azure-monitor-datasource.json index 808baffda64..ad6621ca5b4 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/pl-PL/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/pl-PL/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Subskrypcja", "placeholder-resource-name": "nazwa", "tooltip-region": "Kod regionu zasobu. Opcjonalny w przypadku jednego zasobu, ale wymagany przy wielu zasobach.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Edytuj ręcznie <2>identyfikator URI zasobu. Obsługuje korzystanie z wielu zmiennych szablonu (np. /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Funkcja agregująca", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Podstawowe dzienniki", - "description-basic-logs": "", + "description-basic-logs": "Włączenie tej funkcji wiąże się z opłatami Azure Monitor za każde zapytanie w przypadku paneli pulpitu, które wysyłają zapytania do tabel skonfigurowanych dla <2>podstawowych dzienników.", "label-enable-basic-logs": "Włącz podstawowe dzienniki" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/pt-BR/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/pt-BR/grafana-azure-monitor-datasource.json index 232fdcb80c4..204a98090f0 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/pt-BR/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/pt-BR/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Assinatura", "placeholder-resource-name": "nome", "tooltip-region": "A região de código do recurso. Opcional para um recurso, mas obrigatório ao selecionar vários.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Edite o <2>URI do recurso manualmente. Compatível com o uso de múltiplas variáveis de modelo (por exemplo: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Função de agregação", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Logs básicos", - "description-basic-logs": "", + "description-basic-logs": "A ativação deste recurso incorre em custos por consulta do Azure Monitor em painéis que consultam tabelas configuradas para <2>logs básicos.", "label-enable-basic-logs": "Ativar logs básicos" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/pt-PT/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/pt-PT/grafana-azure-monitor-datasource.json index 16e63fa3e55..14eb8f6c4fb 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/pt-PT/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/pt-PT/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Subscrição", "placeholder-resource-name": "nome", "tooltip-region": "A região de código do recurso. Opcional para um recurso, mas obrigatório ao selecionar vários.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Edite manualmente o <2>URI do recurso. Suporta a utilização de múltiplas variáveis de modelo (p. ex: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Função agregada", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Registos básicos", - "description-basic-logs": "", + "description-basic-logs": "A ativação desta funcionalidade incorre em custos por consulta do Azure Monitor em painéis que consultam tabelas configuradas para <2>Registos básicos.", "label-enable-basic-logs": "Ativar registos básicos" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/ru-RU/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/ru-RU/grafana-azure-monitor-datasource.json index 3d3e4a2be3a..a483da23dcd 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/ru-RU/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/ru-RU/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Подписка", "placeholder-resource-name": "имя", "tooltip-region": "Область кода ресурса. Необязательно для одного ресурса, но обязательно при выборе нескольких.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Вручную измените <2>URI ресурса. Поддерживает использование нескольких переменных шаблона (например, /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Агрегатная функция", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Базовые журналы", - "description-basic-logs": "", + "description-basic-logs": "Включение этой функции влечет за собой затраты на каждый запрос Azure Monitor на панелях дашбордов, связанный с таблицами для <2>базовых журналов.", "label-enable-basic-logs": "Включить базовые журналы" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/sv-SE/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/sv-SE/grafana-azure-monitor-datasource.json index 07021e81d59..5b482198cce 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/sv-SE/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/sv-SE/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Prenumeration", "placeholder-resource-name": "namn", "tooltip-region": "Resursens kodregion. Valfritt för en enskild resurs men obligatoriskt vid val av flera.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "Redigera <2>resursens URI manuellt. Stöder användning av flera mallvariabler (t.ex.: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Aggregeringsfunktion", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Grundläggande loggar", - "description-basic-logs": "", + "description-basic-logs": "Om den här funktionen aktiveras medför det Azure Monitor-kostnader per fråga för instrumentpanelspaneler som frågar tabeller konfigurerade för <2>basloggar.", "label-enable-basic-logs": "Aktivera basloggar" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/tr-TR/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/tr-TR/grafana-azure-monitor-datasource.json index 1b46578214b..417fdfbe1bf 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/tr-TR/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/tr-TR/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "Abonelik", "placeholder-resource-name": "ad", "tooltip-region": "Kaynağın kod bölgesi. Bir kaynak için isteğe bağlıdır ancak birden fazla kaynak seçildiğinde zorunludur.", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "<2>Kaynak URI'sini manuel olarak düzenleyin. Birden fazla şablon değişkeni kullanımını destekler (örnek: /subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "Toplama işlevi", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "Temel Günlükler", - "description-basic-logs": "", + "description-basic-logs": "Bu özelliğin etkinleştirilmesi, <2>Temel Günlükler için yapılandırılmış tabloları sorgulayan pano panellerinde Azure Monitor sorgu başına maliyetlerinin oluşmasına neden olur.", "label-enable-basic-logs": "Temel Günlükleri Etkinleştir" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/zh-Hans/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/zh-Hans/grafana-azure-monitor-datasource.json index eb5e45aaa73..cfb1c8862f6 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/zh-Hans/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/zh-Hans/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "订阅", "placeholder-resource-name": "姓名", "tooltip-region": "资源的代码区域。对于一个资源来说是可选的,但在选择多个资源时则是必须的。", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "手动编辑<2>资源 uri。支持使用多个模板变量(例如:/subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "汇总功能", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "基本日志", - "description-basic-logs": "", + "description-basic-logs": "启用此功能后,在查询表配置为<2>基本日志的数据面板上,Azure Monitor 会产生每次查询的费用。", "label-enable-basic-logs": "启用基本日志" }, "config-editor": { diff --git a/public/app/plugins/datasource/azuremonitor/locales/zh-Hant/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/zh-Hant/grafana-azure-monitor-datasource.json index 29f2a139657..0b4bc313999 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/zh-Hant/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/zh-Hant/grafana-azure-monitor-datasource.json @@ -17,7 +17,7 @@ "label-subscription": "訂閱", "placeholder-resource-name": "名稱", "tooltip-region": "資源的代碼區域。對於單一資源為選填,但選取多個資源時為必填。", - "tooltip-resource-uri": "" + "tooltip-resource-uri": "手動編輯<2>資源 URI。支援使用多個範本變數(例如:/subscriptions/$subId/resourceGroups/$rg)" }, "aggregate-item": { "aria-label-aggregate-function": "彙總函式", @@ -68,7 +68,7 @@ }, "basic-logs-toggle": { "aria-label-enable-basic-logs": "基本紀錄", - "description-basic-logs": "", + "description-basic-logs": "啟用此功能會導致 Azure Monitor 在查詢為<2>基本記錄設定的表格之儀表板面板上,產生每次查詢成本。", "label-enable-basic-logs": "啟用基本紀錄" }, "config-editor": { diff --git a/public/app/plugins/datasource/mssql/locales/cs-CZ/mssql.json b/public/app/plugins/datasource/mssql/locales/cs-CZ/mssql.json index 198e7a85804..f48b613668f 100644 --- a/public/app/plugins/datasource/mssql/locales/cs-CZ/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/cs-CZ/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Pokud používáte starší verzi Microsoft SQL Server, jako je 2008 a 2008R2, možná budete muset zakázat šifrování, abyste se mohli připojit.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Dolní limit pro automatické seskupení podle časového intervalu. Doporučuje se nastavit frekvenci zápisu, například <1>{{exampleInterval}}, pokud jsou data zapisována každou minutu.", "description-tls-cert": "Cesta k souboru obsahujícímu certifikát veřejného klíče CA, který podepsal certifikát SQL Server. Nutné, když je certifikát serveru podepsán sám sebou.", "label-auth-settings": "Nastavení ověření Azure", "label-auth-type": "Typ ověření", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Uveďte, zda by měly být k vyhledání KDC a dalších serverů pro oblast použity záznamy DNS „SRV“. Výchozí hodnota je <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "Cesta ke konfiguračnímu souboru pro <2>balíček MIT krb5. Výchozí hodnota je <4>{{default}}.", "description-udp-preference-limit": "Výchozí hodnota je <1>{{default}} (vždy použít TCP) a je volitelná.", "label-dns-lookup-kdc": "DNS pro vyhledání KDC", "label-krb5-config-file-path": "Cesta ke konfiguračnímu souboru krb5", diff --git a/public/app/plugins/datasource/mssql/locales/de-DE/mssql.json b/public/app/plugins/datasource/mssql/locales/de-DE/mssql.json index b1308275e22..1a08a22f92e 100644 --- a/public/app/plugins/datasource/mssql/locales/de-DE/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/de-DE/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Wenn Sie eine ältere Version von Microsoft SQL Server wie 2008 und 2008R2 nutzen, müssen Sie möglicherweise die Verschlüsselung deaktivieren, um eine Verbindung herstellen zu können.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Eine Untergrenze für die automatische Gruppierung je nach Zeitintervall. Es wird empfohlen, die Schreibfrequenz einzustellen, zum Beispiel <1>{{exampleInterval}}, wenn Ihre Daten jede Minute geschrieben werden.", "description-tls-cert": "Pfad zu einer Datei, die das öffentliche Schlüsselzertifikat der Zertifizierungsstelle enthält, die das SQL Server-Zertifikat signiert hat. Wird benötigt, wenn es sich um ein selbstsigniertes Serverzertifikat handelt.", "label-auth-settings": "Einstellungen für Azure Authentication", "label-auth-type": "Authentifizierungstyp", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Geben Sie an, ob DNS-`SRV`-Einträge verwendet werden sollen, um die KDCs und andere Server für einen Bereich zu lokalisieren. Der Standardwert ist <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "Der Pfad zur Konfigurationsdatei für das <2>Paket MIT krb5. Der Standardwert ist <4>{{default}}.", "description-udp-preference-limit": "Der Standardwert ist <1>{{default}} und bedeutet, dass immer TCP verwendet wird. Dies ist optional.", "label-dns-lookup-kdc": "DNS-Suche KDC", "label-krb5-config-file-path": "Konfigurationsdateipfad für krb5", diff --git a/public/app/plugins/datasource/mssql/locales/es-ES/mssql.json b/public/app/plugins/datasource/mssql/locales/es-ES/mssql.json index a3beb6a86f4..44bdc0cd695 100644 --- a/public/app/plugins/datasource/mssql/locales/es-ES/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/es-ES/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Si está utilizando una versión anterior de Microsoft SQL Server, como 2008 y 2008R2, es posible que deba desactivar el cifrado para poder conectarse.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Un límite inferior para el grupo automático por intervalo de tiempo. Se recomienda configurar la frecuencia de escritura, por ejemplo<1>{{exampleInterval}}, si sus datos se escriben cada minuto.", "description-tls-cert": "Ruta al archivo que contiene el certificado de clave pública de la CA que firmó el certificado de SQL Server. Necesario cuando el certificado del servidor está autofirmado.", "label-auth-settings": "Configuración de autenticación de Azure", "label-auth-type": "Tipo de autenticación", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Indique si los registros DNS «SRV» deben utilizarse para localizar los KDC y otros servidores de un dominio. El valor predeterminado es <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "La ruta del archivo de configuración para el <2>paquete MIT krb5. El valor predeterminado es <4>{{default}}.", "description-udp-preference-limit": "El valor predeterminado es <1>{{default}} y significa que siempre se utiliza TCP y es opcional.", "label-dns-lookup-kdc": "KDC de búsqueda de DNS", "label-krb5-config-file-path": "Ruta del archivo de configuración krb5", diff --git a/public/app/plugins/datasource/mssql/locales/fr-FR/mssql.json b/public/app/plugins/datasource/mssql/locales/fr-FR/mssql.json index 8e706658691..4bbc3d03912 100644 --- a/public/app/plugins/datasource/mssql/locales/fr-FR/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/fr-FR/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Si vous utilisez une ancienne version de Microsoft SQL Server, comme 2008 et 2008R2, vous devrez peut-être désactiver le chiffrement pour pouvoir vous connecter.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Une limite inférieure pour le groupe automatique par intervalle de temps. Il est recommandé de définir la fréquence d’écriture, par exemple<1>{{exampleInterval}} si vos données sont écrites toutes les minutes.", "description-tls-cert": "Chemin d’accès au fichier contenant le certificat de clé publique de l’autorité de certification qui a signé le certificat SQL Server. Nécessaire lorsque le certificat du serveur est auto-signé.", "label-auth-settings": "Paramètres d’authentification Azure", "label-auth-type": "Type d’authentification", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Indiquez si les enregistrements DNS « SRV » doivent être utilisés pour localiser les KDC et d’autres serveurs pour un domaine. La valeur par défaut est <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "Le chemin d’accès au fichier de configuration pour le <2>paquet MIT krb5. La valeur par défaut est <4>{{default}}.", "description-udp-preference-limit": "La valeur par défaut est <1>{{default}} et signifie toujours utiliser TCP et est facultative.", "label-dns-lookup-kdc": "Recherche DNS KDC", "label-krb5-config-file-path": "chemin du fichier de configuration krb5", diff --git a/public/app/plugins/datasource/mssql/locales/hu-HU/mssql.json b/public/app/plugins/datasource/mssql/locales/hu-HU/mssql.json index d742078e2af..d2ab6b0d749 100644 --- a/public/app/plugins/datasource/mssql/locales/hu-HU/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/hu-HU/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Ha a Microsoft SQL Server régebbi verzióját használja, például a 2008-at vagy a 2008R2-t, akkor előfordulhat, hogy le kell tiltania a titkosítást, hogy csatlakozhasson.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Az automatikus csoportosítás időintervallumának alsó határa. Javasoljuk, hogy az írási gyakoriságra állítsa be, például <1>{{exampleInterval}} értékre, ha az adatait percenként írja.", "description-tls-cert": "Az SQL Server-tanúsítványt aláíró hitelesítésszolgáltató nyilvános kulcsos tanúsítványát tartalmazó fájl elérési útvonala. Akkor szükséges, ha a kiszolgáló tanúsítványa saját aláírású.", "label-auth-settings": "Azure-hitelesítési beállítások", "label-auth-type": "Hitelesítés típusa", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Jelölje meg, hogy a DNS „SRV” rekordjait kell-e használni a KDC-k és más kiszolgálók keresésére egy tartományban. Az alapértelmezett érték <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "A <2>MIT krb5 csomag konfigurációs fájljának elérési útvonala. Az alapértelmezett érték <4>{{default}}.", "description-udp-preference-limit": "Az alapértelmezett érték <1>{{default}}, ami azt jelenti, hogy mindig TCP-t használ, és opcionális.", "label-dns-lookup-kdc": "DNS-keresés – KDC", "label-krb5-config-file-path": "krb5 konfigurációs fájl útvonala", diff --git a/public/app/plugins/datasource/mssql/locales/id-ID/mssql.json b/public/app/plugins/datasource/mssql/locales/id-ID/mssql.json index e61a202f711..ea5951aff48 100644 --- a/public/app/plugins/datasource/mssql/locales/id-ID/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/id-ID/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Jika Anda menggunakan Microsoft SQL Server versi lama seperti 2008 dan 2008R2, Anda mungkin perlu menonaktifkan enkripsi untuk dapat terhubung.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Batas bawah untuk grup otomatis berdasarkan interval waktu. Direkomendasikan untuk diatur ke frekuensi tulis, misalnya <1>{{exampleInterval}} jika data Anda ditulis setiap menit.", "description-tls-cert": "Jalur ke file yang berisi sertifikat kunci publik CA yang menandatangani sertifikat SQL Server. Diperlukan ketika sertifikat server ditandatangani sendiri.", "label-auth-settings": "Pengaturan Autentikasi Azure", "label-auth-type": "Jenis Autentikasi", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Tunjukkan apakah catatan DNS 'SRV' harus digunakan untuk menemukan KDC dan server lain untuk suatu realm. Defaultnya adalah <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "Jalur ke file konfigurasi untuk <2>paket MIT krb5. Defaultnya adalah <4>{{default}}.", "description-udp-preference-limit": "Defaultnya adalah <1>{{default}} dan berarti selalu menggunakan TCP dan bersifat opsional.", "label-dns-lookup-kdc": "KDC Pencarian DNS", "label-krb5-config-file-path": "jalur file konfigurasi krb5", diff --git a/public/app/plugins/datasource/mssql/locales/it-IT/mssql.json b/public/app/plugins/datasource/mssql/locales/it-IT/mssql.json index 13291c7e524..958f8f1e732 100644 --- a/public/app/plugins/datasource/mssql/locales/it-IT/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/it-IT/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Se utilizzi una versione precedente di Microsoft SQL Server come 2008 e 2008R2, potrebbe essere necessario disabilitare la crittografia per potersi connettere.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Un limite inferiore per il raggruppamento automatico per intervallo di tempo. Si consiglia di impostare la frequenza di scrittura, ad esempio <1>{{exampleInterval}} se i dati vengono scritti ogni minuto.", "description-tls-cert": "Percorso al file contenente il certificato di chiave pubblica della CA che ha firmato il certificato di SQL Server. Necessario quando il certificato del server è autofirmato.", "label-auth-settings": "Impostazioni di autenticazione di Azure", "label-auth-type": "Tipo di autenticazione", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Indica se i record DNS \"SRV\" devono essere utilizzati per individuare i KDC e altri server per un'area di autenticazione. Il valore predefinito è <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "Il percorso al file di configurazione per il <2>pacchetto MIT krb5. Il valore predefinito è <4>{{default}}.", "description-udp-preference-limit": "Il valore predefinito è <1>{{default}} e significa che si utilizza sempre TCP ed è facoltativo.", "label-dns-lookup-kdc": "KDC di ricerca DNS", "label-krb5-config-file-path": "percorso del file di configurazione krb5", diff --git a/public/app/plugins/datasource/mssql/locales/ja-JP/mssql.json b/public/app/plugins/datasource/mssql/locales/ja-JP/mssql.json index 71971ae17ea..b806f193412 100644 --- a/public/app/plugins/datasource/mssql/locales/ja-JP/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/ja-JP/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "2008や2008R2などの古いバージョンのMicrosoft SQL Serverを使用している場合、接続できるようにするためには、暗号化を無効にする必要がある場合があります。", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "時間間隔による自動グループ化の下限。データが毎分書き込まれる場合は、書き込み頻度に合わせて<1>{{exampleInterval}}のようにに設定することをお勧めします。", "description-tls-cert": "SQL Server証明書に署名したCAの公開鍵証明書を含むファイルへのパス。サーバー証明書が自己署名されている場合に必要です。", "label-auth-settings": "Azure認証設定", "label-auth-type": "認証タイプ", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "DNS「SRV」レコードを使用して、レルムのKDCやその他のサーバーを特定するかどうかを示します。デフォルトは<1>{{default}}です。", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "<2>MIT krb5パッケージの設定ファイルへのパス。デフォルトは<4>{{default}}です。", "description-udp-preference-limit": "デフォルトは<1>{{default}}で、常にTCPを使用することを意味します。オプションです。", "label-dns-lookup-kdc": "DNS検索KDC", "label-krb5-config-file-path": "krb5設定ファイルパス", diff --git a/public/app/plugins/datasource/mssql/locales/ko-KR/mssql.json b/public/app/plugins/datasource/mssql/locales/ko-KR/mssql.json index d88f5e897f1..fbef047ba28 100644 --- a/public/app/plugins/datasource/mssql/locales/ko-KR/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/ko-KR/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "2008 및 2008R2와 같은 이전 버전의 Microsoft SQL Server를 사용하는 경우 연결을 위해 암호화를 비활성화해야 할 수 있습니다.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "시간 간격을 기준으로 자동 그룹화할 때의 하한입니다. 데이터 기록 빈도에 맞춰 설정하는 것이 좋습니다. 예를 들어, 데이터가 1분마다 기록된다면 <1>{{exampleInterval}}으로 설정하세요. ", "description-tls-cert": "SQL 서버 인증서에 서명한 CA의 공개 키 인증서가 포함된 파일의 경로입니다. 서버 인증서가 자체 서명된 경우 필요합니다.", "label-auth-settings": "Azure 인증 설정", "label-auth-type": "인증 유형", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "DNS `SRV` 레코드를 사용하여 Realm의 KDC 및 기타 서버를 찾을지 여부를 나타냅니다. 기본값은 <1>{{default}}입니다.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "<2>MIT krb5 패키지의 구성 파일에 대한 경로입니다. 기본값은 <4>{{default}}입니다.", "description-udp-preference-limit": "기본값은 <1>{{default}}(으)로 항상 TCP를 사용한다는 의미이며, 선택 사항입니다.", "label-dns-lookup-kdc": "DNS 조회 KDC", "label-krb5-config-file-path": "krb5 구성 파일 경로", diff --git a/public/app/plugins/datasource/mssql/locales/nl-NL/mssql.json b/public/app/plugins/datasource/mssql/locales/nl-NL/mssql.json index 6f5631edda2..73a500eef02 100644 --- a/public/app/plugins/datasource/mssql/locales/nl-NL/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/nl-NL/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Als je een oudere versie van Microsoft SQL Server gebruikt, zoals 2008 en 2008R2, moet je mogelijk versleuteling uitschakelen om verbinding te kunnen maken.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Een ondergrens voor de automatische groepering op tijdsinterval. Aanbevolen om in te stellen op schrijffrequentie, bijvoorbeeld <1>{{exampleInterval}} als je gegevens elke minuut worden geschreven.", "description-tls-cert": "Pad naar bestand met het publieke sleutelcertificaat van de CA die het SQL Server-certificaat heeft ondertekend. Nodig wanneer het servercertificaat zelfondertekend is.", "label-auth-settings": "Azure-verificatie-instellingen", "label-auth-type": "Authenticatietype", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Geef aan of DNS 'SRV'-records moeten worden gebruikt om de KDC's en andere servers voor een realm te lokaliseren. De standaardwaarde is <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "Het pad naar het configuratiebestand voor het <2>MIT krb5-pakket. De standaardwaarde is <4>{{default}}.", "description-udp-preference-limit": "De standaardwaarde is <1>{{default}} en betekent altijd TCP gebruiken en is optioneel.", "label-dns-lookup-kdc": "DNS Lookup KDC", "label-krb5-config-file-path": "pad naar het krb5-config-bestand", diff --git a/public/app/plugins/datasource/mssql/locales/pl-PL/mssql.json b/public/app/plugins/datasource/mssql/locales/pl-PL/mssql.json index 98329b765fb..404e2ffed33 100644 --- a/public/app/plugins/datasource/mssql/locales/pl-PL/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/pl-PL/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Jeśli używasz starszej wersji Microsoft SQL Server, takiej jak 2008 lub 2008 R2, może być konieczne wyłączenie szyfrowania w celu nawiązania połączenia.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Dolna granica automatycznego grupowania według odstępu czasu. Zalecane jest ustawienie częstotliwości zapisu, na przykład <1>{{exampleInterval}}, jeśli dane są zapisywane co minutę. ", "description-tls-cert": "Ścieżka do pliku zawierającego certyfikat klucza publicznego urzędu certyfikacji, który podpisał certyfikat SQL Server. Wymagane, gdy certyfikat serwera jest samodzielnie podpisany.", "label-auth-settings": "Ustawienia uwierzytelniania Azure", "label-auth-type": "Typ uwierzytelniania", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Określ, czy rekordy DNS „SRV” powinny być używane do lokalizowania KDC i innych serwerów dla domeny. Domyślnie jest to <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "Ścieżka do pliku konfiguracyjnego dla <2>pakietu MIT krb5. Domyślnie jest to <4>{{default}}.", "description-udp-preference-limit": "Domyślnie jest to <1>{{default}}, co oznacza „zawsze używaj TCP”. Jest to ustawienie opcjonalne.", "label-dns-lookup-kdc": "Wyszukiwanie DNS KDC", "label-krb5-config-file-path": "Ścieżka do pliku konfiguracyjnego krb5", diff --git a/public/app/plugins/datasource/mssql/locales/pt-BR/mssql.json b/public/app/plugins/datasource/mssql/locales/pt-BR/mssql.json index 2c99bffee93..7b79a793e0f 100644 --- a/public/app/plugins/datasource/mssql/locales/pt-BR/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/pt-BR/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Se você estiver usando uma versão mais antiga do Microsoft SQL Server, como 2008 e 2008R2, talvez seja necessário desativar a criptografia para poder se conectar.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Um limite inferior para o grupo automático por intervalo de tempo. Recomendamos definir a frequência de gravação — por exemplo, <1>{{exampleInterval}}, se seus dados forem gravados a cada minuto.", "description-tls-cert": "Caminho para o arquivo que contém o certificado de chave pública da CA que assinou o certificado do SQL Server. Necessário quando o certificado do servidor é autoassinado.", "label-auth-settings": "Configurações de autenticação do Azure", "label-auth-type": "Tipo de autenticação", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Indique se os registros DNS \"SRV\" devem ser usados para localizar os KDCs e outros servidores de um domínio. O padrão é <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "O caminho para o arquivo de configuração do <2>pacote MIT krb5. O padrão é <4>{{default}}.", "description-udp-preference-limit": "O valor padrão é <1>{{default}}, indicando que o TCP será usado sempre e é opcional.", "label-dns-lookup-kdc": "KDC de pesquisa de DNS", "label-krb5-config-file-path": "Caminho do arquivo de configuração krb5", diff --git a/public/app/plugins/datasource/mssql/locales/pt-PT/mssql.json b/public/app/plugins/datasource/mssql/locales/pt-PT/mssql.json index e41988ed380..0b2ef269e12 100644 --- a/public/app/plugins/datasource/mssql/locales/pt-PT/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/pt-PT/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Se estiver a utilizar uma versão mais antiga do Microsoft SQL Server, como 2008 e 2008R2, talvez tenha de desativar a encriptação para poder estabelecer ligação.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Um limite inferior para o agrupamento automático por intervalo de tempo. É recomendado que seja definido para frequência de escrita, por exemplo, <1>{{exampleInterval}} se os seus dados forem escritos a cada minuto.", "description-tls-cert": "Caminho para o ficheiro que contém o certificado de chave pública da AC que assinou o certificado do SQL Server. Necessário quando o certificado do servidor é assinado automaticamente.", "label-auth-settings": "Definições de autenticação do Azure", "label-auth-type": "Tipo de autenticação", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Indique se os registos DNS \"SRV\" devem ser utilizados para localizar os KDC e outros servidores para um âmbito. A predefinição é <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "O caminho para o ficheiro de configuração do <2>pacote MIT krb5. A predefinição é <4>{{default}}.", "description-udp-preference-limit": "A predefinição é <1>{{default}}, que significa utilizar sempre TCP, e é opcional.", "label-dns-lookup-kdc": "KDC de pesquisa de DNS", "label-krb5-config-file-path": "Caminho do ficheiro de configuração krb5", diff --git a/public/app/plugins/datasource/mssql/locales/ru-RU/mssql.json b/public/app/plugins/datasource/mssql/locales/ru-RU/mssql.json index 2a8cc95e103..0cd1eaf499f 100644 --- a/public/app/plugins/datasource/mssql/locales/ru-RU/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/ru-RU/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Если вы используете более старую версию Microsoft SQL Server, например 2008 или 2008R2, для подключения может требоваться отключение шифрования.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Нижний предел для автоматической группировки по временному интервалу. Рекомендуется установить частоту записи, например <1>{{exampleInterval}}, если ваши данные записываются каждую минуту.", "description-tls-cert": "Путь к файлу, содержащему сертификат открытого ключа центра сертификации, который подписал сертификат SQL Server. Требуется, когда сертификат сервера является самозаверяющим.", "label-auth-settings": "Параметры аутентификации Azure", "label-auth-type": "Тип аутентификации", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Укажите, следует ли использовать служебные записи DNS для поиска KDC и других серверов для области. Значение по умолчанию — <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "Путь к файлу конфигурации для <2>пакета MIT krb5. Значение по умолчанию — <4>{{default}}.", "description-udp-preference-limit": "Значение по умолчанию — <1>{{default}}, что означает всегда использовать TCP (необязательно).", "label-dns-lookup-kdc": "DNS-поиск KDC", "label-krb5-config-file-path": "Путь к файлу конфигурации krb5", diff --git a/public/app/plugins/datasource/mssql/locales/sv-SE/mssql.json b/public/app/plugins/datasource/mssql/locales/sv-SE/mssql.json index f5fad3ac672..78c4fe3450f 100644 --- a/public/app/plugins/datasource/mssql/locales/sv-SE/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/sv-SE/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Om du använder en äldre version av Microsoft SQL Server, exempelvis 2008 eller 2008R2, kan du behöva inaktivera kryptering för att kunna ansluta.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "En nedre gräns för den automatiska grupperingen efter tidsintervall. Rekommenderas att ställas in på skrivfrekvens, till exempel<1>{{exampleInterval}} om data skrivs varje minut.", "description-tls-cert": "Sökväg till fil som innehåller det offentliga nyckelcertifikatet för den certifikatutfärdare som undertecknade SQL Server-certifikatet. Behövs när servercertifikatet är självsignerat.", "label-auth-settings": "Inställningar för Azure-autentisering", "label-auth-type": "Behörighetskontrolltyp", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Ange om DNS \"SRV\"-poster ska användas för att hitta KDC:er och andra servrar för en realm. Standard är <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "Sökvägen till konfigurationsfilen för <2>MIT krb5-paketet. Standard är <4>{{default}}.", "description-udp-preference-limit": "Standard är <1>{{default}} och innebär att alltid använda TCP och är valfritt.", "label-dns-lookup-kdc": "DNS-sökning KDC", "label-krb5-config-file-path": "Sökväg till krb5-konfigurationsfil", diff --git a/public/app/plugins/datasource/mssql/locales/tr-TR/mssql.json b/public/app/plugins/datasource/mssql/locales/tr-TR/mssql.json index 585a8541064..cff53a13e08 100644 --- a/public/app/plugins/datasource/mssql/locales/tr-TR/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/tr-TR/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "Microsoft SQL Server'ın 2008 ve 2008R2 gibi eski bir sürümünü kullanıyorsanız bağlanabilmek için şifrelemeyi devre dışı bırakmanız gerekebilir.", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "Otomatik gruplama zaman aralığı için alt sınır. Verileriniz her dakika yazılıyorsa (<1>{{exampleInterval}} gibi) yazma sıklığına ayarlanması önerilir.", "description-tls-cert": "SQL Server sertifikasını imzalayan Sertifika Yetkilisinin (CA) genel anahtar sertifikasını içeren dosyanın yolu. Sunucu sertifikası kendi kendine imzalanmışsa gereklidir.", "label-auth-settings": "Azure Kimlik Doğrulama Ayarları", "label-auth-type": "Kimlik Doğrulama Türü", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "Bir alan için KDC'leri ve diğer sunucuları bulmak için DNS SRV kayıtlarının kullanılıp kullanılmayacağını belirtir. Varsayılan değer <1>{{default}}.", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "<2>MIT krb5 paketi için yapılandırma dosyasının yolu. Varsayılan değer <4>{{default}}.", "description-udp-preference-limit": "Varsayılan değer <1>{{default}} olduğu için her zaman TCP kullanılır ve isteğe bağlıdır.", "label-dns-lookup-kdc": "DNS Arama KDC", "label-krb5-config-file-path": "krb5 yapılandırma dosyasının yolu", diff --git a/public/app/plugins/datasource/mssql/locales/zh-Hans/mssql.json b/public/app/plugins/datasource/mssql/locales/zh-Hans/mssql.json index e13b1998e93..406d43f5fbe 100644 --- a/public/app/plugins/datasource/mssql/locales/zh-Hans/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/zh-Hans/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "如果您使用的是 2008 和 2008R2 等旧版本的 Microsoft SQL Server,可能需要禁用加密才能连接。", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "按时间间隔划分的自动分组下限。如果数据每分钟写入一次,建议设置为写入频率,例如<1>{{exampleInterval}}。", "description-tls-cert": "包含签署 SQL Server 证书的 CA 的公钥证书的文件路径。服务器证书为自签名时需要。", "label-auth-settings": "Azure 身份验证设置", "label-auth-type": "身份验证类型", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "指明是否应使用 DNS `SRV`记录来定位领域的 KDC 和其他服务器。默认值为 <1>{{default}}。", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "<2>MIT krb5 软件包配置文件的路径。默认值为 <4>{{default}}。", "description-udp-preference-limit": "默认值为 <1>{{default}},表示始终使用 TCP,为可选项。", "label-dns-lookup-kdc": "DNS 查询 KDC", "label-krb5-config-file-path": "krb5 配置文件路径", diff --git a/public/app/plugins/datasource/mssql/locales/zh-Hant/mssql.json b/public/app/plugins/datasource/mssql/locales/zh-Hant/mssql.json index bdcd2b1831b..bc4da26a720 100644 --- a/public/app/plugins/datasource/mssql/locales/zh-Hant/mssql.json +++ b/public/app/plugins/datasource/mssql/locales/zh-Hant/mssql.json @@ -69,7 +69,7 @@ "description-encrypt-false": "", "description-encrypt-older-version": "如果您使用的是舊版本的 Microsoft SQL Server(如 2008 和 2008R2),則可能需要停用加密才能連線。", "description-encrypt-true": "", - "description-min-interval": "", + "description-min-interval": "按時間間隔自動分組的下限。建議設定為寫入頻率,例如 <1>{{exampleInterval}},如果您的資料每分鐘寫入一次,則設定為 <1>1 分鐘。", "description-tls-cert": "包含簽署 SQL Server 憑證的 CA 之公開金鑰憑證的檔案路徑。伺服器憑證自行簽署時需要。", "label-auth-settings": "Azure 驗證設定", "label-auth-type": "身分驗證類型", @@ -104,7 +104,7 @@ }, "kerberos-advanced-settings": { "description-dns-lookup-kdc": "指明是否應使用 DNS「SRV」紀錄,來定位領域的 KDC 和其他伺服器。預設值為 <1>{{default}}。", - "description-krb5-config-file-path": "", + "description-krb5-config-file-path": "<2>MIT krb5 套件的設定檔案路徑。預設值為 <4>{{default}}。", "description-udp-preference-limit": "預設值為 <1>{{default}},表示一律使用 TCP,且為選用。", "label-dns-lookup-kdc": "DNS 查找 KDC", "label-krb5-config-file-path": "krb5 設定檔案路徑", diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 1e2530c7e6f..ac6538d3719 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -2093,7 +2093,7 @@ "warning-1": "Odstraněním těchto zásad oznamování dojde k jejich trvalému odebrání.", "warning-2": "Určitě chcete zásady odstranit?" }, - "filter-description": "", + "filter-description": "Filtrujte zásady oznamování pomocí seznamu odpovídajících osob oddělených čárkami, např.:<1>závažnost = kritická, region = EMEA", "generated-policies": "Automaticky generované zásady", "matchers": "Srovnávače", "metadata": { @@ -2832,7 +2832,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Data šablony výstrah", - "only-in-alert": "K dispozici pouze v kontextu výstrahy (např. v rámcismyčky výstrahy)" + "only-in-alert": "" }, "available-context-notification": "K dispozici v kontextu oznámení.", "notification-template-data": "Data šablony oznámení" @@ -3269,7 +3269,7 @@ "auth-settings": "Nastavení ověření" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Konfigurovat nastavení ověření. Další informace najdete v naší <2>dokumentaci." }, "auth-drawer-unconnected": { "advanced-auth": "Pokročilé ověření", @@ -3303,7 +3303,7 @@ "allowed-organizations-description": "Seznam organizací oddělených čárkou nebo mezerou. Uživatel musí být členem alespoň\njedné organizace, aby se mohl přihlásit.", "allowed-organizations-label": "Povolené organizace", "allowed-organizations-placeholder": "Zadejte organizace (my-team, myteam…) a stiskněte klávesu Enter pro přidání", - "api-url-description": "", + "api-url-description": "Koncový bod uživatelských informací vašeho poskytovatele OAuth2. Informace vrácené tímto koncovým bodem musí být kompatibilní s <2>OpenID UserInfo.", "api-url-required": "Toto pole musí být platná adresa URL, pokud je nastavena.", "auth-style-description": "Určuje způsob odesílání „{{ clientIDLabel }}“ a „{{ clientSecretLabel }}“ poskytovateli Oauth2. Výchozí hodnota je AutoDetect.", "auth-style-label": "Styl autorizace", @@ -3448,7 +3448,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Spravujte nastavení ověření a konfigurujte jednotné přihlašování. Další informace najdete v naší <2>dokumentaci." }, "bar-chart": { "warn": { @@ -4884,7 +4884,7 @@ "add-visualization-body": "Vyberte zdroj dat a poté se dotazujte a vizualizujte data pomocí grafů, statistik a tabulek nebo vytvářejte seznamy, přehledy a další widgety.", "add-visualization-button": "Přidat vizualizaci", "add-visualization-header": "Začněte novou nástěnku přidáním vizualizace", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Importujte nástěnky ze souborů nebo z <2>grafana.com.", "import-a-dashboard-header": "Importovat nástěnku", "import-dashboard-button": "Importovat nástěnku", "show-less-dashboards": "", @@ -6671,7 +6671,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Klikněte <2>tady a zjistěte víc o této chybě.", - "success-more-details-links": "" + "success-more-details-links": "Následně můžete začít vizualizovat data <2>vytvořením nástěnky nebo dotazováním na data v <5>Prozkoumat zobrazení." }, "data-sources": { "datasource-add-button": { @@ -6767,7 +6767,7 @@ "test": "Test" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Nebo si to zjednodušte a získejte {{mainDS}} (a {{extraDS}}) – plně spravované, škálovatelné a hostované zdroje dat od Grafana Labs s <6>celoživotním bezplatným plánem Grafana Cloud.", "title-alert": "Nakonfigurujte zdroje dat {{mainDS}} níže" }, "dashboards-table": { @@ -7756,7 +7756,7 @@ "math": { "available-math-functions": "Dostupné matematické funkce", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Podívejte se na naši další dokumentaci o <2>matematických výrazech.", "tooltip-title": "Matematický operátor", "tooltip-trigger": "Výraz" }, @@ -7782,11 +7782,24 @@ "label-upsample": "Zvýšit vzorkovací frekvenci", "tooltip-s-m-h": "10s, 1m, 30m, 1h" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Spustit dotaz", "modal-title": "Editor SQL", "tooltip-experimental": "Integrace LLM pro výrazy jazyka SQL je experimentální. Jakékoli problémy nahlaste týmu Grafana." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Vstup" } @@ -11022,10 +11035,10 @@ "placeholder-optional": "(volitelné)", "role": "Role", "submit": "Odeslat", - "tooltip": "" + "tooltip": "Teď můžete vybrat možnost „Žádná základní role“ a přidat oprávnění k vlastním potřebám. Více informací najdete v <1>naší dokumentaci." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Pošlete pozvánku nebo přidejte stávajícího uživatele Grafany do organizace.<1> {{orgName}}", "text": { "invite-user": "Pozvat uživatele" } @@ -12493,7 +12506,7 @@ }, "menu": { "clear-button": "Vymazat vše", - "tooltip": "" + "tooltip": "Teď můžete vybrat možnost „Žádná základní role“ a přidat oprávnění k vlastním potřebám. Více informací najdete v <1>naší dokumentaci." }, "menu-aria-label": "Nabídka voliče role", "menu-group-option-aria-label": "Možnost voliče role", @@ -12821,7 +12834,7 @@ "info-text": "Vytvořte přímý odkaz na tuto nástěnku nebo panel přizpůsobený možnostmi dole.", "link-url": "Odkaz adresy URL", "render-alert": "Plugin pro renderování obrázků není nainstalován", - "render-instructions": "", + "render-instructions": "Chcete-li vyrenderovat obrázek, musíte si nainstalovat <2>plugin Grafana pro renderování obrázků. Chcete-li nainstalovat plugin, obraťte se na správce Grafany.", "rendered-image": "Přímý odkaz na vyrenderovaný obrázek", "save-alert": "Nástěnka není uložena", "save-dashboard": "Chcete-li vyrenderovat obrázek panelu, musíte nejprve uložit nástěnku.", @@ -13864,7 +13877,7 @@ "label-format": "Formát", "label-set-timezone": "Nastavit časové pásmo", "label-time-field": "Pole času", - "tooltip-format": "", + "tooltip-format": "Výstupní formát pro pole zadané jako <2>řetězec formátu Moment.js.", "tooltip-timezone-manually": "Nastavit časové pásmo data manuálně" }, "format-time-transformer-editor": { diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 546fb6800d6..47445cd97ed 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -2081,7 +2081,7 @@ "warning-1": "Wenn Sie diese Benachrichtigungsrichtlinie löschen, wird sie dauerhaft entfernt.", "warning-2": "Möchten Sie diese Richtlinie wirklich löschen?" }, - "filter-description": "", + "filter-description": "Filtern Sie Benachrichtigungsrichtlinien und trennen Sie die Matchers durch Kommata, z. B.:<1>Schweregrad=kritisch, Region=EMEA", "generated-policies": "Automatisch generierte Richtlinien", "matchers": "Matcher", "metadata": { @@ -2808,7 +2808,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Vorlagendaten für Warnungen", - "only-in-alert": "Nur verfügbar im Rahmen einer Warnung (z. B. in einerAlarmschleife)" + "only-in-alert": "" }, "available-context-notification": "Verfügbar im Rahmen einer Benachrichtigung.", "notification-template-data": "Vorlagendaten für Benachrichtigungen" @@ -3245,7 +3245,7 @@ "auth-settings": "Authentifizierungseinstellungen" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Konfigurieren Sie die Authentifizierungseinstellungen. Erfahren Sie mehr in unserer <2>Dokumentation." }, "auth-drawer-unconnected": { "advanced-auth": "Erweiterte Authentifizierung", @@ -3279,7 +3279,7 @@ "allowed-organizations-description": "Durch Komma oder Leerzeichen getrennte Liste der Organisationen. Der Nutzer sollte Mitglied von mindestens\neiner Organisation sein, um sich anzumelden.", "allowed-organizations-label": "Erlaubte Organisationen", "allowed-organizations-placeholder": "Geben Sie Organisationen (my-team, myteam …) ein und drücken Sie zum Hinzufügen die Eingabetaste", - "api-url-description": "", + "api-url-description": "Der Endpunkt der Benutzerinformationen Ihres OAuth2-Anbieters. Informationen, die von diesem Endpunkt zurückgegeben werden, müssen mit <2>OpenID UserInfo kompatibel sein.", "api-url-required": "Dieses Feld muss – sofern festgelegt – eine gültige URL sein.", "auth-style-description": "Es bestimmt, wie „{{ clientIDLabel }}“ und „{{ clientSecretLabel }}“ an den Oauth2-Provider gesendet werden. Die Standardeinstellung ist AutoDetect.", "auth-style-label": "Authentifizierungsart", @@ -3424,7 +3424,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Verwalten Sie Ihre Authentifizierungseinstellungen und konfigurieren Sie einmalige Anmeldungen. Erfahren Sie mehr in unserer <2>Dokumentation." }, "bar-chart": { "warn": { @@ -4844,7 +4844,7 @@ "add-visualization-body": "Wählen Sie eine Datenquelle aus und visualisieren und fragen Sie dann Ihre Daten mit Diagrammen, Statistiken und Tabellen ab oder erstellen Sie Listen, Markierungen und andere Widgets.", "add-visualization-button": "Visualisierung hinzufügen", "add-visualization-header": "Starten Sie Ihr neues Dashboard, indem Sie eine Visualisierung hinzufügen", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Importieren Sie Dashboards aus Dateien oder von <2>grafana.com.", "import-a-dashboard-header": "Dashboard importieren", "import-dashboard-button": "Dashboard importieren", "show-less-dashboards": "", @@ -6623,7 +6623,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Klicken Sie <2>hier, um mehr über diesen Fehler zu erfahren.", - "success-more-details-links": "" + "success-more-details-links": "Als Nächstes können Sie damit beginnen, Daten zu visualisieren, indem Sie <2>ein Dashboard erstellen oder Daten in der <5>Explore-Ansicht abfragen." }, "data-sources": { "datasource-add-button": { @@ -6719,7 +6719,7 @@ "test": "Test" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Oder sparen Sie sich den Aufwand und erhalten Sie {{mainDS}} (und {{extraDS}}) als vollständig verwaltete, skalierbare und gehostete Datenquellen von Grafana Labs – mit dem <6>jederzeit kostenlosen Grafana-Cloud-Plan.", "title-alert": "Konfigurieren Sie unten Ihre {{mainDS}} Datenquelle" }, "dashboards-table": { @@ -7704,7 +7704,7 @@ "math": { "available-math-functions": "Verfügbare mathematische Funktionen", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Siehe unsere zusätzliche Dokumentation zu <2>Mathematischen Ausdrücken.", "tooltip-title": "Mathematik-Operator", "tooltip-trigger": "Ausdruck" }, @@ -7730,11 +7730,24 @@ "label-upsample": "Upsample", "tooltip-s-m-h": "10 s, 1 min., 30 min., 1 h" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Abfrage ausführen", "modal-title": "SQL Editor", "tooltip-experimental": "Die Integration des SQL Expressions LLM ist experimentell. Bitte melden Sie Probleme dem Grafana-Team." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Eingabe" } @@ -10928,10 +10941,10 @@ "placeholder-optional": "(optional)", "role": "Rolle", "submit": "Absenden", - "tooltip": "" + "tooltip": "Sie können jetzt die Option „Keine Basisrolle“ auswählen und Berechtigungen zu Ihren benutzerdefinierten Anforderungen hinzufügen. Weitere Informationen finden Sie in <1>unserer Dokumentation." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Versenden Sie eine Einladung oder fügen Sie einen bestehenden Grafana-Nutzer zur Organisation hinzu.<1> {{orgName}}", "text": { "invite-user": "Nutzer einladen" } @@ -12383,7 +12396,7 @@ }, "menu": { "clear-button": "Alles löschen", - "tooltip": "" + "tooltip": "Sie können jetzt die Option „Keine Basisrolle“ auswählen und Berechtigungen zu Ihren benutzerdefinierten Anforderungen hinzufügen. Weitere Informationen finden Sie in <1>unserer Dokumentation." }, "menu-aria-label": "Rollenauswahlmenü", "menu-group-option-aria-label": "Rollenauswahloption", @@ -12709,7 +12722,7 @@ "info-text": "Erstelle einen direkten Link zu diesem Dashboard oder Panel mit den folgenden angepassten Optionen.", "link-url": "Link-URL", "render-alert": "Bild-Render-Plug-in nicht installiert", - "render-instructions": "", + "render-instructions": "Um ein Bild zu rendern, müssen Sie das <2>Grafana Image Renderer Plugin installieren. Bitte kontaktieren Sie Ihren Grafana-Administrator, um das Plugin zu installieren.", "rendered-image": "Direktlink zum gerenderten Bild", "save-alert": "Dashboard nicht gespeichert", "save-dashboard": "Um ein Panel-Bild zu rendern, muss zuerst das Dashboard gespeichert werden.", @@ -13750,7 +13763,7 @@ "label-format": "Format", "label-set-timezone": "Zeitzone festlegen", "label-time-field": "Zeitfeld", - "tooltip-format": "", + "tooltip-format": "Das Ausgabeformat für das Feld, das als <2>Moment.js-Format-String angegeben ist.", "tooltip-timezone-manually": "Stellen Sie die Zeitzone des Datums manuell ein" }, "format-time-transformer-editor": { diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 35b1730e477..653d7942c02 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -2081,7 +2081,7 @@ "warning-1": "La eliminación de esta política de notificación la eliminará de forma permanente.", "warning-2": "¿Seguro que quieres eliminar esta política?" }, - "filter-description": "", + "filter-description": "Filtra las políticas de notificación mediante una lista de buscadores de coincidencias separados por comas, por ejemplo:<1>severidad=crítica, región=EMEA", "generated-policies": "Políticas generadas automáticamente", "matchers": "Buscadores de coincidencias", "metadata": { @@ -2808,7 +2808,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Datos de la plantilla de alerta", - "only-in-alert": "Disponible solo cuando está en el contexto de una alerta (por ejemplo, dentro de .Bucle de alertas" + "only-in-alert": "" }, "available-context-notification": "Disponible en el contexto de una notificación.", "notification-template-data": "Datos de la plantilla de notificación" @@ -3245,7 +3245,7 @@ "auth-settings": "Ajustes de autenticación" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Configura los ajustes de autenticación. Encuentra más información en nuestra <2><0>documentación." }, "auth-drawer-unconnected": { "advanced-auth": "Autenticación avanzada", @@ -3279,7 +3279,7 @@ "allowed-organizations-description": "Lista de organizaciones separadas por comas o espacios. El usuario debe ser miembro \nde al menos una organización para iniciar sesión.", "allowed-organizations-label": "Organizaciones permitidas", "allowed-organizations-placeholder": "Introduzca organizaciones (mi-equipo, miequipo...) y pulse Intro para añadirlas", - "api-url-description": "", + "api-url-description": "El punto final de información del usuario de tu proveedor de OAuth2. La información devuelta por este punto final debe ser compatible con <2>OpenID UserInfo.", "api-url-required": "Este campo debe ser una URL válida si se configura.", "auth-style-description": "Determina cómo se envían los valores de «{{ clientIDLabel }}» y «{{ clientSecretLabel }}» al proveedor de Oauth2. El valor predeterminado es «Detección automática».", "auth-style-label": "Estilo de autenticación", @@ -3424,7 +3424,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Gestiona tus ajustes de autenticación y configura el inicio de sesión único. Encuentra más información en nuestra <2><0>documentación." }, "bar-chart": { "warn": { @@ -4844,7 +4844,7 @@ "add-visualization-body": "Selecciona una fuente de datos y luego consulta y visualiza tus datos con gráficos, estadísticas y tablas o cree listas, anotaciones y otros widgets.", "add-visualization-button": "Añadir visualización", "add-visualization-header": "Comienza tu nuevo panel de control añadiendo una visualización", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Importa dashboards desde archivos o <2>grafana.com.", "import-a-dashboard-header": "Importar un tablero", "import-dashboard-button": "Importar panel de control", "show-less-dashboards": "", @@ -6623,7 +6623,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Haz clic <2>aquí para obtener más información sobre este error.", - "success-more-details-links": "" + "success-more-details-links": "A continuación, puedes empezar a visualizar los datos <2>creando un panel de control o consultando los datos en la <5>vista Explorar." }, "data-sources": { "datasource-add-button": { @@ -6719,7 +6719,7 @@ "test": "Prueba" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "O ahórrate el esfuerzo y consigue {{mainDS}} (y{{extraDS}}) como fuentes de datos totalmente gestionadas, escalables y alojadas de Grafana Labs con el <6>plan Grafana Cloud gratuito para siempre.", "title-alert": "Configura tu fuente de datos {{mainDS}} a continuación" }, "dashboards-table": { @@ -7704,7 +7704,7 @@ "math": { "available-math-functions": "Funciones matemáticas disponibles", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Consulta nuestra documentación adicional sobre <2>Expresiones matemáticas.", "tooltip-title": "Operador matemático", "tooltip-trigger": "Expresión" }, @@ -7730,11 +7730,24 @@ "label-upsample": "Aumentar tamaño", "tooltip-s-m-h": "10 s, 1 m, 30 m, 1 h" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Ejecutar consulta", "modal-title": "Editor de SQL", "tooltip-experimental": "La integración de LLM de expresiones SQL es experimental. Avisa de cualquier problema al equipo de Grafana." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Entrada" } @@ -10928,10 +10941,10 @@ "placeholder-optional": "(opcional)", "role": "Rol", "submit": "Enviar", - "tooltip": "" + "tooltip": "Ahora puedes seleccionar la opción «Sin función básica» y añadir permisos según tus necesidades específicas. Puedes encontrar más información en <1>nuestra documentación." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Envía una invitación o añade un usuario de Grafana existente a la organización.<1> {{orgName}}", "text": { "invite-user": "Invitar a usuario" } @@ -12383,7 +12396,7 @@ }, "menu": { "clear-button": "Borrar todo", - "tooltip": "" + "tooltip": "Ahora puedes seleccionar la opción «Sin función básica» y añadir permisos según tus necesidades específicas. Puedes encontrar más información en <1>nuestra documentación." }, "menu-aria-label": "Menú del selector de roles", "menu-group-option-aria-label": "Opción del selector de roles", @@ -12709,7 +12722,7 @@ "info-text": "Cree un enlace directo a este panel de control o panel, personalizado con las siguientes opciones.", "link-url": "URL del enlace", "render-alert": "Complemento de renderizador de imagen no instalado", - "render-instructions": "", + "render-instructions": "Para renderizar una imagen, debes instalar el <2>complemento de renderizado de imágenes Grafana. Ponte en contacto con tu administrador de Grafana para instalarlo.", "rendered-image": "Enlace directo a la imagen renderizada", "save-alert": "El panel de control no se ha guardado", "save-dashboard": "Para renderizar una imagen de panel, primero debe guardar el panel de control.", @@ -13750,7 +13763,7 @@ "label-format": "Formato", "label-set-timezone": "Establecer zona horaria", "label-time-field": "Campo de tiempo", - "tooltip-format": "", + "tooltip-format": "El formato de salida para el campo especificado como una <2>cadena de formato Moment.js.", "tooltip-timezone-manually": "Establecer la zona horaria de la fecha manualmente" }, "format-time-transformer-editor": { diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 74e96034e55..fecfc76d3ea 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -2081,7 +2081,7 @@ "warning-1": "La suppression de cette politique de notification la supprimera définitivement.", "warning-2": "Voulez-vous vraiment supprimer cette politique ?" }, - "filter-description": "", + "filter-description": "Filtrez les politiques de notification en utilisant une liste de correspondances séparées par des virgules, par exemple :<1>gravité=critique, région=EMEA", "generated-policies": "Politiques générées automatiquement", "matchers": "Correspondants", "metadata": { @@ -2808,7 +2808,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Données du modèle d’alerte", - "only-in-alert": "Disponible uniquement dans le contexte d’une alerte (par exemple à l’intérieur de .Boucle d’alertes)" + "only-in-alert": "" }, "available-context-notification": "Disponible dans le contexte d’une notification.", "notification-template-data": "Données du modèle de notification" @@ -3245,7 +3245,7 @@ "auth-settings": "Paramètres d’authentification" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Configurez les paramètres d’authentification. Pour en savoir plus, consultez notre <2>documentation." }, "auth-drawer-unconnected": { "advanced-auth": "Authentification avancée", @@ -3279,7 +3279,7 @@ "allowed-organizations-description": "Liste des organisations séparées par des virgules ou des espaces. L’utilisateur doit être membre\nd’au moins une organisation pour se connecter.", "allowed-organizations-label": "Organisations autorisées", "allowed-organizations-placeholder": "Saisissez les organisations (my-team, myteam...) et appuyez sur Entrée pour ajouter", - "api-url-description": "", + "api-url-description": "Le point de terminaison des informations utilisateur de votre fournisseur OAuth2. Les informations renvoyées par ce point de terminaison doivent être compatibles avec <2>OpenID UserInfo.", "api-url-required": "Ce champ doit être une URL valide s’il est défini.", "auth-style-description": "Il détermine comment « {{ clientIDLabel }} » et « {{ clientSecretLabel }} » sont envoyés au fournisseur Oauth2. La valeur par défaut est AutoDetect.", "auth-style-label": "Style d’authentification", @@ -3424,7 +3424,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Gérez vos paramètres d’authentification et configurez l’authentification unique. Pour en savoir plus, consultez notre <2>documentation." }, "bar-chart": { "warn": { @@ -4844,7 +4844,7 @@ "add-visualization-body": "Sélectionnez une source de données, puis examinez et visualisez vos données avec des graphiques, des statistiques et des tableaux ou créez des listes, des markdowns et d'autres widgets.", "add-visualization-button": "Ajouter une visualisation", "add-visualization-header": "Commencez votre nouveau tableau de bord en ajoutant une visualisation", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Importez des tableaux de bord à partir de fichiers ou du site <2>grafana.com.", "import-a-dashboard-header": "Importer un tableau de bord", "import-dashboard-button": "Importer un tableau de bord", "show-less-dashboards": "", @@ -6623,7 +6623,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Cliquez <2>ici pour en savoir plus sur cette erreur.", - "success-more-details-links": "" + "success-more-details-links": "Ensuite, vous pouvez commencer à visualiser les données en <2>créant un tableau de bord ou en interrogeant les données dans la <5>vue Explorer." }, "data-sources": { "datasource-add-button": { @@ -6719,7 +6719,7 @@ "test": "Test" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Ou simplifiez-vous la tâche et obtenez {{mainDS}} (et {{extraDS}}) sous forme de sources de données entièrement gérées, évolutives et hébergées par Grafana Labs avec le <6>plan Grafana Cloud gratuit à vie.", "title-alert": "Configurer la source de données {{mainDS}} ci-dessous" }, "dashboards-table": { @@ -7704,7 +7704,7 @@ "math": { "available-math-functions": "Fonctions mathématiques disponibles", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Consultez notre documentation supplémentaire sur les <2>expressions mathématiques.", "tooltip-title": "Opérateur mathématique", "tooltip-trigger": "Expression" }, @@ -7730,11 +7730,24 @@ "label-upsample": "Sur-échantillonner", "tooltip-s-m-h": "10 s, 1 m, 30 m, 1 h" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Exécuter la requête", "modal-title": "Éditeur SQL", "tooltip-experimental": "L’intégration des expressions SQL avec les LLM est expérimentale. Merci de signaler tout problème à l’équipe Grafana." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Entrée" } @@ -10928,10 +10941,10 @@ "placeholder-optional": "(facultatif)", "role": "Rôle", "submit": "Envoyer", - "tooltip": "" + "tooltip": "Vous pouvez maintenant sélectionner l'option « Aucun rôle de base » et ajouter des autorisations à vos besoins personnalisés. Vous trouverez plus d'informations dans <1>notre documentation." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Envoyez une invitation ou ajoutez un utilisateur Grafana existant à l’organisation.<1>{{orgName}}", "text": { "invite-user": "Inviter un utilisateur" } @@ -12383,7 +12396,7 @@ }, "menu": { "clear-button": "Tout effacer", - "tooltip": "" + "tooltip": "Vous pouvez maintenant sélectionner l'option « Aucun rôle de base » et ajouter des autorisations à vos besoins personnalisés. Vous trouverez plus d'informations dans <1>notre documentation." }, "menu-aria-label": "Menu du sélecteur de rôle", "menu-group-option-aria-label": "Option du sélecteur de rôle", @@ -12709,7 +12722,7 @@ "info-text": "Créez un lien direct vers ce tableau de bord ou ce panneau, personnalisé avec les options ci-dessous.", "link-url": "Lien URL", "render-alert": "Plugin de rendu d'image non installé", - "render-instructions": "", + "render-instructions": "Pour afficher une image, vous devez installer le <2>plugin de rendu d’images Grafana. Veuillez contacter votre administrateur Grafana pour installer le plugin.", "rendered-image": "Lien direct vers l'image dont il faut améliorer le rendu", "save-alert": "Le tableau de bord n'est pas enregistré", "save-dashboard": "Pour améliorer le rendu d'une image de panneau, vous devez d'abord enregistrer le tableau de bord.", @@ -13750,7 +13763,7 @@ "label-format": "Format", "label-set-timezone": "Définir le fuseau horaire", "label-time-field": "Champ horaire", - "tooltip-format": "", + "tooltip-format": "Le format de sortie pour le champ spécifié en tant que <2>chaîne de format Moment.js.", "tooltip-timezone-manually": "Définir manuellement le fuseau horaire de la date" }, "format-time-transformer-editor": { diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 02d75808711..e691d36eff2 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -2081,7 +2081,7 @@ "warning-1": "Az értesítési házirendet a törlésével véglegesen eltávolítja.", "warning-2": "Biztosan törli ezt a házirendet?" }, - "filter-description": "", + "filter-description": "Az értesítési házirendek szűrése az egyezések vesszővel elválasztott listájának használatával, pl.:<1>súlyosság=kritikus, régió=EMEA", "generated-policies": "Automatikusan generált házirendek", "matchers": "Egyezések", "metadata": { @@ -2808,7 +2808,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Riasztási sablon adatai", - "only-in-alert": "Csak egy riasztás kontextusában érhető el (pl. Riasztási hurok)" + "only-in-alert": "" }, "available-context-notification": "Egy értesítés kontextusában érhető el.", "notification-template-data": "Értesítési sablon adatai" @@ -3245,7 +3245,7 @@ "auth-settings": "Hitelesítési beállítások" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Hitelesítési beállítások konfigurálása. További információkat a <2>dokumentációnkban talál." }, "auth-drawer-unconnected": { "advanced-auth": "Speciális hitelesítés", @@ -3279,7 +3279,7 @@ "allowed-organizations-description": "Vesszővel vagy szóközzel elválasztott szervezetek listája. A bejelentkezéshez a felhasználónak legalább \negy szervezet tagjának kell lennie.", "allowed-organizations-label": "Engedélyezett szervezetek", "allowed-organizations-placeholder": "Adja meg a szervezeteket (my-team, myteam…), és nyomja le az Enter billentyűt a hozzáadáshoz", - "api-url-description": "", + "api-url-description": "Az OAuth2-szolgáltató felhasználói információs végpontja. Az ezen végpont által visszaadott információknak kompatibilisnek kell lenniük az <2>OpenID UserInfóval.", "api-url-required": "Ennek a mezőnek érvényes URL-címnek kell lennie, ha be van állítva.", "auth-style-description": "Ez határozza meg, hogy a(z) „{{ clientIDLabel }}” és a(z) „{{ clientSecretLabel }}” hogyan lesz elküldve az Oauth2-szolgáltatónak. Az alapértelmezett beállítás az AutoDetect (Automatikus észlelés).", "auth-style-label": "Hitelesítési stílus", @@ -3424,7 +3424,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Saját hitelesítési beállítások kezelése és az egyszeri bejelentkezés konfigurálása. További információkat a <2>dokumentációnkban talál." }, "bar-chart": { "warn": { @@ -4844,7 +4844,7 @@ "add-visualization-body": "Válasszon ki egy adatforrást, majd kérdezze le és jelenítse meg az adatait diagramokkal, statisztikákkal és táblázatokkal, vagy hozzon létre listákat, Markdown-elemeket és egyéb widgeteket.", "add-visualization-button": "Vizualizáció hozzáadása", "add-visualization-header": "Indítsa el az új irányítópultot vizualizáció hozzáadásával", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Irányítópultok importálása fájlokból vagy a <2>grafana.com webhelyről.", "import-a-dashboard-header": "Irányítópult importálása", "import-dashboard-button": "Irányítópult importálása", "show-less-dashboards": "", @@ -6623,7 +6623,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Ha többet szeretne megtudni erről a hibáról, kattintson <2>ide.", - "success-more-details-links": "" + "success-more-details-links": "Ezután < 2 >létrehozhat egy irányítópultot, vagy lekérdezheti az adatokat az <5>Explore nézetben." }, "data-sources": { "datasource-add-button": { @@ -6719,7 +6719,7 @@ "test": "Teszt" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Vagy hagyja ki a próbálkozást, és szerezze be a(z) {{mainDS}} adatforrást (és {{extraDS}}) a Grafana Labs teljes körűen felügyelt, skálázható és hosztolt adatforrásait az <6>örökre ingyenes Grafana Cloud-előfizetéssel.", "title-alert": "Konfigurálja {{mainDS}} adatforrását alább" }, "dashboards-table": { @@ -7704,7 +7704,7 @@ "math": { "available-math-functions": "Elérhető matematikai függvények", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Lásd a további dokumentációt a <2>matematikai kifejezésekről.", "tooltip-title": "Matematikai operátor", "tooltip-trigger": "Kifejezés" }, @@ -7730,11 +7730,24 @@ "label-upsample": "Mintasűrűség növelése", "tooltip-s-m-h": "10 mp., 1 p., 30 p., 1 ó." }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Lekérdezés futtatása", "modal-title": "SQL-szerkesztő", "tooltip-experimental": "Az SQL-kifejezések LLM-integrációja kísérleti jellegű. Kérjük, jelentse az esetleges problémákat a Grafana csapatának." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Bemenet" } @@ -10928,10 +10941,10 @@ "placeholder-optional": "(opcionális)", "role": "Szerepkör", "submit": "Küldés", - "tooltip": "" + "tooltip": "Most kiválaszthatja a „Nincs alapvető szerepkör” opciót, és az egyéni igényeinek megfelelő engedélyeket adhat hozzá. További információt a <1>dokumentációnkban talál." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Meghívó küldése vagy meglévő Grafana-felhasználó hozzáadása a szervezethez.<1>{{orgName}}", "text": { "invite-user": "Felhasználó meghívása" } @@ -12383,7 +12396,7 @@ }, "menu": { "clear-button": "Összes törlése", - "tooltip": "" + "tooltip": "Most kiválaszthatja a „Nincs alapvető szerepkör” opciót, és az egyéni igényeinek megfelelő engedélyeket adhat hozzá. További információt a <1>dokumentációnkban talál." }, "menu-aria-label": "Szerepkörválasztó menü", "menu-group-option-aria-label": "Szerepkörválasztási beállítás", @@ -12709,7 +12722,7 @@ "info-text": "Létrehozhat egy közvetlen hivatkozást ehhez az irányítópulthoz vagy panelhez, az alábbi lehetőségekkel testreszabva.", "link-url": "Hivatkozás URL-címe", "render-alert": "Az Image Renderer bővítmény nincs telepítve", - "render-instructions": "", + "render-instructions": "Képek rendereléséhez telepítenie kell a <2>Grafana Image Renderer bővítményt. Kérjük, vegye fel a kapcsolatot Grafana-rendszergazdájával a bővítmény telepítéséhez.", "rendered-image": "Renderelt kép közvetlen hivatkozása", "save-alert": "Az irányítópult nincs mentve", "save-dashboard": "Panelkép rendereléséhez először mentenie kell az irányítópultot.", @@ -13750,7 +13763,7 @@ "label-format": "Formátum", "label-set-timezone": "Időzóna beállítása", "label-time-field": "Időmező", - "tooltip-format": "", + "tooltip-format": "A <2>Moment.js formátumú karakterlánc mező kimeneti formátuma.", "tooltip-timezone-manually": "A dátum időzónájának manuális beállítása" }, "format-time-transformer-editor": { diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index d9b58b75791..ff5ecf95800 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -2075,7 +2075,7 @@ "warning-1": "Menghapus kebijakan pemberitahuan ini akan menghapusnya secara permanen.", "warning-2": "Apa Anda yakin ingin menghapus kebijakan ini?" }, - "filter-description": "", + "filter-description": "Filter kebijakan pemberitahuan menggunakan daftar pencocok yang dipisahkan koma, misalnya:<1>severity=critical, region=EMEA", "generated-policies": "Kebijakan yang dibuat otomatis", "matchers": "Pencocok", "metadata": { @@ -2796,7 +2796,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Data templat peringatan", - "only-in-alert": "Hanya tersedia ketika dalam konteks Peringatan (misalnya di dalam .loop Peringatan)" + "only-in-alert": "" }, "available-context-notification": "Tersedia dalam konteks pemberitahuan.", "notification-template-data": "Data templat pemberitahuan" @@ -3233,7 +3233,7 @@ "auth-settings": "Pengaturan autentikasi" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Konfigurasikan pengaturan autentikasi. Cari tahu selengkapnya di <2>dokumentasi kami." }, "auth-drawer-unconnected": { "advanced-auth": "Autentikasi Lanjutan", @@ -3267,7 +3267,7 @@ "allowed-organizations-description": "Daftar organisasi yang dipisahkan koma atau spasi. Pengguna harus menjadi anggota\ndari setidaknya satu organisasi untuk masuk.", "allowed-organizations-label": "Organisasi yang diizinkan", "allowed-organizations-placeholder": "Masukkan organisasi (my-team, myteam...) dan tekan Enter untuk menambahkan", - "api-url-description": "", + "api-url-description": "Endpoint informasi pengguna penyedia OAuth2 Anda. Informasi yang dikembalikan oleh endpoint ini harus kompatibel dengan <2>OpenID UserInfo.", "api-url-required": "Bidang ini harus berupa URL yang valid jika ditetapkan.", "auth-style-description": "Ini menentukan cara \"{{ clientIDLabel }}\" dan \"{{ clientSecretLabel }}\" dikirim ke penyedia Oauth2. Default adalah AutoDetect.", "auth-style-label": "Metode autentikasi", @@ -3412,7 +3412,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Kelola pengaturan autentikasi Anda dan konfigurasikan single sign-on. Cari tahu selengkapnya di <2>dokumentasi kami." }, "bar-chart": { "warn": { @@ -4824,7 +4824,7 @@ "add-visualization-body": "Pilih sumber data, kemudian kueri dan visualisasikan data Anda dengan bagan, statistik, dan tabel atau buat daftar, markdown, dan widget lainnya.", "add-visualization-button": "Tambahkan visualisasi", "add-visualization-header": "Mulai dasbor baru Anda dengan menambahkan visualisasi", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Impor dasbor dari file atau <2>grafana.com.", "import-a-dashboard-header": "Impor dasbor", "import-dashboard-button": "Impor dasbor", "show-less-dashboards": "", @@ -6599,7 +6599,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Klik <2>di sini untuk mempelajari selengkapnya tentang kesalahan ini.", - "success-more-details-links": "" + "success-more-details-links": "Selanjutnya, Anda dapat mulai memvisualisasikan data dengan <2>membuat dasbor, atau dengan melakukan kueri data di <5>tampilan Explore." }, "data-sources": { "datasource-add-button": { @@ -6695,7 +6695,7 @@ "test": "Tes" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Atau lewati proses dan dapatkan {{mainDS}} (dan {{extraDS}}) sebagai sumber data yang dikelola sepenuhnya, dapat diskalakan, dan di-host dari Grafana Labs dengan <6>paket Grafana Cloud gratis selamanya.", "title-alert": "Konfigurasikan sumber data {{mainDS}} Anda di bawah ini" }, "dashboards-table": { @@ -7678,7 +7678,7 @@ "math": { "available-math-functions": "Fungsi matematika yang tersedia", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Lihat dokumentasi tambahan kami di <2>Pola matematika.", "tooltip-title": "Operator matematika", "tooltip-trigger": "Ekspresi" }, @@ -7704,11 +7704,24 @@ "label-upsample": "Tingkatkan sample", "tooltip-s-m-h": "10 dtk, 1 mnt, 30 mnt, 1 jm" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Jalankan kueri", "modal-title": "Editor SQL", "tooltip-experimental": "Integrasi LLM Ekspresi SQL bersifat eksperimental. Harap laporkan masalah apa pun kepada tim Grafana." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Input" } @@ -10881,10 +10894,10 @@ "placeholder-optional": "(opsional)", "role": "Peran", "submit": "Kirim", - "tooltip": "" + "tooltip": "Anda sekarang dapat memilih opsi \"Tidak ada peran dasar\" dan menambahkan izin ke kebutuhan kustom Anda. Anda dapat menemukan informasi lebih lanjut di <1> dokumentasi kami ." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Kirim undangan atau tambahkan pengguna Grafana yang ada ke organisasi.<1> {{orgName}}", "text": { "invite-user": "Undang pengguna" } @@ -12328,7 +12341,7 @@ }, "menu": { "clear-button": "Hapus semua", - "tooltip": "" + "tooltip": "Anda sekarang dapat memilih opsi \"Tidak ada peran dasar\" dan menambahkan izin ke kebutuhan kustom Anda. Anda dapat menemukan informasi lebih lanjut di <1> dokumentasi kami ." }, "menu-aria-label": "Menu pemilih peran", "menu-group-option-aria-label": "Opsi pemilih peran", @@ -12653,7 +12666,7 @@ "info-text": "Buat tautan langsung ke dasbor atau panel ini, disesuaikan dengan opsi di bawah ini.", "link-url": "Tautkan URL", "render-alert": "Plugin perender gambar belum diinstal", - "render-instructions": "", + "render-instructions": "Untuk merender gambar, Anda harus menginstal <2>plugin perender gambar Grafana. Hubungi administrator Grafana Anda untuk menginstal plugin.", "rendered-image": "Tautan langsung gambar yang dirender", "save-alert": "Dasbor tidak disimpan", "save-dashboard": "Untuk merender gambar panel, Anda harus menyimpan dasbor terlebih dahulu.", @@ -13693,7 +13706,7 @@ "label-format": "Format", "label-set-timezone": "Atur zona waktu", "label-time-field": "Bidang waktu", - "tooltip-format": "", + "tooltip-format": "Format output untuk bidang yang ditentukan sebagai <2>string format Moment.js.", "tooltip-timezone-manually": "Atur zona waktu tanggal secara manual" }, "format-time-transformer-editor": { diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 7ada28c6693..d70278c2c40 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -2081,7 +2081,7 @@ "warning-1": "L'eliminazione di questo criterio di notifica lo rimuoverà in modo permanente.", "warning-2": "Vuoi davvero eliminare questo criterio?" }, - "filter-description": "", + "filter-description": "Filtra i criteri di notifica utilizzando un elenco di corrispondenti separati da virgole, ad esempio:<1>gravità=critica, regione=EMEA", "generated-policies": "Criteri generati automaticamente", "matchers": "Corrispondenze", "metadata": { @@ -2808,7 +2808,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Dati del modello di avviso", - "only-in-alert": "Disponibile solo nel contesto di un avviso (ad es. all'interno di .Ciclo di avvisi)" + "only-in-alert": "" }, "available-context-notification": "Disponibile nel contesto di una notifica.", "notification-template-data": "Dati del modello di notifica" @@ -3245,7 +3245,7 @@ "auth-settings": "Impostazioni di autenticazione" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Configura le impostazioni di autenticazione. Scopri di più nella nostra <2>documentazione." }, "auth-drawer-unconnected": { "advanced-auth": "Autenticazione avanzata", @@ -3279,7 +3279,7 @@ "allowed-organizations-description": "Elenco di organizzazioni separate da virgole o spazi. L'utente deve essere membro\ndi almeno un'organizzazione per accedere.", "allowed-organizations-label": "Organizzazioni consentite", "allowed-organizations-placeholder": "Inserisci le organizzazioni (my-team, myteam...) e premi Invio per aggiungerle", - "api-url-description": "", + "api-url-description": "L'endpoint delle informazioni utente del tuo provider OAuth2. Le informazioni restituite da questo endpoint devono essere compatibili con <2>OpenID UserInfo.", "api-url-required": "Questo campo deve essere un URL valido se impostato.", "auth-style-description": "Determina come \"{{ clientIDLabel }}\" e \"{{ clientSecretLabel }}\" vengono inviati al provider Oauth2. L'impostazione predefinita è Rilevamento automatico.", "auth-style-label": "Stile di autenticazione", @@ -3424,7 +3424,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Gestisci le impostazioni di autenticazione e configura il Single Sign-On. Scopri di più nella nostra <2>documentazione." }, "bar-chart": { "warn": { @@ -4844,7 +4844,7 @@ "add-visualization-body": "Seleziona un'origine dati, quindi esegui query e visualizza i dati con grafici, statistiche e tabelle o crea elenchi, markdown e altri widget.", "add-visualization-button": "Aggiungi visualizzazione", "add-visualization-header": "Avvia il tuo nuovo dashboard aggiungendo una visualizzazione", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Importa dashboard da file o <2>grafana.com.", "import-a-dashboard-header": "Importa un dashboard", "import-dashboard-button": "Importa dashboard", "show-less-dashboards": "", @@ -6623,7 +6623,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Fai clic <2>qui per saperne di più su questo errore.", - "success-more-details-links": "" + "success-more-details-links": "Successivamente, puoi iniziare a visualizzare i dati <2>creando un dashboard o eseguendo query sui dati nella <5>vista Esplora." }, "data-sources": { "datasource-add-button": { @@ -6719,7 +6719,7 @@ "test": "Prova" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Oppure puoi ignorare tutto e utilizzare {{mainDS}} (e {{extraDS}}) come origini dei dati completamente gestite, scalabili e ospitate da Grafana Labs con il <6>piano Grafana Cloud gratuito per sempre.", "title-alert": "Configura la tua origine dei dati {{mainDS}} qui sotto" }, "dashboards-table": { @@ -7704,7 +7704,7 @@ "math": { "available-math-functions": "Funzioni matematiche disponibili", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Consulta la nostra documentazione aggiuntiva su <2>Espressioni matematiche.", "tooltip-title": "Operatore matematico", "tooltip-trigger": "Espressione" }, @@ -7730,11 +7730,24 @@ "label-upsample": "Aumenta campionamento", "tooltip-s-m-h": "10 s, 1 m, 30 m, 1 h" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Esegui query", "modal-title": "Editor SQL", "tooltip-experimental": "L'integrazione LLM delle Espressioni SQL è sperimentale. Segnala eventuali problemi al team Grafana." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Inserisci" } @@ -10928,10 +10941,10 @@ "placeholder-optional": "(opzionale)", "role": "Ruolo", "submit": "Invia", - "tooltip": "" + "tooltip": "Ora puoi selezionare l'opzione \"Nessun ruolo di base\" e aggiungere autorizzazioni in base alle tue esigenze personalizzate. Puoi trovare maggiori informazioni nella <1>nostra documentazione." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Invia un invito o aggiungi un utente Grafana esistente all'organizzazione.<1> {{orgName}}", "text": { "invite-user": "Invita un utente" } @@ -12383,7 +12396,7 @@ }, "menu": { "clear-button": "Cancella tutto", - "tooltip": "" + "tooltip": "Ora puoi selezionare l'opzione \"Nessun ruolo di base\" e aggiungere autorizzazioni in base alle tue esigenze personalizzate. Puoi trovare maggiori informazioni nella <1>nostra documentazione." }, "menu-aria-label": "Menu selettore ruoli", "menu-group-option-aria-label": "Opzione selettore ruoli", @@ -12709,7 +12722,7 @@ "info-text": "Crea un link diretto a questo dashboard o pannello, personalizzato con le opzioni seguenti.", "link-url": "Collega URL", "render-alert": "Plug-in del renderer di immagini non installato", - "render-instructions": "", + "render-instructions": "Per eseguire il rendering di un'immagine, è necessario installare il <2>plug-in del renderer di immagini Grafana. Contatta l'amministratore di Grafana per installare il plug-in.", "rendered-image": "Immagine con rendering del collegamento diretto", "save-alert": "Il dashboard non è stato salvato", "save-dashboard": "Per eseguire il rendering di un'immagine del pannello, è necessario prima salvare il dashboard.", @@ -13750,7 +13763,7 @@ "label-format": "Formato", "label-set-timezone": "Imposta fuso orario", "label-time-field": "Campo dell'ora", - "tooltip-format": "", + "tooltip-format": "Il formato di output per il campo specificato come <2>stringa di formato Moment.js.", "tooltip-timezone-manually": "Imposta manualmente il fuso orario della data" }, "format-time-transformer-editor": { diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 5043d63283e..9667ae7ea41 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -2075,7 +2075,7 @@ "warning-1": "この通知ポリシーを削除すると、完全に削除されます。", "warning-2": "本当にこれを削除してもよろしいですか?" }, - "filter-description": "", + "filter-description": "通知ポリシーを、カンマで区切られたマッチャーのリストを使用してフィルタリングします。例:<1>severity=critical, region=EMEA", "generated-policies": "自動生成されたポリシー", "matchers": "マッチャー", "metadata": { @@ -2796,7 +2796,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "アラートテンプレートデータ", - "only-in-alert": "アラートのコンテキストでのみ使用できます(アラートループ内など)" + "only-in-alert": "" }, "available-context-notification": "通知のコンテキストで利用可能です。", "notification-template-data": "通知テンプレートデータ" @@ -3233,7 +3233,7 @@ "auth-settings": "認証設定" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "認証設定を行ってください。詳細については、<2>ドキュメントをご覧ください。" }, "auth-drawer-unconnected": { "advanced-auth": "高度な認証", @@ -3267,7 +3267,7 @@ "allowed-organizations-description": "カンマまたはスペースで区切られた組織のリスト。ユーザーがログインするには、少なくとも\n1つの組織のメンバーである必要があります。", "allowed-organizations-label": "許可された組織", "allowed-organizations-placeholder": "組織(my-team、myteam...)を入力し、Enterキーを押して追加します", - "api-url-description": "", + "api-url-description": "OAuth2プロバイダーのユーザー情報エンドポイント。このエンドポイントから返される情報は<2>OpenID UserInfoと互換性がある必要があります。", "api-url-required": "このフィールドを設定する場合、有効なURLを入力する必要があります。", "auth-style-description": "「{{ clientIDLabel }}」と「{{ clientSecretLabel }}」がOAuth2プロバイダーに送信される方法を決定します。デフォルトは自動検出です。", "auth-style-label": "認証形式", @@ -3412,7 +3412,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "認証設定を管理し、シングルサインオンを設定します。詳細については、<2>ドキュメントをご覧ください。" }, "bar-chart": { "warn": { @@ -4824,7 +4824,7 @@ "add-visualization-body": "データソースを選択し、グラフ、統計、テーブルを使用してデータをクエリおよび視覚化するか、リスト、マークダウン、その他のウィジェットを作成します。", "add-visualization-button": "視覚化を追加", "add-visualization-header": "視覚化を追加して新しいダッシュボードを開始します", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "ファイルまたは<1>grafana.comからダッシュボードをインポートします。", "import-a-dashboard-header": "ダッシュボードをインポートする", "import-dashboard-button": "ダッシュボードをインポート", "show-less-dashboards": "", @@ -6599,7 +6599,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "このエラーの詳細については、<2>こちらをクリックしてください。", - "success-more-details-links": "" + "success-more-details-links": "次に、<2>ダッシュボードを構築するか、<5>Exploreビューでデータをクエリすることで、データの視覚化を開始できます。" }, "data-sources": { "datasource-add-button": { @@ -6695,7 +6695,7 @@ "test": "テスト" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "または、<6>永久無料のGrafana Cloudプランで、Grafana Labsからフルマネージドでスケーラブルなホスト済みデータソースの{{mainDS}}(および{{extraDS}})を入手して、手間を省くことも可能です。", "title-alert": "以下で{{mainDS}}データソースを設定します" }, "dashboards-table": { @@ -7678,7 +7678,7 @@ "math": { "available-math-functions": "利用可能な数学関数", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "<2>数式に関する追加ドキュメントをご覧ください。", "tooltip-title": "数学演算子", "tooltip-trigger": "式" }, @@ -7704,11 +7704,24 @@ "label-upsample": "アップサンプリング", "tooltip-s-m-h": "10秒、1分、30分、1時間" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "クエリを実行", "modal-title": "SQLエディター", "tooltip-experimental": "SQL Expressions LLMの統合は実験的です。問題が発生した場合は、Grafanaチームに報告してください。" }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "入力" } @@ -10881,10 +10894,10 @@ "placeholder-optional": "(任意)", "role": "ロール", "submit": "送信", - "tooltip": "" + "tooltip": "「基本的な役割なし」オプションを選択し、カスタムニーズに権限を追加できるようになりました。詳細については、<1>ドキュメントをご覧ください。" }, "user-invite-page": { - "sub-title": "", + "sub-title": "招待状を送信するか、既存のGrafanaユーザーを組織<1>{{orgName}}に追加します。", "text": { "invite-user": "ユーザーを招待" } @@ -12328,7 +12341,7 @@ }, "menu": { "clear-button": "すべてをクリア", - "tooltip": "" + "tooltip": "「基本的な役割なし」オプションを選択し、カスタムニーズに権限を追加できるようになりました。詳細については、<1>ドキュメントをご覧ください。" }, "menu-aria-label": "ロール選択ツールメニュー", "menu-group-option-aria-label": "ロール選択オプション", @@ -12653,7 +12666,7 @@ "info-text": "以下のオプションでカスタマイズした、このダッシュボードまたはパネルへの直接リンクを作成します。", "link-url": "URLをリンク", "render-alert": "画像レンダラープラグインがインストールされていません", - "render-instructions": "", + "render-instructions": "画像をレンダリングするには、<2>Grafana画像レンダラープラグインをインストールする必要があります。プラグインをインストールするには、Grafana管理者にお問い合わせください。", "rendered-image": "レンダリングされた画像への直接リンク", "save-alert": "ダッシュボードは保存されていません", "save-dashboard": "パネル画像をレンダリングするには、最初にダッシュボードを保存する必要があります。", @@ -13693,7 +13706,7 @@ "label-format": "形式", "label-set-timezone": "タイムゾーンを設定", "label-time-field": "時間フィールド", - "tooltip-format": "", + "tooltip-format": "<2>Moment.js形式文字列として指定されたフィールドの出力形式。", "tooltip-timezone-manually": "日付のタイムゾーンを手動で設定" }, "format-time-transformer-editor": { diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 0b205d6c624..115f653a526 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -2075,7 +2075,7 @@ "warning-1": "이 알림 정책을 삭제하면 영구적으로 제거됩니다.", "warning-2": "정말로 이 정책을 삭제하시겠어요?" }, - "filter-description": "", + "filter-description": "다음과 같이 쉼표로 구분된 일치 조건의 목록을 사용하여 알림 정책을 필터링합니다. <1>심각도=심각, 지역=EMEA", "generated-policies": "자동 생성된 정책", "matchers": "일치 조건", "metadata": { @@ -2796,7 +2796,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "경고 템플릿 데이터", - "only-in-alert": "경고 콘텍스트에서만 사용할 수 있습니다(예: 내부 .경고 루프)" + "only-in-alert": "" }, "available-context-notification": "알림 컨텍스트에서 사용할 수 있습니다.", "notification-template-data": "알림 템플릿 데이터" @@ -3233,7 +3233,7 @@ "auth-settings": "인증 설정" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "인증 설정을 구성합니다. 자세한 내용은 Grafana의 <2>문서를 확인하세요." }, "auth-drawer-unconnected": { "advanced-auth": "고급 인증", @@ -3267,7 +3267,7 @@ "allowed-organizations-description": "쉼표 또는 공백으로 구분된 조직 목록입니다. 사용자가 로그인하려면 하나 이상의\n조직의 멤버여야 합니다.", "allowed-organizations-label": "허용된 조직", "allowed-organizations-placeholder": "조직(my-team, myteam...)을 입력하고 엔터 키를 눌러 추가", - "api-url-description": "", + "api-url-description": "OAuth2 제공자의 사용자 정보 엔드포인트입니다. 이 엔드포인트에서 반환되는 정보는 <2>OpenID UserInfo와 호환되어야 합니다.", "api-url-required": "설정된 경우 이 필드는 유효한 URL이어야 합니다.", "auth-style-description": "\"{{ clientIDLabel }}\" 및 \"{{ clientSecretLabel }}\"이(가) Oauth2 제공자에 전송되는 방식을 결정합니다. 기본값은 AutoDetect입니다.", "auth-style-label": "인증 스타일", @@ -3412,7 +3412,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "인증 설정을 관리하고 싱글 사인온을 구성하세요. 자세한 내용은 Grafana의 <2>문서를 확인하세요." }, "bar-chart": { "warn": { @@ -4824,7 +4824,7 @@ "add-visualization-body": "데이터 소스를 선택한 다음 차트, 통계, 표를 사용하여 데이터를 쿼리하고 시각화하거나 목록, 마크다운 및 기타 위젯을 생성합니다.", "add-visualization-button": "시각화 추가", "add-visualization-header": "시각화 추가하여 새 대시보드 시작하기", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "파일 또는 <1>grafana.com에서 대시보드 가져오기", "import-a-dashboard-header": "대시보드 가져오기", "import-dashboard-button": "대시보드 가져오기", "show-less-dashboards": "", @@ -6599,7 +6599,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "이 오류에 대해 자세히 알아보려면 <2>여기를 클릭하세요.", - "success-more-details-links": "" + "success-more-details-links": "그런 다음 <2>대시보드를 구축하거나 <5>탐색 보기에서 데이터를 쿼리하여 데이터 시각화를 시작할 수 있습니다." }, "data-sources": { "datasource-add-button": { @@ -6695,7 +6695,7 @@ "test": "테스트" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "또는 이 작업을 건너뛰고 <6>Grafana Cloud 평생 무료 플랜을 통해 Grafana Labs에서 완전 관리되고 확장 가능하며 호스팅되는 데이터 소스인 {{mainDS}}(및 {{extraDS}})을(를) 받으세요.", "title-alert": "아래에서 {{mainDS}} 데이터 소스를 구성하세요" }, "dashboards-table": { @@ -7678,7 +7678,7 @@ "math": { "available-math-functions": "사용 가능한 수학 함수", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "<2>수식에 대한 추가 문서를 참조하세요.", "tooltip-title": "수학 연산자", "tooltip-trigger": "표현식" }, @@ -7704,11 +7704,24 @@ "label-upsample": "업샘플", "tooltip-s-m-h": "10초, 1분, 30분, 1시간" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "쿼리 실행", "modal-title": "SQL 편집기", "tooltip-experimental": "SQL 표현식 LLM 통합 기능은 실험 단계입니다. 문제가 발생하면 Grafana 팀에 보고해 주세요." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "입력" } @@ -10881,10 +10894,10 @@ "placeholder-optional": "(선택 사항)", "role": "역할", "submit": "제출", - "tooltip": "" + "tooltip": "이제 '기본 역할 없음' 옵션을 선택하고 사용자의 필요에 맞게 권한을 추가할 수 있습니다. 자세한 내용은 <1>문서를 참조하세요." }, "user-invite-page": { - "sub-title": "", + "sub-title": "초대장을 보내거나 기존 Grafana 사용자를<1> {{orgName}} 조직에 추가하세요.", "text": { "invite-user": "사용자 초대" } @@ -12328,7 +12341,7 @@ }, "menu": { "clear-button": "전체 초기화", - "tooltip": "" + "tooltip": "이제 '기본 역할 없음' 옵션을 선택하고 사용자의 필요에 맞게 권한을 추가할 수 있습니다. 자세한 내용은 <1>문서를 참조하세요." }, "menu-aria-label": "역할 선택기 메뉴", "menu-group-option-aria-label": "역할 선택기 옵션", @@ -12653,7 +12666,7 @@ "info-text": "아래 옵션으로 사용자 정의된 이 대시보드 또는 패널에 대한 직접 링크를 생성합니다.", "link-url": "링크 URL", "render-alert": "이미지 렌더러 플러그인 미설치", - "render-instructions": "", + "render-instructions": "이미지를 렌더링하려면 <2>Grafana 이미지 렌더러 플러그인을 설치해야 합니다. 플러그인을 설치하려면 Grafana 관리자에게 문의하세요.", "rendered-image": "렌더링된 이미지에 직접 연결", "save-alert": "대시보드 저장되지 않음", "save-dashboard": "패널 이미지를 렌더링하려면 먼저 대시보드를 저장해야 합니다.", @@ -13693,7 +13706,7 @@ "label-format": "형식", "label-set-timezone": "시간대 설정", "label-time-field": "시간 필드", - "tooltip-format": "", + "tooltip-format": "<2>Moment.js 형식 문자열로 지정된 필드의 출력 형식입니다.", "tooltip-timezone-manually": "날짜의 시간대를 수동으로 설정합니다" }, "format-time-transformer-editor": { diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index fae593379b6..f1236285a6b 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -2081,7 +2081,7 @@ "warning-1": "Als je dit meldingsbeleid verwijdert, wordt dit permanent verwijderd.", "warning-2": "Weet je zeker dat je dit beleid wil verwijderen?" }, - "filter-description": "", + "filter-description": "Filter meldingsbeleid met behulp van een door komma's gescheiden lijst met matchers, bijv.:<1>severity=critical, region=EMEA", "generated-policies": "Automatisch gegenereerd beleid", "matchers": "Matchers", "metadata": { @@ -2808,7 +2808,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Waarschuwingssjabloongegevens", - "only-in-alert": "Alleen beschikbaar in de context van een waarschuwing (bijv. in .Waarschuwingslus)" + "only-in-alert": "" }, "available-context-notification": "Beschikbaar in het kader van een melding.", "notification-template-data": "Meldingssjabloongegevens" @@ -3245,7 +3245,7 @@ "auth-settings": "Auth-instellingen" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Auth-instellingen configureren. Lees meer in onze <2>documentatie." }, "auth-drawer-unconnected": { "advanced-auth": "Geavanceerde auth", @@ -3279,7 +3279,7 @@ "allowed-organizations-description": "Lijst van door komma's of spaties gescheiden organisaties. De gebruiker moet lid zijn van ten minste\néén organisatie om in te loggen.", "allowed-organizations-label": "Toegestane organisaties", "allowed-organizations-placeholder": "Voer organisaties in (my-team, myteam...) en druk op Enter om toe te voegen", - "api-url-description": "", + "api-url-description": "Het eindpunt voor gebruikersinformatie van je OAuth2-provider. Informatie die door dit eindpunt wordt geretourneerd, moet compatibel zijn met <2>OpenID-gebruikersinformatie.", "api-url-required": "Dit veld moet een geldige URL zijn, indien ingesteld.", "auth-style-description": "Het bepaalt hoe '{{ clientIDLabel }}' en '{{ clientSecretLabel }}' worden verzonden naar de Oauth2-provider. Standaard is AutoDetect.", "auth-style-label": "Auth-stijl", @@ -3424,7 +3424,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Je auth-instellingen beheren en single sign-on configureren. Lees meer in onze <2>documentatie." }, "bar-chart": { "warn": { @@ -4844,7 +4844,7 @@ "add-visualization-body": "Selecteer een gegevensbron en zoek en visualiseer je gegevens met grafieken, statistieken en tabellen of maak lijsten, prijsverlagingen en andere widgets.", "add-visualization-button": "Visualisatie toevoegen", "add-visualization-header": "Begin je nieuwe dashboard door een visualisatie toe te voegen", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Importeer dashboards uit bestanden of <1>grafana.com.", "import-a-dashboard-header": "Dashboard importeren", "import-dashboard-button": "Dashboard importeren", "show-less-dashboards": "", @@ -6623,7 +6623,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Klik <2>hier voor meer informatie over deze fout.", - "success-more-details-links": "" + "success-more-details-links": "Vervolgens kun je beginnen met het visualiseren van gegevens door <2> een dashboard te bouwen of door gegevens op te vragen in de <5> Verkennen-weergave." }, "data-sources": { "datasource-add-button": { @@ -6719,7 +6719,7 @@ "test": "Testen" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Of bespaar je de moeite en krijg {{mainDS}} (en {{extraDS}}) als volledig beheerde, schaalbare en gehoste gegevensbronnen van Grafana Labs met het <6>altijd gratis Grafana Cloud-abonnement.", "title-alert": "Configureer hieronder je gegevensbron voor {{mainDS}}" }, "dashboards-table": { @@ -7704,7 +7704,7 @@ "math": { "available-math-functions": "Beschikbare wiskundige functies", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Zie onze aanvullende documentatie over <2>wiskundige expressies.", "tooltip-title": "Wiskundige operator", "tooltip-trigger": "Expressie" }, @@ -7730,11 +7730,24 @@ "label-upsample": "Upsample", "tooltip-s-m-h": "10s, 1m, 30m, 1u" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Query uitvoeren", "modal-title": "SQL-editor", "tooltip-experimental": "SQL Expressions LLM-integratie is experimenteel. Meld eventuele problemen aan het Grafana-team." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Invoer" } @@ -10928,10 +10941,10 @@ "placeholder-optional": "(optioneel)", "role": "Rol", "submit": "Verzenden", - "tooltip": "" + "tooltip": "Je kunt nu de optie 'Geen basisrol' selecteren en toestemmingen toevoegen aan je aangepaste behoeften. Meer informatie vind je in <1>onze documentatie." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Stuur uitnodiging of voeg een bestaande Grafana-gebruiker toe aan de organisatie.<1> {{orgName}}", "text": { "invite-user": "Gebruiker uitnodigen" } @@ -12383,7 +12396,7 @@ }, "menu": { "clear-button": "Alles wissen", - "tooltip": "" + "tooltip": "Je kunt nu de optie 'Geen basisrol' selecteren en toestemmingen toevoegen aan je aangepaste behoeften. Meer informatie vind je in <1>onze documentatie." }, "menu-aria-label": "Menu rolkiezer", "menu-group-option-aria-label": "Optie rolkiezer", @@ -12709,7 +12722,7 @@ "info-text": "Maak een directe link naar dit dashboard of paneel, aangepast met de onderstaande opties.", "link-url": "Link-URL", "render-alert": "Afbeeldingweergave-plug-in niet geïnstalleerd", - "render-instructions": "", + "render-instructions": "Om een afbeelding weer te geven, moet je de <2>Grafana- afbeeldingsweergave-plug-in installeren. Neem contact op met je Grafana-beheerder om de plug-in te installeren.", "rendered-image": "Directe link naar gerenderde afbeelding", "save-alert": "Dashboard is niet opgeslagen", "save-dashboard": "Om een paneelafbeelding weer te geven, moet je eerst het dashboard opslaan.", @@ -13750,7 +13763,7 @@ "label-format": "Formaat", "label-set-timezone": "Tijdzone instellen", "label-time-field": "Tijdveld", - "tooltip-format": "", + "tooltip-format": "De uitvoerindeling voor het veld dat is opgegeven als een <2>Moment.js-indelingsreeks.", "tooltip-timezone-manually": "Stel de tijdzone van de datum handmatig in" }, "format-time-transformer-editor": { diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 581e3de181b..1a5897fd7ed 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -2093,7 +2093,7 @@ "warning-1": "Usunięcie tego zbioru zasad dot. powiadomień jest nieodwracalne.", "warning-2": "Na pewno chcesz usunąć te zasady?" }, - "filter-description": "", + "filter-description": "Filtruj zasady powiadomień, używając oddzielonej przecinkami listy dopasowań, np.:<1>severity=critical, region=EMEA", "generated-policies": "Zasady generowane automatycznie", "matchers": "Dopasowania", "metadata": { @@ -2832,7 +2832,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Dane szablonu alertu", - "only-in-alert": "Dostępne tylko w kontekście alertu (np. wewnątrz pętli .Alerts)" + "only-in-alert": "" }, "available-context-notification": "Dostępne w kontekście powiadomienia.", "notification-template-data": "Dane szablonu powiadomienia" @@ -3269,7 +3269,7 @@ "auth-settings": "Ustawienia uwierzytelniania" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Skonfiguruj ustawienia uwierzytelniania. Więcej informacji znajdziesz w <2>dokumentacji." }, "auth-drawer-unconnected": { "advanced-auth": "Zaawansowane uwierzytelnianie", @@ -3303,7 +3303,7 @@ "allowed-organizations-description": "Lista organizacji rozdzielona przecinkami lub spacjami. Użytkownik musi należeć do co najmniej \njednej organizacji, aby się zalogować.", "allowed-organizations-label": "Dozwolone organizacje", "allowed-organizations-placeholder": "Wprowadź organizacje (mój-zespół, mój_zespół itp.) i naciśnij klawisz Enter, aby je dodać", - "api-url-description": "", + "api-url-description": "Punkt końcowy dostawcy OAuth2 udostępniający informacje o użytkownikach. Informacje zwracane przez ten punkt końcowy muszą być zgodne z formatem <2>OpenID UserInfo.", "api-url-required": "To pole musi zawierać prawidłowy adres URL, jeśli jest ustawiony.", "auth-style-description": "Określa, w jaki sposób „{{ clientIDLabel }}” i „{{ clientSecretLabel }}” są wysyłane do dostawcy OAuth2. Domyślnie jest to tryb automatycznego wykrywania.", "auth-style-label": "Styl uwierzytelniania", @@ -3448,7 +3448,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Zarządzaj ustawieniami uwierzytelniania i skonfiguruj logowanie jednokrotne. Więcej informacji znajdziesz w <2>dokumentacji." }, "bar-chart": { "warn": { @@ -4884,7 +4884,7 @@ "add-visualization-body": "Wybierz źródło danych, a następnie wyszukaj i zwizualizuj dane na wykresach, w statystykach i tabelach lub utwórz listy, korzystaj ze znaczników markdown i innych widżetów.", "add-visualization-button": "Dodaj wizualizację", "add-visualization-header": "Dodaj wizualizację, aby rozpocząć tworzenie nowego pulpitu", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Zaimportuj pulpity z plików lub witryny <2>grafana.com.", "import-a-dashboard-header": "Importuj pulpit", "import-dashboard-button": "Importuj pulpit", "show-less-dashboards": "", @@ -6671,7 +6671,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Kliknij <2>tutaj, aby dowiedzieć się więcej o tym błędzie.", - "success-more-details-links": "" + "success-more-details-links": "Następnie możesz rozpocząć wizualizację danych, <2>tworząc pulpit lub tworząc zapytanie dotyczące danych w widoku <5>Eksploruj." }, "data-sources": { "datasource-add-button": { @@ -6767,7 +6767,7 @@ "test": "Test" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Możesz ułatwić sobie życie i wybrać {{mainDS}} (oraz {{extraDS}}) jako w pełni zarządzane, skalowalne źródła danych hostowane przez Grafana Labs w ramach <6>planu Grafana Cloud, który będzie zawsze bezpłatny.", "title-alert": "Skonfiguruj źródło danych {{mainDS}} poniżej" }, "dashboards-table": { @@ -7756,7 +7756,7 @@ "math": { "available-math-functions": "Dostępne funkcje matematyczne", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Zapoznaj się z naszą dodatkową dokumentacją na temat <2>wyrażeń matematycznych.", "tooltip-title": "Operator matematyczny", "tooltip-trigger": "Wyrażenie" }, @@ -7782,11 +7782,24 @@ "label-upsample": "Zwiększ próbkowanie", "tooltip-s-m-h": "10s, 1m, 30m, 1h" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Uruchom zapytanie", "modal-title": "Edytor SQL", "tooltip-experimental": "Integracja LLM z wyrażeniami SQL jest eksperymentalna. Wszelkie problemy zgłaszaj zespołowi Grafana." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Wejście" } @@ -11022,10 +11035,10 @@ "placeholder-optional": "(opcjonalnie)", "role": "Rola", "submit": "Prześlij", - "tooltip": "" + "tooltip": "Możesz teraz wybrać opcję „Brak podstawowej roli” i dodać uprawnienia zgodnie z własnymi potrzebami. Więcej informacji znajdziesz w <1>dokumentacji." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Wyślij zaproszenie lub dodaj istniejącego użytkownika Grafany do organizacji.<1> {{orgName}}", "text": { "invite-user": "Zaproś użytkownika" } @@ -12493,7 +12506,7 @@ }, "menu": { "clear-button": "Wyczyść wszystko", - "tooltip": "" + "tooltip": "Możesz teraz wybrać opcję „Brak podstawowej roli” i dodać uprawnienia zgodnie z własnymi potrzebami. Więcej informacji znajdziesz w <1>dokumentacji." }, "menu-aria-label": "Menu selektora ról", "menu-group-option-aria-label": "Opcja selektora ról", @@ -12821,7 +12834,7 @@ "info-text": "Utwórz bezpośredni link do tego pulpitu lub panelu, dostosowany za pomocą poniższych opcji.", "link-url": "Link URL", "render-alert": "Nie zainstalowano wtyczki renderowania obrazów", - "render-instructions": "", + "render-instructions": "Aby wyrenderować obraz, musisz zainstalować <2>wtyczkę do renderowania obrazów w Grafanie. Skontaktuj się z administratorem usługi Grafana, aby zainstalować wtyczkę.", "rendered-image": "Obraz renderowany z bezpośredniego linku", "save-alert": "Nie zapisano pulpitu", "save-dashboard": "Aby wyrenderować obraz panelu, musisz najpierw zapisać pulpit.", @@ -13864,7 +13877,7 @@ "label-format": "Format", "label-set-timezone": "Ustaw strefę czasową", "label-time-field": "Pole czasu", - "tooltip-format": "", + "tooltip-format": "Format wyjściowy pola określonego jako <2>Ciąg formatowania Moment.js.", "tooltip-timezone-manually": "Ręcznie ustaw strefę czasową daty" }, "format-time-transformer-editor": { diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 9fb0c60ec94..419d550fe09 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -2081,7 +2081,7 @@ "warning-1": "A exclusão desta política de notificação irá removê-la permanentemente.", "warning-2": "Tem certeza de que deseja excluir esta política?" }, - "filter-description": "", + "filter-description": "Filtre as políticas de notificação usando uma lista de correspondências separadas por vírgulas, por exemplo:<1>severity=critical, region=EMEA", "generated-policies": "Políticas geradas automaticamente", "matchers": "Correspondências", "metadata": { @@ -2808,7 +2808,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Dados do modelo de alerta", - "only-in-alert": "Disponível apenas quando estiver no contexto de um Alerta (por exemplo, dentro do .Loop de alertas)" + "only-in-alert": "" }, "available-context-notification": "Disponível no contexto de uma notificação.", "notification-template-data": "Dados do modelo de notificação" @@ -3245,7 +3245,7 @@ "auth-settings": "Configurações de autenticação" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Defina as configurações de autenticação. Saiba mais na nossa <2>documentação." }, "auth-drawer-unconnected": { "advanced-auth": "Autenticação avançada", @@ -3279,7 +3279,7 @@ "allowed-organizations-description": "Lista de organizações separadas por vírgulas ou espaços. O usuário deve ser membro \nde pelo menos uma organização para fazer login.", "allowed-organizations-label": "Organizações permitidas", "allowed-organizations-placeholder": "Insira as organizações (minha-equipe, minhaequipe...) e pressione Enter para adicioná-las", - "api-url-description": "", + "api-url-description": "O endpoint de informações do usuário do seu provedor OAuth2. As informações que retornaram com este endpoint devem ser compatíveis com <2>OpenID UserInfo.", "api-url-required": "Este campo deve ser uma URL válida, se definido.", "auth-style-description": "Ele determina como \"{{ clientIDLabel }}\" e \"{{ clientSecretLabel }}\" são enviados para o provedor Oauth2. O padrão é AutoDetect.", "auth-style-label": "Estilo de autenticação", @@ -3424,7 +3424,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Gerencie suas configurações de autenticação e configure a autenticação única. Saiba mais na nossa <2>documentação." }, "bar-chart": { "warn": { @@ -4844,7 +4844,7 @@ "add-visualization-body": "Selecione uma fonte de dados e consulte e visualize seus dados com gráficos, estatísticas e tabelas ou crie listas, markdowns e outros widgets.", "add-visualization-button": "Adicionar visualização", "add-visualization-header": "Comece seu novo painel de controle adicionando uma visualização", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Importe painéis por meio de arquivos ou do <2>grafana.com.", "import-a-dashboard-header": "Importar um painel de controle", "import-dashboard-button": "Importar painel de controle", "show-less-dashboards": "", @@ -6623,7 +6623,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Clique <2>aqui para saber mais sobre esse erro.", - "success-more-details-links": "" + "success-more-details-links": "Em seguida, você pode começar a visualizar os dados <2>criando um painel de controle ou consultando os dados na <5>Visualização de exploração." }, "data-sources": { "datasource-add-button": { @@ -6719,7 +6719,7 @@ "test": "Teste" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Ou evite essa tarefa e obtenha {{mainDS}} (e {{extraDS}}) como fontes de dados totalmente gerenciadas, escaláveis e hospedadas da Grafana Labs com o <6>plano gratuito vitalício da Grafana Cloud.", "title-alert": "Configure sua fonte de {{mainDS}} dados abaixo" }, "dashboards-table": { @@ -7704,7 +7704,7 @@ "math": { "available-math-functions": "Funções matemáticas disponíveis", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Consulte nossa documentação adicional sobre <2>expressões matemáticas.", "tooltip-title": "Operador matemático", "tooltip-trigger": "Expressão" }, @@ -7730,11 +7730,24 @@ "label-upsample": "Aumentar taxa de amostragem", "tooltip-s-m-h": "10s, 1m, 30m, 1h" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Executar consulta", "modal-title": "Editor de SQL", "tooltip-experimental": "A integração do LLM de expressões SQL está em fase de testes. Informe à equipe do Grafana se surgir algum problema." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Entrada" } @@ -10928,10 +10941,10 @@ "placeholder-optional": "(opcional)", "role": "Função", "submit": "Enviar", - "tooltip": "" + "tooltip": "Agora você pode selecionar a opção \"Sem função básica\" e adicionar permissões personalizadas com base nas suas necessidades. Você pode encontrar mais informações em <1>nossa documentação." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Envie um convite ou adicione um usuário que já possui conta na Grafana à organização.<1> {{orgName}}", "text": { "invite-user": "Convidar usuário" } @@ -12383,7 +12396,7 @@ }, "menu": { "clear-button": "Limpar tudo", - "tooltip": "" + "tooltip": "Agora você pode selecionar a opção \"Sem função básica\" e adicionar permissões personalizadas com base nas suas necessidades. Você pode encontrar mais informações em <1>nossa documentação." }, "menu-aria-label": "Menu do seletor de função", "menu-group-option-aria-label": "Opção do seletor de função", @@ -12709,7 +12722,7 @@ "info-text": "Crie um link direto para este painel de controle ou painel, personalizado com as opções abaixo.", "link-url": "URL do link", "render-alert": "Plug-in renderizador de imagem não instalado", - "render-instructions": "", + "render-instructions": "Para renderizar uma imagem, você precisa instalar o <2>plug-in renderizador de imagens do Grafana. Entre em contato com o administrador do Grafana para instalar o plug-in.", "rendered-image": "Link direto de imagem renderizada", "save-alert": "O painel de controle não está salvo", "save-dashboard": "Para renderizar uma imagem do painel, você deve salvar o painel de controle primeiro.", @@ -13750,7 +13763,7 @@ "label-format": "Formato", "label-set-timezone": "Definir fuso horário", "label-time-field": "Campo de hora", - "tooltip-format": "", + "tooltip-format": "O formato de saída para o campo especificado como uma <2>sequência de caracteres de formato Moment.js.", "tooltip-timezone-manually": "Defina o fuso horário da data manualmente" }, "format-time-transformer-editor": { diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index edb3fcc3a16..28b728aee0f 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -2081,7 +2081,7 @@ "warning-1": "A eliminação desta política de notificação irá removê-la permanentemente.", "warning-2": "Tem a certeza de que pretende eliminar esta política?" }, - "filter-description": "", + "filter-description": "Filtre as políticas de notificação utilizando uma lista de correspondências separadas por vírgulas, por ex.:<1>gravidade=crítica, região=EMEA", "generated-policies": "Políticas geradas automaticamente", "matchers": "Correspondências", "metadata": { @@ -2808,7 +2808,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Dados do modelo de alerta", - "only-in-alert": "Disponível apenas quando está no contexto de um alerta (por exemplo, dentro de .Ciclo de alertas)" + "only-in-alert": "" }, "available-context-notification": "Disponível no contexto de uma notificação.", "notification-template-data": "Dados do modelo de notificação" @@ -3245,7 +3245,7 @@ "auth-settings": "Definições de autenticação" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Configurar as definições de autenticação. Saiba mais na nossa <2>documentação." }, "auth-drawer-unconnected": { "advanced-auth": "Autenticação avançada", @@ -3279,7 +3279,7 @@ "allowed-organizations-description": "Lista de organizações separadas por vírgulas ou espaços. O utilizador deve ser membro \nde pelo menos uma organização para iniciar sessão.", "allowed-organizations-label": "Organizações permitidas", "allowed-organizations-placeholder": "Introduza organizações (my-team, myteam...) e prima Enter para adicionar", - "api-url-description": "", + "api-url-description": "O ponto final de informações do utilizador do seu fornecedor OAuth2. As informações obtidas por este ponto final devem ser compatíveis com <2>OpenID UserInfo.", "api-url-required": "Este campo deve ser um URL válido, se definido.", "auth-style-description": "Determina como \"{{ clientIDLabel }}\" e \"{{ clientSecretLabel }}\" são enviados para o fornecedor Oauth2. A predefinição é AutoDetect.", "auth-style-label": "Estilo de autenticação", @@ -3424,7 +3424,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Faça a gestão das suas definições de autenticação e configure o início de sessão único. Saiba mais na nossa <2>documentação." }, "bar-chart": { "warn": { @@ -4844,7 +4844,7 @@ "add-visualization-body": "Selecione uma origem de dados e, em seguida, consulte e visualize os seus dados com gráficos, estatísticas e tabelas ou crie listas, remarcações e outros widgets.", "add-visualization-button": "Adicionar visualização", "add-visualization-header": "Inicie o seu novo painel de controlo ao adicionar uma visualização", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Importe painéis de controlo de ficheiros ou de <2>grafana.com.", "import-a-dashboard-header": "Importar um painel de controlo", "import-dashboard-button": "Importar painel de controlo", "show-less-dashboards": "", @@ -6623,7 +6623,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Clique <2>aqui para saber mais sobre este erro.", - "success-more-details-links": "" + "success-more-details-links": "Em seguida, pode começar a visualizar os dados <2>criando um painel de controlo ou consultando os dados na <5>vista Explorar." }, "data-sources": { "datasource-add-button": { @@ -6719,7 +6719,7 @@ "test": "Teste" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Ou evite o esforço e obtenha {{mainDS}} (e {{extraDS}}) como origens de dados totalmente geridas, escaláveis e hospedadas da Grafana Labs com o <6>plano Grafana Cloud gratuito para sempre.", "title-alert": "Configurar a sua origem de dados {{mainDS}} abaixo" }, "dashboards-table": { @@ -7704,7 +7704,7 @@ "math": { "available-math-functions": "Funções matemáticas disponíveis", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Consulte a nossa documentação adicional sobre <2>expressões matemáticas.", "tooltip-title": "Operador matemático", "tooltip-trigger": "Expressão" }, @@ -7730,11 +7730,24 @@ "label-upsample": "Aumentar a quantidade de amostras", "tooltip-s-m-h": "10s, 1m, 30m, 1h" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Executar consulta", "modal-title": "Editor SQL", "tooltip-experimental": "A integração de LLM de expressões SQL é experimental. Comunique quaisquer problemas à equipa da Grafana." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Entrada" } @@ -10928,10 +10941,10 @@ "placeholder-optional": "(opcional)", "role": "Função", "submit": "Enviar", - "tooltip": "" + "tooltip": "Agora pode selecionar a opção \"Sem função básica\" e adicionar permissões às suas necessidades personalizadas. Pode encontrar mais informações na <1>nossa documentação." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Envie um convite ou adicione um utilizador Grafana existente à organização.<1> {{orgName}}", "text": { "invite-user": "Convidar utilizador" } @@ -12383,7 +12396,7 @@ }, "menu": { "clear-button": "Limpar tudo", - "tooltip": "" + "tooltip": "Agora pode selecionar a opção \"Sem função básica\" e adicionar permissões às suas necessidades personalizadas. Pode encontrar mais informações na <1>nossa documentação." }, "menu-aria-label": "Menu do selecionador de funções", "menu-group-option-aria-label": "Opção do selecionador de funções", @@ -12709,7 +12722,7 @@ "info-text": "Crie um link direto para este painel de controlo ou painel, personalizado com as opções abaixo.", "link-url": "URL do link", "render-alert": "Plugin de renderização de imagem não instalado", - "render-instructions": "", + "render-instructions": "Para renderizar uma imagem, tem de instalar o <2>plugin de renderização de imagens Grafana. Entre em contacto com o seu administrador Grafana para instalar o plugin.", "rendered-image": "Link direto para imagem renderizada", "save-alert": "O painel de controlo não está guardado", "save-dashboard": "Para processar uma imagem de painel, tem de guardar o painel de controlo primeiro.", @@ -13750,7 +13763,7 @@ "label-format": "Formato", "label-set-timezone": "Definir o fuso horário", "label-time-field": "Campo de hora", - "tooltip-format": "", + "tooltip-format": "O formato de saída para o campo especificado como uma <2>sequência de caracteres de formato Moment.js.", "tooltip-timezone-manually": "Defina o fuso horário da data manualmente" }, "format-time-transformer-editor": { diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index a7a264ece15..1d05879899a 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -2093,7 +2093,7 @@ "warning-1": "Политика уведомления будет безвозвратно удалена.", "warning-2": "Действительно удалить политику?" }, - "filter-description": "", + "filter-description": "Фильтруйте политики уведомления, используя список сопоставлений, разделенных запятыми, например, <1>серьезность=критическая, регион=EMEA", "generated-policies": "Автоматически генерируемые политики", "matchers": "Средства сопоставления", "metadata": { @@ -2832,7 +2832,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Данные шаблона оповещения", - "only-in-alert": "Доступно только в контексте оповещения (например, внутрицикла оповещений)" + "only-in-alert": "" }, "available-context-notification": "Доступно в контексте уведомления.", "notification-template-data": "Данные шаблона уведомления" @@ -3269,7 +3269,7 @@ "auth-settings": "Параметры аутентификации" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Настройка параметров аутентификации Подробнее см. в нашей <2>документации." }, "auth-drawer-unconnected": { "advanced-auth": "Расширенная аутентификация", @@ -3303,7 +3303,7 @@ "allowed-organizations-description": "Список организаций, разделенных запятыми или пробелами. Чтобы войти в систему, пользователь должен \nбыть участником хотя бы одной организации.", "allowed-organizations-label": "Разрешенные организации", "allowed-organizations-placeholder": "Введите названия организаций (my-team, myteam...) и нажмите «Enter», чтобы их добавить", - "api-url-description": "", + "api-url-description": "Конечная точка информации о пользователе вашего поставщика OAuth2. Информация, возвращаемая этой конечной точкой, должна быть совместима с <2>OpenID UserInfo.", "api-url-required": "В поле должен быть указан действительный URL-адрес (если он настроен).", "auth-style-description": "Определяет, каким образом «{{ clientIDLabel }}» и «{{ clientSecretLabel }}» отправляются поставщику Oauth2. По умолчанию используется автоопределение.", "auth-style-label": "Метод авторизации", @@ -3448,7 +3448,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Управление параметрами аутентификации и настройка единого входа Подробнее см. в нашей <2>документации." }, "bar-chart": { "warn": { @@ -4884,7 +4884,7 @@ "add-visualization-body": "Выберите источник данных, а затем запрашивайте и визуализируйте свои данные с помощью диаграмм, статистики и таблиц или создавайте списки, разметки и другие виджеты.", "add-visualization-button": "Добавить визуализацию", "add-visualization-header": "Создание нового дашборда с добавлением визуализации", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Импортируйте дашборды из файлов или с <1>grafana.com.", "import-a-dashboard-header": "Импорт дашборда", "import-dashboard-button": "Импорт дашборда", "show-less-dashboards": "", @@ -6671,7 +6671,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Чтобы узнать больше об этой ошибке, нажмите <2>здесь.", - "success-more-details-links": "" + "success-more-details-links": "Затем вы можете начать визуализировать данные, <2>создав панель или запросив данные в <5>представлении Explore." }, "data-sources": { "datasource-add-button": { @@ -6767,7 +6767,7 @@ "test": "Тестирование" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Или не тратьте время и получите {{mainDS}} (и {{extraDS}}) в качестве полностью управляемых, масштабируемых и размещенных источников данных от Grafana Labs с <6>бессрочным бесплатным планом Grafana Cloud.", "title-alert": "Настройте свой источник данных {{mainDS}} ниже" }, "dashboards-table": { @@ -7756,7 +7756,7 @@ "math": { "available-math-functions": "Доступные математические функции", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Подробнее о <2>математических выражениях см. в нашей дополнительной документации.", "tooltip-title": "Математический оператор", "tooltip-trigger": "Выражение" }, @@ -7782,11 +7782,24 @@ "label-upsample": "Увеличить частоту выборки", "tooltip-s-m-h": "10 с, 1 мин, 30 мин, 1 ч" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Выполнить запрос", "modal-title": "Редактор SQL", "tooltip-experimental": "Интеграция LLM с SQL-выражениями является экспериментальной. При обнаружении проблем свяжитесь с командой Grafana." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Ввод" } @@ -11022,10 +11035,10 @@ "placeholder-optional": "(необязательно)", "role": "Роль", "submit": "Отправить", - "tooltip": "" + "tooltip": "Теперь вы можете выбрать параметр «Нет основных ролей» и добавить разрешения в соответствии со своими потребностями. Дополнительную информацию можно найти в <1>нашей документации." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Отправьте приглашение или добавьте существующего пользователя Grafana в организацию <1>{{orgName}}", "text": { "invite-user": "Пригласить пользователя" } @@ -12493,7 +12506,7 @@ }, "menu": { "clear-button": "Очистить всё", - "tooltip": "" + "tooltip": "Теперь вы можете выбрать параметр «Нет основных ролей» и добавить разрешения в соответствии со своими потребностями. Дополнительную информацию можно найти в <1>нашей документации." }, "menu-aria-label": "Меню указателя роли", "menu-group-option-aria-label": "Параметр указателя роли", @@ -12821,7 +12834,7 @@ "info-text": "Создайте прямую ссылку на дашборд или панель, настроенные с помощью приведенных ниже параметров.", "link-url": "Привязать URL-адрес", "render-alert": "Плагин визуализации изображений не установлен", - "render-instructions": "", + "render-instructions": "Чтобы визуализировать изображение, необходимо установить <2>плагин визуализации изображений Grafana. Чтобы установить плагин, обратитесь к своему администратору Grafana.", "rendered-image": "Прямая ссылка на визуализированное изображение", "save-alert": "Дашборд не сохранен", "save-dashboard": "Чтобы визуализировать изображение панели, сначала необходимо сохранить дашборд.", @@ -13864,7 +13877,7 @@ "label-format": "Формат", "label-set-timezone": "Установить часовой пояс", "label-time-field": "Поле времени", - "tooltip-format": "", + "tooltip-format": "Формат вывода для поля, указанного в виде <2>строки в формате Moment.js.", "tooltip-timezone-manually": "Установите часовой пояс вручную" }, "format-time-transformer-editor": { diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 9a1940aa2b4..95e2ae6130e 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -2081,7 +2081,7 @@ "warning-1": "Om du tar bort denna aviseringspolicy tas den bort permanent.", "warning-2": "Är du säker på att du vill ta bort denna policy?" }, - "filter-description": "", + "filter-description": "Filtrera aviseringspolicyer med hjälp av en kommaseparerad lista över matchare, till exempel:<1>severity=critical, region=EMEA", "generated-policies": "Automatiskt genererade policyer", "matchers": "Matchare", "metadata": { @@ -2808,7 +2808,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Larmmalldata", - "only-in-alert": "Endast tillgängligt när det är i samband med ett larm (t.ex. inuti .Alerts-loop)" + "only-in-alert": "" }, "available-context-notification": "Tillgängligt i samband med ett meddelande.", "notification-template-data": "Aviseringsmalldata" @@ -3245,7 +3245,7 @@ "auth-settings": "Autentiseringsinställningar" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Konfigurera autentiseringsinställningar. Läs mer i vår <2>dokumentation." }, "auth-drawer-unconnected": { "advanced-auth": "Avancerad autentisering", @@ -3279,7 +3279,7 @@ "allowed-organizations-description": "Lista över kommaseparerade eller mellanslagsavgränsade organisationer. Användaren bör vara medlem \n i minst en organisation för att logga in.", "allowed-organizations-label": "Tillåtna organisationer", "allowed-organizations-placeholder": "Ange organisationer (mitt-team, mittteam …) och tryck på returtangenten för att lägga till", - "api-url-description": "", + "api-url-description": "Slutpunkten för användarinformation för din OAuth2-leverantör. Information som returneras av denna slutpunkt måste vara kompatibel med <2>OpenID UserInfo.", "api-url-required": "Om detta fält är inställt måste det vara en giltig webbadress.", "auth-style-description": "Det avgör hur ”{{ clientIDLabel }}” och ”{{ clientSecretLabel }}” skickas till Oauth2-leverantören. Standard är AutoDetect.", "auth-style-label": "Autentiseringsstil", @@ -3424,7 +3424,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Hantera dina autentiseringsinställningar och konfigurera enkel inloggning. Läs mer i vår <2>dokumentation." }, "bar-chart": { "warn": { @@ -4844,7 +4844,7 @@ "add-visualization-body": "Välj en datakälla och sedan en fråga och visualisera dina data med diagram, statistik och tabeller eller skapa listor, markdown-dokument och andra widgetar.", "add-visualization-button": "Lägg till visualisering", "add-visualization-header": "Starta din nya instrumentpanel genom att lägga till en visualisering", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Importera instrumentpaneler från filer eller från <2>grafana.com.", "import-a-dashboard-header": "Importera en instrumentpanel", "import-dashboard-button": "Importera instrumentpanel", "show-less-dashboards": "", @@ -6623,7 +6623,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Klicka <2>här för att läsa mer om detta fel.", - "success-more-details-links": "" + "success-more-details-links": "Därefter kan du börja visualisera data genom att <2>bygga en instrumentpanel eller genom att fråga efter data i <5>Utforska vy." }, "data-sources": { "datasource-add-button": { @@ -6719,7 +6719,7 @@ "test": "Test" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Eller hoppa över ansträngningen och få {{mainDS}} (och {{extraDS}}) som fullt hanterade, skalbara och värdbaserade datakällor från Grafana Labs med <6>Grafana Cloud-prenumerationen gratis för alltid.", "title-alert": "Konfigurera din {{mainDS}}-datakälla nedan" }, "dashboards-table": { @@ -7704,7 +7704,7 @@ "math": { "available-math-functions": "Tillgängliga matematikfunktioner", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "Se vår ytterligare dokumentation om <2>matematiska uttryck.", "tooltip-title": "Matematisk operator", "tooltip-trigger": "Uttryck" }, @@ -7730,11 +7730,24 @@ "label-upsample": "Sampla upp", "tooltip-s-m-h": "10 s, 1 m, 30 m, 1 h" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "Kör fråga", "modal-title": "SQL-redigerare", "tooltip-experimental": "Integreringen för SQL Expressions LLM är i ett experimentellt skede. Rapportera alla problem du stöter på till Grafana-teamet." }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Ingång" } @@ -10928,10 +10941,10 @@ "placeholder-optional": "(valfritt)", "role": "Roll", "submit": "Skicka in", - "tooltip": "" + "tooltip": "Du kan nu välja alternativet ”Ingen grundläggande roll” och lägga till behörigheter för dina anpassade behov. Du hittar mer information i <1>vår dokumentation." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Skicka inbjudan eller lägg till en befintlig Grafana-användare i organisationen.<1> {{orgName}}", "text": { "invite-user": "Bjud in användare" } @@ -12383,7 +12396,7 @@ }, "menu": { "clear-button": "Rensa alla", - "tooltip": "" + "tooltip": "Du kan nu välja alternativet ”Ingen grundläggande roll” och lägga till behörigheter för dina anpassade behov. Du hittar mer information i <1>vår dokumentation." }, "menu-aria-label": "Menyn Rollväljare", "menu-group-option-aria-label": "Alternativ för rollväljare", @@ -12709,7 +12722,7 @@ "info-text": "Skapa en direktlänk till denna instrumentpanel eller panel, anpassad med alternativen nedan.", "link-url": "Länkens webbadress", "render-alert": "Tilläggsprogram för bildrendering inte installerat", - "render-instructions": "", + "render-instructions": "För att rendera en bild måste du installera <2>Grafanas bildrenderingstillägg. Kontakta din Grafana-administratör för installation av tillägget.", "rendered-image": "Direktlänk till renderad bild", "save-alert": "Instrumentpanelen sparades inte", "save-dashboard": "För att rendera en panelbild måste du spara instrumentpanelen först.", @@ -13750,7 +13763,7 @@ "label-format": "Format", "label-set-timezone": "Ange tidszon", "label-time-field": "Tidsfält", - "tooltip-format": "", + "tooltip-format": "Utdataformatet för fältet anges som en <2>Moment.js-formatsträng.", "tooltip-timezone-manually": "Ställ in tidszonen för datumet manuellt" }, "format-time-transformer-editor": { diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index a081fdbd8d3..16e0520c9a7 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -2081,7 +2081,7 @@ "warning-1": "Bu bildirim politikası silindiğinde kalıcı olarak kaldırılacaktır.", "warning-2": "Bu politikayı silmek istediğinizden emin misiniz?" }, - "filter-description": "", + "filter-description": "Bildirim politikalarını, virgülle ayrılmış eşleşenler listesi kullanarak filtreleyin, örneğin: <1>severity=critical, region=EMEA", "generated-policies": "Otomatik olarak oluşturulan politikalar", "matchers": "Eşleşenler", "metadata": { @@ -2808,7 +2808,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "Uyarı şablonu verileri", - "only-in-alert": "Yalnızca bir Uyarı bağlamında kullanılabilir (örn.Alerts döngüsü içinde)" + "only-in-alert": "" }, "available-context-notification": "Bir bildirim bağlamında kullanılabilir.", "notification-template-data": "Bildirim şablonu verileri" @@ -3245,7 +3245,7 @@ "auth-settings": "Kimlik doğrulaması ayarları" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "Kimlik doğrulama ayarlarını yapılandırın. Daha fazla bilgi için <2>belgelerimize bakın." }, "auth-drawer-unconnected": { "advanced-auth": "Gelişmiş Kimlik Doğrulaması", @@ -3279,7 +3279,7 @@ "allowed-organizations-description": "Virgül veya boşlukla ayrılmış kuruluş listesi. Kullanıcının giriş yapabilmesi için en az bir\ngrubun üyesi olması gerekir.", "allowed-organizations-label": "İzin verilen kuruluşlar", "allowed-organizations-placeholder": "Kuruluşları girin (benim-ekibim, benimekibim...) ve eklemek için Enter tuşuna basın", - "api-url-description": "", + "api-url-description": "OAuth2 sağlayıcınızın kullanıcı bilgisi uç noktası. Bu uç noktadan dönen bilgiler <2>OpenID UserInfo ile uyumlu olmalıdır.", "api-url-required": "Bu alan ayarlanmışsa geçerli bir URL olmalıdır", "auth-style-description": "\"{{ clientIDLabel }}\" ve \"{{ clientSecretLabel }}\" bilgilerinin OAuth2 sağlayıcısına nasıl gönderileceğini belirler. Varsayılan Otomatik Algıla'dır.", "auth-style-label": "Kimlik doğrulama stili", @@ -3424,7 +3424,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "Kimlik doğrulama ayarlarınızı yönetin ve çoklu oturum açmayı yapılandırın. Daha fazla bilgi için <2>belgelerimize bakın." }, "bar-chart": { "warn": { @@ -4844,7 +4844,7 @@ "add-visualization-body": "Bir veri kaynağı seçin ve ardından grafikler, istatistikler ve tablolarla verilerinizi sorgulayın ve görselleştirin veya listeler, markdown'lar ve diğer widget'lar oluşturun.", "add-visualization-button": "Görselleştirme ekle", "add-visualization-header": "Bir görselleştirme ekleyerek yeni panonuzu başlatın", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "Panoları dosyalardan veya <2>grafana.com'dan içe aktarın.", "import-a-dashboard-header": "Bir panoyu içe aktar", "import-dashboard-button": "Panoyu içe aktar", "show-less-dashboards": "", @@ -6623,7 +6623,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "Bu hata hakkında daha fazla bilgi edinmek için <2>buraya tıklayın.", - "success-more-details-links": "" + "success-more-details-links": "Ardından, bir <2>pano oluşturarak veya <5>Keşfet görünümünde verileri sorgulayarak verileri görselleştirmeye başlayabilirsiniz." }, "data-sources": { "datasource-add-button": { @@ -6719,7 +6719,7 @@ "test": "Test" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "Veya uğraşmadan {{mainDS}} (ve {{extraDS}}) veri kaynaklarını Grafana Labs'tan tamamen yönetilen, ölçeklenebilir ve barındırılan <6>daima ücretsiz Grafana Cloud planıyla edinin.", "title-alert": "Aşağıdan {{mainDS}} veri kaynağınızı yapılandırın" }, "dashboards-table": { @@ -7704,7 +7704,7 @@ "math": { "available-math-functions": "Kullanılabilir matematiksel işlevler", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": " <2>Matematiksel ifadeler hakkında ek belgelerimize bakın.", "tooltip-title": "Matematik işleci", "tooltip-trigger": "İfade" }, @@ -7730,11 +7730,24 @@ "label-upsample": "Örneklemeyi artır", "tooltip-s-m-h": "10 sn, 1 dk, 30 dk, 1 sa" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "", "modal-title": "", "tooltip-experimental": "" }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "Girdi" } @@ -10928,10 +10941,10 @@ "placeholder-optional": "(isteğe bağlı)", "role": "Rol", "submit": "Gönder", - "tooltip": "" + "tooltip": "Artık \"Temel rol yok\" seçeneğini seçebilir ve izinleri özel ihtiyaçlarınıza göre ekleyebilirsiniz. Daha fazla bilgi için <1>belgelerimize göz atabilirsiniz." }, "user-invite-page": { - "sub-title": "", + "sub-title": "Davet gönderin veya mevcut bir Grafana kullanıcısını <1> {{orgName}} kuruluşuna ekleyin.", "text": { "invite-user": "Kullanıcı davet et" } @@ -12383,7 +12396,7 @@ }, "menu": { "clear-button": "Tümünü temizle", - "tooltip": "" + "tooltip": "Artık \"Temel rol yok\" seçeneğini seçebilir ve izinleri özel ihtiyaçlarınıza göre ekleyebilirsiniz. Daha fazla bilgi için <1>belgelerimize göz atabilirsiniz." }, "menu-aria-label": "Rol seçici menüsü", "menu-group-option-aria-label": "Rol seçme opsiyonu", @@ -12709,7 +12722,7 @@ "info-text": "Aşağıdaki seçeneklerle özelleştirilen bu panoya veya panele doğrudan bir bağlantı oluşturun.", "link-url": "Bağlantı URL'si", "render-alert": "Görüntü işleme eklentisi yüklü değil", - "render-instructions": "", + "render-instructions": "Bir görsel oluşturmak için <2>Grafana görüntü işleme eklentisini yüklemeniz gerekir. Lütfen eklentiyi yüklemek için Grafana yöneticinizle iletişime geçin.", "rendered-image": "Doğrudan bağlantılı oluşturulmuş görüntü", "save-alert": "Pano kaydedilmedi", "save-dashboard": "Panel görüntüsü oluşturmak için önce panoyu kaydetmelisiniz.", @@ -13750,7 +13763,7 @@ "label-format": "Biçim", "label-set-timezone": "Zaman dilimini ayarla", "label-time-field": "Zaman alanı", - "tooltip-format": "", + "tooltip-format": "Alan için çıktı biçimi, <2>Moment.js biçimlendirme dizesi olarak belirtilir.", "tooltip-timezone-manually": "Tarih için saat dilimini manuel olarak ayarlayın" }, "format-time-transformer-editor": { diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 481399a0bcb..4dccf320255 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -2075,7 +2075,7 @@ "warning-1": "删除此通知策略将永久移除它。", "warning-2": "您确定要删除此策略吗?" }, - "filter-description": "", + "filter-description": "使用半角逗号分隔的匹配器列表来筛选通知策略,例如:<1>severity=critical, region=EMEA", "generated-policies": "自动生成的策略", "matchers": "匹配器", "metadata": { @@ -2796,7 +2796,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "警报模板数据", - "only-in-alert": "仅在警报上下文中可用(例如在警报循环中)" + "only-in-alert": "" }, "available-context-notification": "在通知上下文中可用。", "notification-template-data": "通知模板数据" @@ -3233,7 +3233,7 @@ "auth-settings": "身份验证设置" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "配置身份验证设置。不妨在我们的<2>文档中了解更多信息。" }, "auth-drawer-unconnected": { "advanced-auth": "高级身份验证", @@ -3267,7 +3267,7 @@ "allowed-organizations-description": "以逗号或空格分隔的组织列表。用户应至少是一个\n组织的成员才能登录。", "allowed-organizations-label": "允许的组织", "allowed-organizations-placeholder": "输入组织(my-team、myteam...)并按 Enter 键添加", - "api-url-description": "", + "api-url-description": "OAuth2 提供商的用户信息端点。此端点返回的信息必须与 <2>OpenID UserInfo 兼容。", "api-url-required": "如果设置,此字段必须是有效的网址。", "auth-style-description": "它决定了如何将“{{ clientIDLabel }}”和“{{ clientSecretLabel }}”发送到 Oauth2 提供程序。默认值为 AutoDetect。", "auth-style-label": "身份验证样式", @@ -3412,7 +3412,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "管理身份验证设置并配置单点登录。不妨在我们的<2>文档中了解更多信息。" }, "bar-chart": { "warn": { @@ -4824,7 +4824,7 @@ "add-visualization-body": "选择一个数据源,然后用图表、统计信息和表格查询您的数据以及将其可视化,或创建列表、Markdown 和其他小部件。", "add-visualization-button": "添加可视化", "add-visualization-header": "通过添加可视化开始您的新仪表板", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "从文件或 <2>grafana.com 导入数据面板。", "import-a-dashboard-header": "导入仪表板", "import-dashboard-button": "导入仪表板", "show-less-dashboards": "", @@ -6599,7 +6599,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "点击<2>此处了解有关此错误的更多信息。", - "success-more-details-links": "" + "success-more-details-links": "接下来,您可以通过<2>构建数据面板或在<5>Explore 视图中查询数据来开始可视化数据。" }, "data-sources": { "datasource-add-button": { @@ -6695,7 +6695,7 @@ "test": "测试" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "您也可以省去这些步骤,通过<6>永久免费的 Grafana Cloud 计划,从 Grafana Labs 获取完全托管、可扩展和托管式数据源 {{mainDS}}(和 {{extraDS}})。", "title-alert": "在下方配置您的 {{mainDS}} 数据源" }, "dashboards-table": { @@ -7678,7 +7678,7 @@ "math": { "available-math-functions": "可用的数学函数", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "请参阅我们关于<2>数学表达式的其他文档。", "tooltip-title": "数学运算符", "tooltip-trigger": "表达式" }, @@ -7704,11 +7704,24 @@ "label-upsample": "升高采样", "tooltip-s-m-h": "10 秒、1 分钟、30 分钟、1 小时" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "运行查询", "modal-title": "SQL 编辑器", "tooltip-experimental": "SQL 表达式 LLM 集成是实验性的。若有任何问题,请向 Grafana 团队报告。" }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "输入" } @@ -10881,10 +10894,10 @@ "placeholder-optional": "(可选)", "role": "角色", "submit": "提交", - "tooltip": "" + "tooltip": "您现在可以选择“无基本角色”选项,并根据自定义需求添加权限。您可以在<1>我们的文档中找到更多信息。" }, "user-invite-page": { - "sub-title": "", + "sub-title": "发送邀请或将现有 Grafana 用户添加到组织。<1>{{orgName}}", "text": { "invite-user": "邀请用户" } @@ -12328,7 +12341,7 @@ }, "menu": { "clear-button": "全部清除", - "tooltip": "" + "tooltip": "您现在可以选择“无基本角色”选项,并根据自定义需求添加权限。您可以在<1>我们的文档中找到更多信息。" }, "menu-aria-label": "角色选择器菜单", "menu-group-option-aria-label": "角色选择器选项", @@ -12653,7 +12666,7 @@ "info-text": "创建指向此仪表板或面板的直接链接,通过以下选项自定义。", "link-url": "链接网址", "render-alert": "未安装图像渲染器插件", - "render-instructions": "", + "render-instructions": "要渲染图像,您必须安装 <2>Grafana 图像渲染器插件。请与您的 Grafana 管理员联系以安装插件。", "rendered-image": "直接链接至渲染后的图像", "save-alert": "仪表板未保存", "save-dashboard": "要渲染面板图像,需要先保存仪表板。", @@ -13693,7 +13706,7 @@ "label-format": "格式", "label-set-timezone": "设置时区", "label-time-field": "时间字段", - "tooltip-format": "", + "tooltip-format": "指定为 <2>Moment.js 格式字符串的字段的输出格式。", "tooltip-timezone-manually": "手动设置日期的时区" }, "format-time-transformer-editor": { diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 061e2cb2f50..dec7715e1fd 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -2075,7 +2075,7 @@ "warning-1": "刪除此通知政策將永久移除該政策。", "warning-2": "您確定要刪除這個政策嗎?" }, - "filter-description": "", + "filter-description": "使用以逗號分隔的比對條件清單來篩選通知政策,例如:<1>severity=critical, region=EMEA", "generated-policies": "自動產生的政策", "matchers": "匹配條件", "metadata": { @@ -2796,7 +2796,7 @@ "template-data-docs": { "alert-template-data-table": { "alert-template-data": "警報範本資料", - "only-in-alert": "僅在警報內容中才可用(例如在警報迴圈中)" + "only-in-alert": "" }, "available-context-notification": "可用於通知內容。", "notification-template-data": "通知範本資料" @@ -3233,7 +3233,7 @@ "auth-settings": "驗證設定" }, "auth-drawer-unconneced": { - "subtitle": "" + "subtitle": "設定驗證設定。請在我們的<2>文件中閱讀更多資訊。" }, "auth-drawer-unconnected": { "advanced-auth": "進階驗證", @@ -3267,7 +3267,7 @@ "allowed-organizations-description": "以逗號或空格分隔的組織清單。使用者應至少是\n一個組織的成員才能登入。", "allowed-organizations-label": "允許的組織", "allowed-organizations-placeholder": "輸入組織 (my-team、myteam...),然後按 Enter 新增", - "api-url-description": "", + "api-url-description": "OAuth2 提供者的使用者資訊端點。此端點傳回的資訊必須與 <2>OpenID UserInfo 相容。", "api-url-required": "如果設定,則此欄位必須為有效的網址。", "auth-style-description": "此設定決定了如何將「{{ clientIDLabel }}」和「{{ clientSecretLabel }}」傳送給 Oauth2 提供者。預設為 AutoDetect。", "auth-style-label": "驗證樣式", @@ -3412,7 +3412,7 @@ } }, "auth-config-auth-config-page-unconnected": { - "subtitle": "" + "subtitle": "管理您的驗證設定並設定單一登入。請在我們的<2>文件中閱讀更多資訊。" }, "bar-chart": { "warn": { @@ -4824,7 +4824,7 @@ "add-visualization-body": "選取資料來源,然後使用圖表、統計資料及表格查詢並將資料可視化,或建立清單、標記及其他小工具。", "add-visualization-button": "新增可視化", "add-visualization-header": "透過新增可視化來啟動您的新儀表板", - "import-a-dashboard-body": "", + "import-a-dashboard-body": "從檔案或 <2>grafana.com 匯入儀表板。", "import-a-dashboard-header": "匯入儀表板", "import-dashboard-button": "匯入儀表板", "show-less-dashboards": "", @@ -6599,7 +6599,7 @@ }, "data-source-testing-status-page": { "error-more-details-link": "點選<2>此處,了解更多關於此錯誤的資訊。", - "success-more-details-links": "" + "success-more-details-links": "接下來,您可以透過<2>建立儀表板或在<5>瀏覽檢視中查詢資料,開始將資料視覺化。" }, "data-sources": { "datasource-add-button": { @@ -6695,7 +6695,7 @@ "test": "測試" }, "cloud-info-box": { - "body-alert": "", + "body-alert": "或者,跳過這個步驟,並透過<6>永久免費的 Grafana Cloud 方案,從 Grafana Labs 取得完全受控、可擴展及託管的資料來源 {{mainDS}}(和 {{extraDS}})。", "title-alert": "在下方設定您的 {{mainDS}} 資料來源" }, "dashboards-table": { @@ -7678,7 +7678,7 @@ "math": { "available-math-functions": "可用的數學函數", "run-math-operations": "", - "tooltip-footer": "", + "tooltip-footer": "請參閱我們關於<2>數學表達式的其他文件。", "tooltip-title": "數學運算子", "tooltip-trigger": "表達式" }, @@ -7704,11 +7704,24 @@ "label-upsample": "向上取樣", "tooltip-s-m-h": "10 秒、1 分鐘、30 分鐘、1 小時" }, + "schema-inspector-panel": { + "aria-label-close-schema-inspector": "" + }, "sql-expr": { "button-run-query": "執行查詢", "modal-title": "SQL 編輯器", "tooltip-experimental": "SQL 運算式 LLM 整合為實驗性功能。如有任何問題,請向 Grafana 團隊回報。" }, + "sql-schema": { + "close-schema-inspector": "", + "error-title": "", + "inspect-button": "", + "loading": "", + "no-data-title": "", + "no-fields-desc": "", + "no-fields-title": "", + "query-error-title": "" + }, "threshold": { "label-input": "輸入" } @@ -10881,10 +10894,10 @@ "placeholder-optional": "(選填)", "role": "角色", "submit": "提交", - "tooltip": "" + "tooltip": "您現在可以選擇「無基本角色」選項,並根據您的自訂需求新增權限。您可以在<1>我們的文件中找到更多資訊。" }, "user-invite-page": { - "sub-title": "", + "sub-title": "傳送邀請或將現有 Grafana 使用者新增至組織。<1>{{orgName}}", "text": { "invite-user": "邀請使用者" } @@ -12328,7 +12341,7 @@ }, "menu": { "clear-button": "全部清除", - "tooltip": "" + "tooltip": "您現在可以選擇「無基本角色」選項,並根據您的自訂需求新增權限。您可以在<1>我們的文件中找到更多資訊。" }, "menu-aria-label": "角色選擇器選單", "menu-group-option-aria-label": "角色選擇器選項", @@ -12653,7 +12666,7 @@ "info-text": "建立指向此儀表板或面板的直接連結,並使用以下選項自訂。", "link-url": "連結網址", "render-alert": "未安裝圖片轉譯器外掛程式", - "render-instructions": "", + "render-instructions": "若要轉譯圖片,您必須安裝 <2>Grafana 圖片轉譯器外掛程式。請聯絡您的 Grafana 管理員以安裝外掛程式。", "rendered-image": "直接連結轉譯的圖片", "save-alert": "儀表板未儲存", "save-dashboard": "若要呈現面板圖片,您必須先儲存儀表板。", @@ -13693,7 +13706,7 @@ "label-format": "格式", "label-set-timezone": "設定時區", "label-time-field": "時間欄位", - "tooltip-format": "", + "tooltip-format": "指定為 <2>Moment.js 格式字串的欄位輸出格式。", "tooltip-timezone-manually": "手動設定日期的時區" }, "format-time-transformer-editor": { From ede756f5a86601aaa5942005e038fa89052276a4 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:59:25 -0600 Subject: [PATCH 090/378] Suggestions: Introduce V2 right sidebar (#114274) * newVizSuggestions feature toggle * panel empty state * add data check * select first suggestion * update text * PanelEmptyState component * add test * ? * remove fake translation * Visualizations -> All visualizations * updates * move empty state to UnconfiguredPanel * default tab to Suggestions; new tabs and content * select first suggestion; apply button; removed search functionaility from suggestions * extract hasData * refactor * cleanup * fix default selection * translation * hasData test * update e2e * fix width of suggestions tabs * make sure we only show a max of two columns * useMeasure with a static gridTemplateColumns def * Suggestions: hashes on suggestions, update logic to select first suggestion * reorganize comments a little bit * fix types * add tests for hashes --------- Co-authored-by: Paul Marbach --- .../visualization-suggestions.spec.ts | 12 +- eslint-suppressions.json | 5 - .../src/types/suggestions.test.ts | 75 +++++++ .../grafana-data/src/types/suggestions.ts | 54 ++++- .../panel-edit/PanelEditor.tsx | 5 +- .../panel-edit/PanelOptionsPane.tsx | 5 +- .../panel-edit/PanelVizTypePicker.tsx | 169 ++++++++-------- .../PanelEditor/VisualizationSelectPane.tsx | 2 +- .../VisualizationSuggestionCard.tsx | 31 +-- .../VisualizationSuggestions.tsx | 186 ++++++++++-------- .../panel/suggestions/getAllSuggestions.ts | 1 + .../features/panel/suggestions/utils.test.ts | 40 +++- .../app/features/panel/suggestions/utils.ts | 12 +- .../plugins/panel/timeseries/suggestions.ts | 1 + public/locales/en-US/grafana.json | 8 +- 15 files changed, 404 insertions(+), 202 deletions(-) create mode 100644 packages/grafana-data/src/types/suggestions.test.ts diff --git a/e2e-playwright/various-suite/visualization-suggestions.spec.ts b/e2e-playwright/various-suite/visualization-suggestions.spec.ts index 57ccfccc475..d01447b7eb7 100644 --- a/e2e-playwright/various-suite/visualization-suggestions.spec.ts +++ b/e2e-playwright/various-suite/visualization-suggestions.spec.ts @@ -17,10 +17,7 @@ test.describe( // Try visualization suggestions await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); - await panelEditPage - .getByGrafanaSelector(selectors.components.RadioButton.container) - .filter({ hasText: 'Suggestions' }) - .click(); + await panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('Suggestions')).click(); // Verify we see suggestions const lineChartCard = panelEditPage.getByGrafanaSelector( @@ -28,12 +25,7 @@ test.describe( ); await expect(lineChartCard).toBeVisible(); - // Verify search works - const searchInput = page.getByPlaceholder('Search for...'); - await searchInput.fill('Table'); - - // Should no longer see line chart - await expect(lineChartCard).toBeHidden(); + // TODO: in this part of the test, we will change the query and the transforms and observe suggestions being updated. // Select a visualization await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click(); diff --git a/eslint-suppressions.json b/eslint-suppressions.json index b3eaa1feb52..e9af258606b 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1955,11 +1955,6 @@ "count": 1 } }, - "public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "public/app/features/dashboard-scene/saving/SaveDashboardForm.tsx": { "no-restricted-syntax": { "count": 1 diff --git a/packages/grafana-data/src/types/suggestions.test.ts b/packages/grafana-data/src/types/suggestions.test.ts new file mode 100644 index 00000000000..502590952d9 --- /dev/null +++ b/packages/grafana-data/src/types/suggestions.test.ts @@ -0,0 +1,75 @@ +import { getSuggestionHash, PanelPluginVisualizationSuggestion } from './suggestions'; + +interface FakeOptions { + foo?: number; + bar?: string; +} + +interface FakeFieldOptions { + foo?: { + a: number; + b: number; + }; + bar?: string; +} + +describe('getSuggestionHash', () => { + it.each<{ + name: string; + a: Omit, 'hash'>; + b: Omit, 'hash'>; + expectEqual: boolean; + }>([ + { + name: 'should create different hashes for different pluginIds', + a: { pluginId: 'a', name: 'my suggestion' }, + b: { pluginId: 'b', name: 'my suggestion' }, + expectEqual: false, + }, + { + name: 'should create different hashes for different options', + a: { pluginId: 'a', name: 'my suggestion', options: { foo: 1 } }, + b: { pluginId: 'a', name: 'my suggestion', options: { foo: 2 } }, + expectEqual: false, + }, + { + name: 'should create different hashes for different fieldConfig', + a: { + pluginId: 'a', + name: 'my suggestion', + fieldConfig: { defaults: { custom: { bar: 'x', foo: { a: 1, b: 2 } } }, overrides: [] }, + }, + b: { + pluginId: 'a', + name: 'my suggestion', + fieldConfig: { defaults: { custom: { bar: 'y', foo: { a: 1, b: 2 } } }, overrides: [] }, + }, + expectEqual: false, + }, + { + name: 'should create same hashes for same suggestions', + a: { + pluginId: 'a', + name: 'my suggestion', + options: { foo: 1, bar: 'x' }, + fieldConfig: { defaults: { custom: { bar: 'x', foo: { a: 1, b: 2 } } }, overrides: [] }, + }, + b: { + pluginId: 'a', + name: 'my suggestion', + options: { bar: 'x', foo: 1 }, + fieldConfig: { defaults: { custom: { foo: { b: 2, a: 1 }, bar: 'x' } }, overrides: [] }, + }, + expectEqual: true, + }, + ])('$name', ({ a, b, expectEqual }) => { + const hashA = getSuggestionHash(a); + const hashB = getSuggestionHash(b); + + if (expectEqual) { + expect(hashA).toEqual(hashB); + } else { + expect(hashA).not.toEqual(hashB); + } + }); +}); diff --git a/packages/grafana-data/src/types/suggestions.ts b/packages/grafana-data/src/types/suggestions.ts index 1765e5406f2..475495cc24a 100644 --- a/packages/grafana-data/src/types/suggestions.ts +++ b/packages/grafana-data/src/types/suggestions.ts @@ -8,6 +8,47 @@ import { PanelModel } from './dashboard'; import { FieldConfigSource } from './fieldOverrides'; import { PanelData } from './panel'; +/** + * @internal + * generates a hash for a suggestion based for use by the UI. + */ +export function getSuggestionHash(suggestion: Omit): string { + return deterministicObjectHash(suggestion); +} + +function strHash(str: string): string { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash |= 0; // Convert to 32bit integer + } + return hash.toString(36); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function deterministicObjectHash>(obj: T): string { + let result = ''; + for (const key of Object.keys(obj).sort()) { + const value = obj[key]; + if (value === undefined) { + continue; + } + result += key + ':'; + if (typeof value === 'object' && value !== null) { + result += deterministicObjectHash(value); + } else if (Array.isArray(value)) { + result += value + .map((v) => (typeof value === 'object' && value !== null ? deterministicObjectHash(v) : String(v))) + .join(','); + } else { + result += String(value); + } + result += ';'; + } + return strHash(result); +} + /** * @alpha * A suggestion for a visualization given some data. This represents the shape of the panel (including options and field config) @@ -49,6 +90,8 @@ export interface PanelPluginVisualizationSuggestion( - defaults: PanelPluginVisualizationSuggestion + defaults: Omit, 'hash'> ) { return new VisualizationSuggestionsListAppender(this.list, defaults); } @@ -127,10 +170,15 @@ export class VisualizationSuggestionsListAppender) { - this.list.push(defaultsDeep(suggestion, this.defaults)); + this.appendAll([suggestion]); } appendAll(suggestions: Array>) { - this.list.push(...suggestions.map((o) => defaultsDeep(o, this.defaults))); + this.list.push( + ...suggestions.map((s): PanelPluginVisualizationSuggestion => { + const suggestionWithDefaults = defaultsDeep(s, this.defaults); + return Object.assign(suggestionWithDefaults, { hash: getSuggestionHash(suggestionWithDefaults) }); + }) + ); } } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 1ff7eef89ca..903e871366d 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -23,6 +23,7 @@ import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/Opti import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard'; import { saveLibPanel } from 'app/features/library-panels/state/api'; import { getAllSuggestions } from 'app/features/panel/suggestions/getAllSuggestions'; +import { hasData } from 'app/features/panel/suggestions/utils'; import { DashboardEditActionEvent } from '../edit-pane/shared'; import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker'; @@ -130,9 +131,7 @@ export class PanelEditor extends SceneObjectBase { this._subs.add( dataObject.subscribeToState(async () => { const { data } = dataObject.state; - const hasData = data && data.series && data.series.length > 0 && data.series.some((frame) => frame.length > 0); - - if (hasData && panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) { + if (hasData(data) && panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) { const panelModel = new PanelModelCompatibilityWrapper(panel); const suggestions = await getAllSuggestions(data, panelModel); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx index f71d1238971..4c5b7729745 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx @@ -99,7 +99,10 @@ export class PanelOptionsPane extends SceneObjectBase { panel.onFieldConfigChange(fieldConfigWithOverrides, true); } - this.onToggleVizPicker(); + // Handle preview suggestions + if (!options.withModKey) { + this.onToggleVizPicker(); + } }; onSetSearchQuery = (searchQuery: string) => { diff --git a/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx b/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx index a57aac9d427..d5b236fdd58 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx @@ -1,14 +1,13 @@ import { css } from '@emotion/css'; import { debounce } from 'lodash'; -import { useEffect, useMemo, useState } from 'react'; -import { useLocalStorage } from 'react-use'; +import { useCallback, useMemo, useState } from 'react'; +import { useSessionStorage } from 'react-use'; -import { GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; +import { GrafanaTheme2, PanelData } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { VizPanel } from '@grafana/scenes'; -import { Button, Field, FilterInput, RadioButtonGroup, ScrollContainer, useStyles2 } from '@grafana/ui'; +import { FilterInput, ScrollContainer, Tab, TabContent, TabsBar, useStyles2 } from '@grafana/ui'; import { LS_VISUALIZATION_SELECT_TAB_KEY } from 'app/core/constants'; import { VisualizationSelectPaneTab } from 'app/features/dashboard/components/PanelEditor/types'; import { VisualizationSuggestions } from 'app/features/panel/components/VizTypePicker/VisualizationSuggestions'; @@ -26,8 +25,25 @@ export interface Props { onClose: () => void; } +const getTabs = (): Array<{ label: string; value: VisualizationSelectPaneTab }> => { + const suggestionsTab = { + label: t('dashboard-scene.panel-viz-type-picker.radio-options.label.suggestions', 'Suggestions'), + value: VisualizationSelectPaneTab.Suggestions, + }; + const allVisualizationsTab = { + label: t('dashboard-scene.panel-viz-type-picker.radio-options.label.all-visualizations', 'All visualizations'), + value: VisualizationSelectPaneTab.Visualizations, + }; + return config.featureToggles.newVizSuggestions + ? [suggestionsTab, allVisualizationsTab] + : [allVisualizationsTab, suggestionsTab]; +}; + export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) { const styles = useStyles2(getStyles); + const panelModel = useMemo(() => new PanelModelCompatibilityWrapper(panel), [panel]); + + /** SEARCH */ const [searchQuery, setSearchQuery] = useState(''); const trackSearch = useMemo( () => @@ -44,87 +60,74 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) { }, 300), [] ); - const handleSearchChange = (value: string) => { setSearchQuery(value); }; - const tabKey = LS_VISUALIZATION_SELECT_TAB_KEY; - const defaultTab = VisualizationSelectPaneTab.Visualizations; - const panelModel = useMemo(() => new PanelModelCompatibilityWrapper(panel), [panel]); + /** TABS */ + const tabs = useMemo(getTabs, []); + const defaultTab = tabs[0].value; + const [listMode, setListMode] = useSessionStorage(LS_VISUALIZATION_SELECT_TAB_KEY, defaultTab); - const supportedListModes = useMemo( - () => new Set([VisualizationSelectPaneTab.Visualizations, VisualizationSelectPaneTab.Suggestions]), - [] + const handleListModeChange = useCallback( + (value: VisualizationSelectPaneTab) => { + reportInteraction(INTERACTION_EVENT_NAME, { + item: INTERACTION_ITEM.CHANGE_TAB, + tab: VisualizationSelectPaneTab[value], + creator_team: 'grafana_plugins_catalog', + schema_version: '1.0.0', + }); + setListMode(value); + }, + [setListMode] ); - const [listMode, setListMode] = useLocalStorage(tabKey, defaultTab); - const handleListModeChange = (value: VisualizationSelectPaneTab) => { - reportInteraction(INTERACTION_EVENT_NAME, { - item: INTERACTION_ITEM.CHANGE_TAB, - tab: VisualizationSelectPaneTab[value], - creator_team: 'grafana_plugins_catalog', - schema_version: '1.0.0', - }); - setListMode(value); - }; - - useEffect(() => { - if (listMode && !supportedListModes.has(listMode)) { - setListMode(defaultTab); - } - }, [defaultTab, listMode, setListMode, supportedListModes]); - - const radioOptions: Array> = [ - { - label: t('dashboard-scene.panel-viz-type-picker.radio-options.label.visualizations', 'Visualizations'), - value: VisualizationSelectPaneTab.Visualizations, - }, - { - label: t('dashboard-scene.panel-viz-type-picker.radio-options.label.suggestions', 'Suggestions'), - value: VisualizationSelectPaneTab.Suggestions, - }, - ]; return (
-
- -
- - - + {/*@TODO: Re-enable/move close button*/} + {/**/} + + {tabs.map((tab) => ( + handleListModeChange(tab.value)} + /> + ))} + - {listMode === VisualizationSelectPaneTab.Visualizations && ( - - )} - {listMode === VisualizationSelectPaneTab.Suggestions && ( - - )} + + {listMode === VisualizationSelectPaneTab.Suggestions && ( + + )} + {listMode === VisualizationSelectPaneTab.Visualizations && ( + <> +
+ +
+ + + )} +
); @@ -141,10 +144,18 @@ const getStyles = (theme: GrafanaTheme2) => ({ }), searchRow: css({ display: 'flex', - marginBottom: theme.spacing(1), + marginBottom: theme.spacing(2), + }), + tabs: css({ + width: '100%', + }), + tab: css({ + flexGrow: 1, + justifyContent: 'center', + textAlign: 'center', }), closeButton: css({ - marginLeft: theme.spacing(1), + marginLeft: 'auto', }), customFieldMargin: css({ marginBottom: theme.spacing(1), diff --git a/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx b/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx index ffa32a42254..779dfe3d360 100644 --- a/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx +++ b/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx @@ -108,7 +108,7 @@ export const VisualizationSelectPane = ({ panel, data }: Props) => { )} {listMode === VisualizationSelectPaneTab.Suggestions && ( - + )} {listMode === VisualizationSelectPaneTab.LibraryPanels && ( diff --git a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestionCard.tsx b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestionCard.tsx index 32eae1afc87..7f04c8a9dea 100644 --- a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestionCard.tsx +++ b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestionCard.tsx @@ -1,45 +1,42 @@ import { css, cx } from '@emotion/css'; import { cloneDeep } from 'lodash'; -import { CSSProperties } from 'react'; +import { CSSProperties, HTMLAttributes } from 'react'; import { GrafanaTheme2, PanelData, PanelPluginVisualizationSuggestion } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { config } from '@grafana/runtime'; import { Tooltip, useStyles2 } from '@grafana/ui'; import { PanelRenderer } from '../PanelRenderer'; -import { VizTypeChangeDetails } from './types'; - -export interface Props { +export interface Props extends HTMLAttributes { data: PanelData; width: number; suggestion: PanelPluginVisualizationSuggestion; - onChange: (details: VizTypeChangeDetails) => void; + isSelected?: boolean; } -export function VisualizationSuggestionCard({ data, suggestion, onChange, width }: Props) { +export function VisualizationSuggestionCard({ data, suggestion, width, isSelected = false, onClick }: Props) { const styles = useStyles2(getStyles); const { innerStyles, outerStyles, renderWidth, renderHeight } = getPreviewDimensionsAndStyles(width); const cardOptions = suggestion.cardOptions ?? {}; + const isNewVizSuggestionsEnabled = config.featureToggles.newVizSuggestions; const commonButtonProps = { 'aria-label': suggestion.name, - className: styles.vizBox, + className: cx(styles.vizBox, isNewVizSuggestionsEnabled && isSelected && styles.selectedBox), 'data-testid': selectors.components.VisualizationPreview.card(suggestion.name), style: outerStyles, - onClick: () => { - onChange({ - pluginId: suggestion.pluginId, - options: suggestion.options, - fieldConfig: suggestion.fieldConfig, - }); - }, + onClick, }; if (cardOptions.imgSrc) { return ( - @@ -100,6 +97,10 @@ const getStyles = (theme: GrafanaTheme2) => { background: theme.colors.background.secondary, }, }), + selectedBox: css({ + border: `2px solid ${theme.colors.primary.main}`, + boxShadow: `0 0 0 1px ${theme.colors.primary.main}`, + }), imgBox: css({ display: 'flex', flexDirection: 'column', diff --git a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx index 46232beff41..ad4583fc55f 100644 --- a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx +++ b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx @@ -1,118 +1,137 @@ import { css } from '@emotion/css'; -import { useMemo } from 'react'; -import { useAsync } from 'react-use'; +import { useState, useEffect, useCallback } from 'react'; +import { useAsync, useMeasure } from 'react-use'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2, PanelData, PanelModel, PanelPluginVisualizationSuggestion } from '@grafana/data'; -import { Trans } from '@grafana/i18n'; +import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; -import { Icon, Text, useStyles2 } from '@grafana/ui'; +import { Button, Icon, Text, useStyles2 } from '@grafana/ui'; +import { UNCONFIGURED_PANEL_PLUGIN_ID } from 'app/features/dashboard-scene/scene/UnconfiguredPanel'; import { getAllSuggestions } from '../../suggestions/getAllSuggestions'; +import { hasData } from '../../suggestions/utils'; import { VisualizationSuggestionCard } from './VisualizationSuggestionCard'; import { VizTypeChangeDetails } from './types'; export interface Props { - searchQuery: string; onChange: (options: VizTypeChangeDetails) => void; data?: PanelData; panel?: PanelModel; - trackSearch?: (q: string, count: number) => void; } -export function VisualizationSuggestions({ searchQuery, onChange, data, panel, trackSearch }: Props) { +const MIN_COLUMN_SIZE = 260; + +export function VisualizationSuggestions({ onChange, data, panel }: Props) { const styles = useStyles2(getStyles); const { value: suggestions } = useAsync(() => getAllSuggestions(data, panel), [data, panel]); - const filteredSuggestions = useMemo(() => { - const result = filterSuggestionsBySearch(searchQuery, suggestions); - if (trackSearch) { - trackSearch(searchQuery, result.length); + const [suggestionHash, setSuggestionHash] = useState(null); + const [firstCardRef, { width }] = useMeasure(); + const [firstCardHash, setFirstCardHash] = useState(null); + + const isNewVizSuggestionsEnabled = config.featureToggles.newVizSuggestions; + + const isUnconfiguredPanel = panel?.type === UNCONFIGURED_PANEL_PLUGIN_ID; + + const applySuggestion = useCallback( + (suggestion: PanelPluginVisualizationSuggestion, isPreview?: boolean) => { + onChange({ + pluginId: suggestion.pluginId, + options: suggestion.options, + fieldConfig: suggestion.fieldConfig, + withModKey: isPreview, + }); + + if (isPreview) { + setSuggestionHash(suggestion.hash); + } + }, + [onChange] + ); + + useEffect(() => { + if (!isNewVizSuggestionsEnabled || !suggestions || suggestions.length === 0) { + return; } - return result; - }, [searchQuery, suggestions, trackSearch]); - const hasData = data?.series && data.series.length > 0 && !data.series.every((frame) => frame.length === 0); + // if the first suggestion has changed, we're going to change the currently selected suggestion and + // set the firstCardHash to the new first suggestion's hash. We also choose the first suggestion if + // the previously selected suggestion is no longer present in the list. + const newFirstCardHash = suggestions?.[0]?.hash ?? null; + if (firstCardHash !== newFirstCardHash || suggestions.every((s) => s.hash !== suggestionHash)) { + applySuggestion(suggestions[0], true); + setFirstCardHash(newFirstCardHash); + return; + } + }, [suggestions, suggestionHash, firstCardHash, isNewVizSuggestionsEnabled, isUnconfiguredPanel, applySuggestion]); - if (config.featureToggles.newVizSuggestions && !hasData && !searchQuery) { - return ( -
- - - - Run a query to start seeing suggested visualizations - - -
- ); + const renderEmptyState = () => ( +
+ + + + Run a query to start seeing suggested visualizations + + +
+ ); + + if (isNewVizSuggestionsEnabled && (!data || !hasData(data))) { + return renderEmptyState(); + } + + if (!data) { + return null; } return ( // This div is needed in some places to make AutoSizer work
- {({ width }) => { - if (!width) { - return null; - } + {() => ( +
+
+ {suggestions?.map((suggestion, index) => { + const isCardSelected = isNewVizSuggestionsEnabled && suggestionHash === suggestion.hash; - width = width - 1; - const columnCount = Math.floor(width / 200); - const spaceBetween = 8 * (columnCount! - 1); - const previewWidth = Math.floor((width - spaceBetween) / columnCount!); - - return ( -
-
-
- Based on current data -
-
-
- {filteredSuggestions.map((suggestion, index) => ( - - ))} - {searchQuery && filteredSuggestions.length === 0 && ( -
- - No results matched your query - + return ( +
+ {isCardSelected && ( + + )} + applySuggestion(suggestion, isNewVizSuggestionsEnabled)} + />
- )} -
+ ); + })}
- ); - }} +
+ )}
); } -function filterSuggestionsBySearch( - searchQuery: string, - suggestions?: PanelPluginVisualizationSuggestion[] -): PanelPluginVisualizationSuggestion[] { - if (!searchQuery || !suggestions) { - return suggestions || []; - } - - const regex = new RegExp(searchQuery, 'i'); - - return suggestions.filter((s) => regex.test(s.name) || regex.test(s.pluginId)); -} - const getStyles = (theme: GrafanaTheme2) => { return { - heading: css({ - ...theme.typography.h5, - margin: theme.spacing(0, 0.5, 1), - }), filterRow: css({ display: 'flex', flexDirection: 'row', @@ -128,7 +147,7 @@ const getStyles = (theme: GrafanaTheme2) => { grid: css({ display: 'grid', gridGap: theme.spacing(1), - gridTemplateColumns: 'repeat(auto-fill, 144px)', + gridTemplateColumns: `repeat(auto-fit, minmax(${MIN_COLUMN_SIZE}px, 1fr))`, marginBottom: theme.spacing(1), justifyContent: 'space-evenly', }), @@ -145,5 +164,16 @@ const getStyles = (theme: GrafanaTheme2) => { color: theme.colors.text.secondary, marginBottom: theme.spacing(2), }), + cardContainer: css({ + position: 'relative', + }), + applySuggestionButton: css({ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + zIndex: 10, + padding: '0 16px', + }), }; }; diff --git a/public/app/features/panel/suggestions/getAllSuggestions.ts b/public/app/features/panel/suggestions/getAllSuggestions.ts index 86fb0e17bde..ff6fbef50e8 100644 --- a/public/app/features/panel/suggestions/getAllSuggestions.ts +++ b/public/app/features/panel/suggestions/getAllSuggestions.ts @@ -52,6 +52,7 @@ export async function getAllSuggestions( name: plugin.name, pluginId: plugin.id, description: plugin.info.description, + hash: 'plugin-empty-' + plugin.id, cardOptions: { imgSrc: plugin.info.logos.small, }, diff --git a/public/app/features/panel/suggestions/utils.test.ts b/public/app/features/panel/suggestions/utils.test.ts index 905b59930a5..04d263415db 100644 --- a/public/app/features/panel/suggestions/utils.test.ts +++ b/public/app/features/panel/suggestions/utils.test.ts @@ -1,6 +1,14 @@ -import { createDataFrame, FieldType, getPanelDataSummary, PanelDataSummary } from '@grafana/data'; +import { + createDataFrame, + FieldType, + getPanelDataSummary, + PanelDataSummary, + PanelData, + LoadingState, + getDefaultTimeRange, +} from '@grafana/data'; -import { showDefaultSuggestion } from './utils'; +import { showDefaultSuggestion, hasData } from './utils'; describe('Suggestions utils', () => { describe('showDefaultSuggestion', () => { @@ -30,4 +38,32 @@ describe('Suggestions utils', () => { expect(result).toBeUndefined(); }); }); + + describe('hasData', () => { + it('should return false when data is undefined', () => { + expect(hasData(undefined)).toBe(false); + }); + + it('should return false when data has no series', () => { + const data: PanelData = { + series: [], + state: LoadingState.Done, + timeRange: getDefaultTimeRange(), + }; + expect(hasData(data)).toBe(false); + }); + + it('should return true when at least one series has data', () => { + const data: PanelData = { + state: LoadingState.Done, + series: [ + createDataFrame({ + fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3] }], + }), + ], + timeRange: getDefaultTimeRange(), + }; + expect(hasData(data)).toBe(true); + }); + }); }); diff --git a/public/app/features/panel/suggestions/utils.ts b/public/app/features/panel/suggestions/utils.ts index 8668ec2f654..ca5cc74bd77 100644 --- a/public/app/features/panel/suggestions/utils.ts +++ b/public/app/features/panel/suggestions/utils.ts @@ -1,4 +1,4 @@ -import { PanelDataSummary } from '@grafana/data'; +import { PanelData, PanelDataSummary } from '@grafana/data'; /** * @internal @@ -9,3 +9,13 @@ import { PanelDataSummary } from '@grafana/data'; export function showDefaultSuggestion(fn: (panelDataSummary: PanelDataSummary) => boolean | void) { return (panelDataSummary: PanelDataSummary) => (fn(panelDataSummary) ? [{}] : undefined); } + +/** + * @internal + * Checks if the panel has data + * @param data - PanelData + * @returns true if data exists and has at least one non-empty series + */ +export function hasData(data?: PanelData): boolean { + return Boolean(data && data.series && data.series.length > 0 && data.series.some((frame) => frame.length > 0)); +} diff --git a/public/app/plugins/panel/timeseries/suggestions.ts b/public/app/plugins/panel/timeseries/suggestions.ts index 8ca7d6148ed..d41179d3f36 100644 --- a/public/app/plugins/panel/timeseries/suggestions.ts +++ b/public/app/plugins/panel/timeseries/suggestions.ts @@ -231,6 +231,7 @@ export function getPrepareTimeseriesSuggestion(panelId: number): PanelPluginVisu return { name: 'Transform to wide time series format', + hash: 'timeseries-transform-prepare-wide', pluginId: 'timeseries', transformations, }; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 474fde1a97d..fd2b50edb93 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -6182,8 +6182,8 @@ "placeholder-search-for": "Search for...", "radio-options": { "label": { - "suggestions": "Suggestions", - "visualizations": "Visualizations" + "all-visualizations": "All visualizations", + "suggestions": "Suggestions" } }, "title-close": "Close" @@ -11050,8 +11050,8 @@ "tooltip-delete": "Delete" }, "visualization-suggestions": { - "based-on-current-data": "Based on current data", - "no-results-matched-your-query": "No results matched your query" + "apply-suggestion-aria-label": "Apply {{suggestionName}} visualization", + "use-this-suggestion": "Use this suggestion" }, "viz-type-picker": { "could-anything-matching-query": "Could not find anything matching your query" From 84f44c45c10b6d3af1d115dbea36d8d43d198654 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Tue, 25 Nov 2025 09:13:39 +0100 Subject: [PATCH 091/378] Docs: Document auditing logs loki retries and timeout options (#114364) --- .../configure-grafana/enterprise-configuration/index.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md b/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md index 7f1cf5e3cb5..94e7a1eec55 100644 --- a/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md +++ b/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md @@ -266,6 +266,14 @@ If true, it establishes a secure connection to Loki. Defaults to true. Set the tenant ID for Loki communication, which is disabled by default. The tenant ID is required to interact with Loki running in [multi-tenant mode](/docs/loki/latest/operations/multi-tenancy/). +### retries + +The amount of times the HTTP or gRPC client will retry a failed request to Loki. The default is `10`. + +### timeout + +The timeout duration of an HTTP request or gRPC call to Loki. The default is `3s`. + ## [auth.saml] ### enabled From 89576baf2e579c63af993fed4b6a463b6f904a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 25 Nov 2025 09:16:31 +0100 Subject: [PATCH 092/378] Revert "TimeComparison: Ensure headerActions are activated" (#114370) Revert "TimeComparison: Ensure headerActions are activated (#111224)" This reverts commit 5556bcbc5283c738adae47b032841df8fdbf6d71. --- .../dashboard-scene/panel-edit/PanelEditor.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 903e871366d..75e82ef6093 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -16,7 +16,6 @@ import { sceneGraph, sceneUtils, VizPanel, - isSceneObject, } from '@grafana/scenes'; import { Panel } from '@grafana/schema'; import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions'; @@ -104,16 +103,6 @@ export class PanelEditor extends SceneObjectBase { const deactivateParents = activateSceneObjectAndParentTree(panel); - // Ensure headerActions are activated - const headerActions = panel.state.headerActions; - if (headerActions) { - (Array.isArray(headerActions) ? headerActions : [headerActions]).forEach((action) => { - if (isSceneObject(action)) { - action.activate(); - } - }); - } - this.waitForPlugin(); return () => { From 7edaf76de49737f2a889e5f434e799d5bc8b086d Mon Sep 17 00:00:00 2001 From: Anna Urbiztondo Date: Tue, 25 Nov 2025 09:28:20 +0100 Subject: [PATCH 093/378] Docs: asCode - export resources (#114244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP * Edits * Fixes * New title * Update docs/sources/as-code/observability-as-code/provision-resources/export-resources.md Co-authored-by: Roberto Jiménez Sánchez * Feedback * Prettier * Link * Added cli option * Prettier * Update docs/sources/as-code/observability-as-code/provision-resources/export-resources.md Co-authored-by: Roberto Jiménez Sánchez * Update docs/sources/as-code/observability-as-code/provision-resources/export-resources.md Co-authored-by: Roberto Jiménez Sánchez * Update docs/sources/as-code/observability-as-code/provision-resources/export-resources.md Co-authored-by: Roberto Jiménez Sánchez * Update docs/sources/as-code/observability-as-code/provision-resources/export-resources.md Co-authored-by: Roberto Jiménez Sánchez * Feedback * Fixes * Prettier * Fix? * Prettier --------- Co-authored-by: Roberto Jiménez Sánchez --- .../provision-resources/export-resources.md | 101 ++++++++++++++++++ .../provision-resources/git-sync-setup.md | 3 +- .../provisioned-dashboards.md | 78 ++++++++------ .../provision-resources/use-git-sync.md | 3 +- 4 files changed, 149 insertions(+), 36 deletions(-) create mode 100644 docs/sources/as-code/observability-as-code/provision-resources/export-resources.md diff --git a/docs/sources/as-code/observability-as-code/provision-resources/export-resources.md b/docs/sources/as-code/observability-as-code/provision-resources/export-resources.md new file mode 100644 index 00000000000..96bf299df08 --- /dev/null +++ b/docs/sources/as-code/observability-as-code/provision-resources/export-resources.md @@ -0,0 +1,101 @@ +--- +description: Export non-provisioned resources from Grafana. +keywords: + - dashboards + - resources + - git sync + - github + - export +labels: + products: + - enterprise + - oss + - cloud +title: Add non-provisioned resources from Grafana +menuTitle: Add non-provisioned resources +weight: 300 +canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/provision-resources/export-resources/ +--- + +# Export non-provisioned resources from Grafana + +{{< admonition type="caution" >}} + +Git Sync is available in [private preview](https://grafana.com/docs/release-life-cycle/) for Grafana Cloud. Support and documentation is available but might be limited to enablement, configuration, and some troubleshooting. No SLAs are provided. You can sign up to the private preview using the [Git Sync early access form](https://forms.gle/WKkR3EVMcbqsNnkD9). + +Git Sync and local file provisioning are [experimental features](https://grafana.com/docs/release-life-cycle/) introduced in Grafana v12 for open source and Enterprise editions. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. + +{{< /admonition >}} + +Git Sync is under development, and traditional operations like `import`, `copy`, `move`, or `save` to a provisioned folder are not yet supported for dashboards already existing in Grafana. To use Git Sync with existing dashboards you must export them to the repository first. + +Currently, Git Sync doesn't offer any built-in functionality to easily export resources from Grafana in bulk. However, the following options are available: + +- [Export the dashboard with Grafana CLI](#add-a-dashboard-with-grafana-cli) (recommended) +- [Copy the dashboard as JSON and commit to the repository](#add-a-dashboard-via-json-export) + +{{< admonition type="caution" >}} + +Refer to [Known limitations](https://grafana.com/docs/grafana//observability-as-code/provision-resources/intro-git-sync#known-limitations/) before using Git Sync. + +{{< /admonition >}} + +## Add a dashboard with Grafana CLI + +You can export an existing dashboard with the [Grafana CLI](https://grafana.com/docs/grafana//as-code/observability-as-code/grafana-cli/) and commit it to your Git repository. + +Use `grafanactl` to download the resources you want to sync from Grafana, and then commit and push those files to the repository. Git Sync will then detect the commit, and synchronize with Grafana. To do so, follow these steps: + +1. Set up the `grafanactl` context to point to your instance as documented in [Defining contexts](https://grafana.github.io/grafanactl/configuration/#defining-contexts). +1. Pull the resources you want to sync from the instance to your local repository: + +``` +grafanactl resources pull dashboards --path +``` + +Next, commit and push the resources to your Git repository: + +``` +git add +git commit -m "Add dashboards from Grafana" +git push +``` + +Where: + +- __: The path to the repository synced with Git Sync +- __: The path where the dashboards you want to export are located. The dashboards path must be under the repository + +See more at [Manage resources with Grafana CLI](https://grafana.github.io/grafanactl/guides/manage-resources/). + +## Add a dashboard via JSON export + +To add an existing dashboard to Git Sync via JSON export, you need to: + +1. Export the dashboard as JSON +1. Convert it to the Custom Resource Definition (CRD) format required by the Grafana App Platform +1. Commit the converted file to your Git repository + +### Required JSON format + +To export a dashboard as a JSON file it must follow this CRD structure: + +```yaml +{ + 'apiVersion': 'dashboard.grafana.app/v1beta1', + 'kind': 'Dashboard', + 'metadata': { 'name': 'dcf2lve9akj8xsd' }, + 'spec': { /* Original dashboard JSON goes here */ }, +} +``` + +The structure includes: + +- `apiVersion`: Specifies the API version (currently `v1beta1`) +- `kind`: Identifies the resource type (Dashboard) +- `metadata`: Contains the dashboard identifier `uid`. You can find the identifier in the dahsboard's URL or in the exported JSON +- `spec`: Wraps your original dashboard JSON + +## Edit Git-managed dashboards + +After you've saved a dashboard in Git, it'll be synchronized automatically, and you'll be able to work with it as any other provisioned resource. Refer to [Work with provisioned dashboards](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/provisioned-dashboards/) for more information. diff --git a/docs/sources/as-code/observability-as-code/provision-resources/git-sync-setup.md b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-setup.md index 0a7bd7be28b..f6113fe428d 100644 --- a/docs/sources/as-code/observability-as-code/provision-resources/git-sync-setup.md +++ b/docs/sources/as-code/observability-as-code/provision-resources/git-sync-setup.md @@ -9,8 +9,9 @@ labels: products: - enterprise - oss + - cloud title: Set up Git Sync -weight: 100 +weight: 110 canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/provision-resources/git-sync-setup/ aliases: - ../../../observability-as-code/provision-resources/git-sync-setup/ # /docs/grafana/next/observability-as-code/provision-resources/git-sync-setup/ diff --git a/docs/sources/as-code/observability-as-code/provision-resources/provisioned-dashboards.md b/docs/sources/as-code/observability-as-code/provision-resources/provisioned-dashboards.md index cc4964ff9e1..83ec3869927 100644 --- a/docs/sources/as-code/observability-as-code/provision-resources/provisioned-dashboards.md +++ b/docs/sources/as-code/observability-as-code/provision-resources/provisioned-dashboards.md @@ -10,7 +10,7 @@ labels: - enterprise - oss title: Work with provisioned dashboards -weight: 300 +weight: 350 canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/provision-resources/provisioned-dashboards/ aliases: - ../../../observability-as-code/provision-resources/provisioned-dashboards/ # /docs/grafana/next/observability-as-code/provision-resources/provisioned-dashboards/ @@ -26,33 +26,30 @@ Git Sync and local file provisioning are [experimental features](https://grafana {{< /admonition >}} -Using Provisioning, you can choose to store your dashboard JSON files in either GitHub repositories using Git Sync or a local file path. +Using provisioning, you can choose to store your dashboard JSON files in either GitHub repositories using Git Sync or a local path, and manage them through the Grafana interface. Dashboards and folders synchronized using Git Sync or a local path are referred to as _provisioned_ resources. For more information, refer to the [Dashboards](https://grafana.com/docs/grafana//dashboards/) documentation. ## Provisioning methods -Dashboards and folders synchronized using Git Sync or a local file path are referred to as "provisioned" resources. - ### Git Sync provisioning -Of the two experimental options, **Git Sync** is the recommended method for provisioning your dashboards. +Of the two options, **Git Sync** is the recommended method for provisioning your dashboards. You can synchronize any new dashboards and changes to existing dashboards to your configured GitHub repository. If you push a change in the repository, those changes are mirrored in your Grafana instance. -For more information on configuring Git Sync, refer to [Introduction to Git Sync](https://grafana.com/docs/grafana//observability-as-code/provision-resources/intro-git-sync/). +For more information on configuring Git Sync, refer to [Introduction to Git Sync](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/intro-git-sync/). ### Local path provisioning Local path provisioning makes files from a specified path available within Grafana, and any changes made in the configured local path are updated in Grafana. Note that these provisioned resources can only be modified in the local files and not within Grafana. -Refer to [Set up file provisioning](https://grafana.com/docs/grafana//observability-as-code/provision-resources/file-path-setup) to learn more about the version of local file provisioning in Grafana 12. +Refer to [Set up file provisioning](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/file-path-setup) to learn more about the version of local file provisioning in Grafana 12. {{< admonition type="note" >}} -The experimental local path provisioning using **Administration** > **Provisioning** will replace the file provisioning methods Grafana uses for referencing local file. +The **Administration** > **Provisioning** local path provisioning will replace the file provisioning methods Grafana uses for referencing local files. -For production systems, use the established methods for provisioning file systems in Grafana. -Refer to [Provision Grafana](https://grafana.com/docs/grafana//administration/provisioning/#provision-folders-structure-from-filesystem-to-grafana) for more information. +For production systems, use the established methods for provisioning file systems in Grafana. Refer to [Provision Grafana](https://grafana.com/docs/grafana//administration/provisioning/#provision-folders-structure-from-filesystem-to-grafana) for more information. {{< /admonition >}} ## Manage dashboards provisioned with Git Sync @@ -61,9 +58,9 @@ Using Git Sync, you can manage your dashboards in the UI and synchronize them wi Git Sync changes the behavior in Grafana for dashboards that are saved in Git Sync: -- Dashboards saved in your repository or local folder configured with Git Sync appear in a provisioned folder in Grafana. -- Any dashboard folders saved with Git Sync have a **Provisioned** label in the UI. -- Any changes to a provisioned resources have to be saved to the repository by opening a pull request or committing directly to the `main` branch. +- Dashboards saved in your repository or local folder configured with Git Sync appear in a provisioned folder in Grafana +- Any dashboard folders saved with Git Sync have a **Provisioned** label in the UI +- Any changes to a provisioned resources have to be saved to the repository by opening a pull request or committing directly to the `main` branch You can set a single folder, or multiple folders to a different repository, with up to 10 connections. @@ -71,18 +68,29 @@ You can set a single folder, or multiple folders to a different repository, with By default, Git version control uses a branch-based workflow for changes. This means that you can: -- Commit changes to an existing branch (such as `main`) or save them to a new branch in your GitHub repository. -- Use pull requests to review changes to dashboards. -- Preview the changes before merging. +- Commit changes to an existing branch (such as `main`) or save them to a new branch in your GitHub repository +- Use pull requests to review changes to dashboards +- Preview the changes before merging To learn more about Git, refer to [Getting Started - About Version Control](https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control) of the [Pro Git book](https://git-scm.com/book/en/v2) in the official Git documentation. ### Add and save a new dashboard +{{< admonition type="note" >}} + +If you want to add an existing dashboard to your provisioned resources, refer to [Export non-provisioned resources from Grafana](https://grafana.com/docs/grafana//as-code/observability-as-code/provision-resources/export-resources/). + +{{< /admonition >}} + +You have two options for creating **new** dashboards or folders in Git Sync: + +- Create them directly within Git Sync-managed folders in the Grafana UI +- Add them by committing JSON files to your Git repository + When you create a new dashboard in a provisioned folder associated with a GitHub repository, you follow the same process you use for any new dashboard. Refer to [Create a dashboard](http://grafana.com/docs/grafana//dashboards/build-dashboards/create-dashboard/) for more information. -After you create the dashboard, the steps are similar to [Save dashboard changes to GitHub](#save-dashboard-changes-to-github). +After you create the dashboard, follow these steps: 1. Select **Save** to preserve the new dashboard. 1. Enter a title for the dashboard and a description. @@ -94,26 +102,27 @@ After you create the dashboard, the steps are similar to [Save dashboard changes ### Save dashboard changes to GitHub -When you edit a provisioned resource, you are prompted to save or discard those changes. -Saving changes requires opening a pull request in your GitHub repository. +When you edit a provisioned resource, you're prompted to save or discard those changes. Saving changes requires opening a pull request in your GitHub repository. + +To save dashboard changes: 1. Select **Edit** to update a provisioned dashboard. Make your desired changes. 1. Click **Save dashboard**. 1. On the **Provisioned dashboard** panel, choose the options you want to use: - - **Update default refresh value**: Check this box to make the current refresh the new default. - - **Update default variable values**: Check this box to make the current values the new default. - - **Path**: Provide the path for your repository, ending in a JSON or YAML file. - - **Workflow:** Select **Push to main** to make a Git commit directly to the repository or **Push to a new branch** to create a pull request. - - **Branch**: Specify the branch name in GitHub (for example, main). This option only appears if you select **Push to a new branch**. - - **Comment**: Add a comment describing your changes. + - **Update default refresh value**: Check this box to make the current refresh the new default + - **Update default variable values**: Check this box to make the current values the new default + - **Path**: Provide the path for your repository, ending in a JSON or YAML file + - **Workflow:** Select **Push to main** to make a Git commit directly to the repository or **Push to a new branch** to create a pull request + - **Branch**: Specify the branch name in GitHub (for example, main). This option only appears if you select **Push to a new branch** + - **Comment**: Add a comment describing your changes 1. Optional: Select the **Changes** tab to view the differences between the updates you made and the original resource. 1. Select **Save**. -1. If you chose **Push to a new branch**, select **Open a pull request in GitHub** to open a new PR to your repository. GitHub opens with your dashboard’s code as the contents of the PR. +1. If you chose **Push to a new branch**, select **Open a pull request in GitHub** to open a new PR to your repository. GitHub opens with your dashboard's code as the contents of the PR. 1. Follow your usual GitHub workflow to save and merge the PR to your repository. @@ -121,18 +130,19 @@ Saving changes requires opening a pull request in your GitHub repository. You can remove a provisioned dashboard by deleting the dashboard from the repository. The Grafana UI updates when the changes from the GitHub repository sync. -To restore a deleted dashboard, raise a PR directly in your GitHub repository. Restoring resources from the UI is currently not possible. +To restore a deleted dashboard, raise a PR directly in your GitHub repository. Restoring resources from the UI isn't currently possible. -### Tips +### Best practices -- Use GitHub pull requests for changes to maintain review processes. -- Provide clear commit messages describing your changes. -- Regularly sync your repository to keep Grafana up to date. -- Review the **Events** tab to monitor sync status. +Follow these recommendations when working with provisioned dashboards: + +- **Use GitHub pull requests for changes**: Maintain review processes for dashboard modifications +- **Provide clear commit messages**: Describe your changes to help with tracking and collaboration +- **Regularly sync your repository**: Keep Grafana up to date with the latest changes +- **Review the Events tab**: Monitor sync status to ensure changes are applied correctly ## Manage dashboards provisioned with file provisioning -To update any resources in the local path, you need to edit the files directly and then save them locally. -These changes are synchronized to Grafana. However, you can't create, edit, or delete these resources using the Grafana UI. +To update any resources in the local path, you need to edit the files directly and then save them locally. These changes are synchronized to Grafana. However, you can't create, edit, or delete these resources using the Grafana UI. Refer to [Set up file provisioning](https://grafana.com/docs/grafana//observability-as-code/provision-resources/file-path-setup/) for configuration instructions. diff --git a/docs/sources/as-code/observability-as-code/provision-resources/use-git-sync.md b/docs/sources/as-code/observability-as-code/provision-resources/use-git-sync.md index 257e83c41ed..f7d37d4b94e 100644 --- a/docs/sources/as-code/observability-as-code/provision-resources/use-git-sync.md +++ b/docs/sources/as-code/observability-as-code/provision-resources/use-git-sync.md @@ -11,9 +11,10 @@ labels: products: - enterprise - oss + - cloud title: Manage provisioned repositories with Git Sync menuTitle: Manage repositories with Git Sync -weight: 400 +weight: 120 canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/provision-resources/use-git-sync/ aliases: - ../../../observability-as-code/provision-resources/use-git-sync/ # /docs/grafana/next/observability-as-code/provision-resources/use-git-sync/ From c7345ac11e82af3fbaa8692b6a43b74e2fe30d8a Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 25 Nov 2025 09:51:09 +0100 Subject: [PATCH 094/378] Content outline: Fix runtime error with system variable and ad hoc filters (#114306) --- eslint-suppressions.json | 3 --- .../settings/variables/VariableEditableElement.tsx | 2 +- .../settings/variables/VariableSetEditableElement.tsx | 9 +++++++-- .../settings/variables/components/AdHocVariableForm.tsx | 6 +++--- .../dashboard-scene/settings/variables/utils.test.ts | 2 +- .../features/dashboard-scene/settings/variables/utils.ts | 2 +- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index e9af258606b..2450d3721d7 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2071,9 +2071,6 @@ } }, "public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx": { - "no-restricted-syntax": { - "count": 1 - }, "react-hooks/rules-of-hooks": { "count": 4 } diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx index c9b64183853..78876f4ee55 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx @@ -150,7 +150,7 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable const oldName = useRef(name); return ( - + isEditableVariableType(variable.state.type)); } public useEditPaneOptions = useEditPaneOptions.bind(this, this.set); @@ -71,9 +73,12 @@ export function VariableList({ set }: { set: SceneVariableSet }) { DashboardInteractions.addVariableButtonClicked({ source: 'edit_pane' }); }, [set]); + // Filter out system and snapshot variables - they should not appear in the list + const editableVariables = variables.filter((variable) => isEditableVariableType(variable.state.type)); + return ( - {variables.map((variable) => ( + {editableVariables.map((variable) => ( // TODO fix keyboard a11y here // eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/click-events-have-key-events
onEditVariable(variable)}> diff --git a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx index 5434068f21e..334d7db98da 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx @@ -98,9 +98,9 @@ export function AdHocVariableForm({ > { - if (defaultKeys === undefined) { + if (defaultKeys == null) { onDefaultKeysChange([]); } else { onDefaultKeysChange(undefined); @@ -109,7 +109,7 @@ export function AdHocVariableForm({ /> - {defaultKeys !== undefined && ( + {defaultKeys != null && ( { }); it('should return false for non-editable variable types', () => { - const nonEditableTypes: VariableType[] = ['system']; + const nonEditableTypes: VariableType[] = ['system', 'snapshot']; nonEditableTypes.forEach((type) => { expect(isEditableVariableType(type)).toBe(false); }); diff --git a/public/app/features/dashboard-scene/settings/variables/utils.ts b/public/app/features/dashboard-scene/settings/variables/utils.ts index 23941e3e330..bbb56b0e8c6 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.ts @@ -47,7 +47,7 @@ interface EditableVariableConfig { export type EditableVariableType = Exclude; export function isEditableVariableType(type: VariableType): type is EditableVariableType { - return type !== 'system'; + return type !== 'system' && type !== 'snapshot'; } export const getEditableVariables: () => Record = () => ({ From 18bf4d99cdb8e67593491a94ce9615fba60d4a75 Mon Sep 17 00:00:00 2001 From: Laura Benz <48948963+L-M-K-B@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:28:05 +0100 Subject: [PATCH 095/378] FeatureTracking: Adjust and add events (#114305) * feat: add tracking for opening menu * refactor: add location to tracking event * refactor: apply changes from code review --- .../core/components/AppChrome/QuickAdd/QuickAdd.tsx | 10 +++++++++- .../browse-dashboards/components/CreateNewButton.tsx | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx index 94603269de4..0f7c967f9f6 100644 --- a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx +++ b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx @@ -47,6 +47,14 @@ export const QuickAdd = ({}: Props) => { if (!showQuickAdd) { return null; } + const handleVisibleChange = () => { + if (!isOpen) { + reportInteraction('grafana_create_new_button_menu_opened', { + from: 'quickadd', + }); + } + setIsOpen(!isOpen); + }; const MenuActions = () => { return ( @@ -68,7 +76,7 @@ export const QuickAdd = ({}: Props) => { return showQuickAdd ? ( <> - + { - !isOpen && reportInteraction('grafana_create_new_button_menu_opened'); + if (!isOpen) { + reportInteraction('grafana_create_new_button_menu_opened', { + from: location.pathname, + }); + } setIsOpen(!isOpen); }; From fbc0540528ad700e671e32fa8960e60bc5599145 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 25 Nov 2025 12:39:29 +0300 Subject: [PATCH 096/378] Chore: Remove unused+experimental /dashboards/calculate-diff API support (#114151) --- .../src/clients/rtkq/legacy/endpoints.gen.ts | 22 ----- pkg/api/api.go | 2 - pkg/api/dashboard.go | 97 ------------------- pkg/api/dashboard_test.go | 34 ------- public/api-merged.json | 53 ---------- public/openapi3.json | 50 ---------- 6 files changed, 258 deletions(-) diff --git a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts index 8d2e821668b..b9be8da0043 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts @@ -656,10 +656,6 @@ const injectedRtkApi = api }), providesTags: ['dashboards', 'snapshots'], }), - calculateDashboardDiff: build.mutation({ - query: (queryArg) => ({ url: `/dashboards/calculate-diff`, method: 'POST', body: queryArg.body }), - invalidatesTags: ['dashboards'], - }), postDashboard: build.mutation({ query: (queryArg) => ({ url: `/dashboards/db`, method: 'POST', body: queryArg.saveDashboardCommand }), invalidatesTags: ['dashboards'], @@ -2556,18 +2552,6 @@ export type SearchDashboardSnapshotsApiArg = { /** Limit the number of returned results */ limit?: number; }; -export type CalculateDashboardDiffApiResponse = /** status 200 (empty) */ number[]; -export type CalculateDashboardDiffApiArg = { - body: { - base?: CalculateDiffTarget; - /** The type of diff to return - Description: - `basic` - `json` */ - diffType?: 'basic' | 'json'; - new?: CalculateDiffTarget; - }; -}; export type PostDashboardApiResponse = /** status 200 (empty) */ { /** FolderUID The unique identifier (uid) of the folder the dashboard belongs to. */ folderUid?: string; @@ -4400,11 +4384,6 @@ export type DashboardSnapshotDto = { name?: string; updated?: string; }; -export type CalculateDiffTarget = { - dashboardId?: number; - unsavedDashboard?: Json; - version?: number; -}; export type SaveDashboardCommand = { UpdatedAt?: string; dashboard?: Json; @@ -6636,7 +6615,6 @@ export const { useLazyRouteConvertPrometheusGetRuleGroupQuery, useSearchDashboardSnapshotsQuery, useLazySearchDashboardSnapshotsQuery, - useCalculateDashboardDiffMutation, usePostDashboardMutation, useGetHomeDashboardQuery, useLazyGetHomeDashboardQuery, diff --git a/pkg/api/api.go b/pkg/api/api.go index 918ec160b69..88f432c206c 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -473,8 +473,6 @@ func (hs *HTTPServer) registerRoutes() { }) }) - dashboardRoute.Post("/calculate-diff", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff)) - dashboardRoute.Post("/db", authorize(ac.EvalAny(ac.EvalPermission(dashboards.ActionDashboardsCreate), ac.EvalPermission(dashboards.ActionDashboardsWrite))), routing.Wrap(hs.PostDashboard)) dashboardRoute.Get("/home", routing.Wrap(hs.GetHomeDashboard)) dashboardRoute.Get("/tags", hs.GetDashboardTags) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 1383f007c49..9dfa7c95413 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -19,7 +19,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/components/dashdiffs" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -829,102 +828,6 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R return response.JSON(http.StatusOK, dashVersionMeta) } -// swagger:route POST /dashboards/calculate-diff dashboards calculateDashboardDiff -// -// Perform diff on two dashboards. -// -// Produces: -// - application/json -// - text/html -// -// Responses: -// 200: calculateDashboardDiffResponse -// 401: unauthorisedError -// 403: forbiddenError -// 500: internalServerError -func (hs *HTTPServer) CalculateDashboardDiff(c *contextmodel.ReqContext) response.Response { - ctx, span := tracer.Start(c.Req.Context(), "api.CalculateDashboardDiff") - defer span.End() - c.Req = c.Req.WithContext(ctx) - - apiOptions := dtos.CalculateDiffOptions{} - if err := web.Bind(c.Req, &apiOptions); err != nil { - return response.Error(http.StatusBadRequest, "bad request data", err) - } - - evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(apiOptions.Base.DashboardId, 10))) - if canWrite, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canWrite { - return dashboardGuardianResponse(err) - } - - if apiOptions.Base.DashboardId != apiOptions.New.DashboardId { - evaluator = accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(apiOptions.New.DashboardId, 10))) - if canWrite, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canWrite { - return dashboardGuardianResponse(err) - } - } - - options := dashdiffs.Options{ - OrgId: c.GetOrgID(), - DiffType: dashdiffs.ParseDiffType(apiOptions.DiffType), - Base: dashdiffs.DiffTarget{ - DashboardId: apiOptions.Base.DashboardId, - Version: apiOptions.Base.Version, - UnsavedDashboard: apiOptions.Base.UnsavedDashboard, - }, - New: dashdiffs.DiffTarget{ - DashboardId: apiOptions.New.DashboardId, - Version: apiOptions.New.Version, - UnsavedDashboard: apiOptions.New.UnsavedDashboard, - }, - } - - baseVersionQuery := dashver.GetDashboardVersionQuery{ - DashboardID: options.Base.DashboardId, - Version: options.Base.Version, - OrgID: options.OrgId, - } - - baseVersionRes, err := hs.dashboardVersionService.Get(c.Req.Context(), &baseVersionQuery) - if err != nil { - if errors.Is(err, dashver.ErrDashboardVersionNotFound) { - return response.Error(http.StatusNotFound, "Dashboard version not found", err) - } - return response.Error(http.StatusInternalServerError, "Unable to compute diff", err) - } - - newVersionQuery := dashver.GetDashboardVersionQuery{ - DashboardID: options.New.DashboardId, - Version: options.New.Version, - OrgID: options.OrgId, - } - - newVersionRes, err := hs.dashboardVersionService.Get(c.Req.Context(), &newVersionQuery) - if err != nil { - if errors.Is(err, dashver.ErrDashboardVersionNotFound) { - return response.Error(http.StatusNotFound, "Dashboard version not found", err) - } - return response.Error(http.StatusInternalServerError, "Unable to compute diff", err) - } - - baseData := baseVersionRes.Data - newData := newVersionRes.Data - - result, err := dashdiffs.CalculateDiff(c.Req.Context(), &options, baseData, newData) - if err != nil { - if errors.Is(err, dashver.ErrDashboardVersionNotFound) { - return response.Error(http.StatusNotFound, "Dashboard version not found", err) - } - return response.Error(http.StatusInternalServerError, "Unable to compute diff", err) - } - - if options.DiffType == dashdiffs.DiffDelta { - return response.Respond(http.StatusOK, result.Delta).SetHeader("Content-Type", "application/json") - } - - return response.Respond(http.StatusOK, result.Delta).SetHeader("Content-Type", "text/html") -} - // swagger:route POST /dashboards/uid/{uid}/restore dashboards versions restoreDashboardVersionByUID // // Restore a dashboard to a given dashboard version using UID. diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 362be520230..eceebaacdd6 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -1,7 +1,6 @@ package api import ( - "bytes" "context" "encoding/json" "fmt" @@ -335,17 +334,6 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) { return server.Send(webtest.RequestWithSignedInUser(server.NewGetRequest("/api/dashboards/uid/1/versions"), userWithPermissions(1, permissions))) } - calculateDiff := func(server *webtest.Server, permissions []accesscontrol.Permission) (*http.Response, error) { - cmd := &dtos.CalculateDiffOptions{ - Base: dtos.CalculateDiffTarget{DashboardId: 1, Version: 1}, - New: dtos.CalculateDiffTarget{DashboardId: 1, Version: 2}, - DiffType: "json", - } - jsonBytes, err := json.Marshal(cmd) - require.NoError(t, err) - return server.SendJSON(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/dashboards/calculate-diff", bytes.NewReader(jsonBytes)), userWithPermissions(1, permissions))) - } - t.Run("Should not be able to list dashboard versions without correct permission", func(t *testing.T) { server := setup() @@ -379,28 +367,6 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) { require.NoError(t, res.Body.Close()) }) - - t.Run("Should be able to diff dashboards with correct permissions", func(t *testing.T) { - server := setup() - - permissions := []accesscontrol.Permission{ - {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll}, - } - - res, err := calculateDiff(server, permissions) - require.NoError(t, err) - assert.Equal(t, http.StatusOK, res.StatusCode) - require.NoError(t, res.Body.Close()) - }) - - t.Run("Should not be able to diff dashboards without permissions", func(t *testing.T) { - server := setup() - - res, err := calculateDiff(server, []accesscontrol.Permission{}) - require.NoError(t, err) - assert.Equal(t, http.StatusForbidden, res.StatusCode) - require.NoError(t, res.Body.Close()) - }) } func TestIntegrationDashboardAPIEndpoint(t *testing.T) { diff --git a/public/api-merged.json b/public/api-merged.json index 0c977043122..ace7476c7d5 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -3353,59 +3353,6 @@ } } }, - "/dashboards/calculate-diff": { - "post": { - "produces": [ - "application/json", - "text/html" - ], - "tags": [ - "dashboards" - ], - "summary": "Perform diff on two dashboards.", - "operationId": "calculateDashboardDiff", - "parameters": [ - { - "name": "Body", - "in": "body", - "required": true, - "schema": { - "type": "object", - "properties": { - "base": { - "$ref": "#/definitions/CalculateDiffTarget" - }, - "diffType": { - "description": "The type of diff to return\nDescription:\n`basic`\n`json`", - "type": "string", - "enum": [ - "basic", - "json" - ] - }, - "new": { - "$ref": "#/definitions/CalculateDiffTarget" - } - } - } - } - ], - "responses": { - "200": { - "$ref": "#/responses/calculateDashboardDiffResponse" - }, - "401": { - "$ref": "#/responses/unauthorisedError" - }, - "403": { - "$ref": "#/responses/forbiddenError" - }, - "500": { - "$ref": "#/responses/internalServerError" - } - } - } - }, "/dashboards/db": { "post": { "description": "Creates a new dashboard or updates an existing dashboard.\nNote: This endpoint is not intended for creating folders, use `POST /api/folders` for that.", diff --git a/public/openapi3.json b/public/openapi3.json index 00336f4b49f..eaa4082ab48 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -17605,56 +17605,6 @@ ] } }, - "/dashboards/calculate-diff": { - "post": { - "operationId": "calculateDashboardDiff", - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "base": { - "$ref": "#/components/schemas/CalculateDiffTarget" - }, - "diffType": { - "description": "The type of diff to return\nDescription:\n`basic`\n`json`", - "enum": [ - "basic", - "json" - ], - "type": "string" - }, - "new": { - "$ref": "#/components/schemas/CalculateDiffTarget" - } - }, - "type": "object" - } - } - }, - "required": true, - "x-originalParamName": "Body" - }, - "responses": { - "200": { - "$ref": "#/components/responses/calculateDashboardDiffResponse" - }, - "401": { - "$ref": "#/components/responses/unauthorisedError" - }, - "403": { - "$ref": "#/components/responses/forbiddenError" - }, - "500": { - "$ref": "#/components/responses/internalServerError" - } - }, - "summary": "Perform diff on two dashboards.", - "tags": [ - "dashboards" - ] - } - }, "/dashboards/db": { "post": { "description": "Creates a new dashboard or updates an existing dashboard.\nNote: This endpoint is not intended for creating folders, use `POST /api/folders` for that.", From a2f14d7306d3e3a1de9f82148fd18760e6531e18 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Tue, 25 Nov 2025 10:43:20 +0100 Subject: [PATCH 097/378] Dependencies: Bump golang.org/x/crypto to v0.45.0 (#114384) --- .citools/src/air/go.mod | 6 +- .citools/src/air/go.sum | 24 ++-- .citools/src/bra/go.mod | 2 +- .citools/src/bra/go.sum | 4 +- .citools/src/cog/go.mod | 10 +- .citools/src/cog/go.sum | 24 ++-- .citools/src/cue/go.mod | 12 +- .citools/src/cue/go.sum | 24 ++-- .citools/src/golangci-lint/go.mod | 12 +- .citools/src/golangci-lint/go.sum | 24 ++-- .citools/src/jb/go.mod | 2 +- .citools/src/jb/go.sum | 4 +- .citools/src/lefthook/go.mod | 6 +- .citools/src/lefthook/go.sum | 16 +-- .citools/src/swagger/go.mod | 12 +- .citools/src/swagger/go.sum | 24 ++-- apps/advisor/go.mod | 2 +- apps/advisor/go.sum | 4 +- apps/alerting/notifications/go.mod | 1 + apps/alerting/notifications/go.sum | 4 +- apps/dashboard/go.mod | 2 +- apps/dashboard/go.sum | 4 +- apps/example/go.mod | 2 +- apps/example/go.sum | 4 +- apps/iam/go.mod | 28 ++-- apps/iam/go.sum | 125 +++++------------- apps/plugins/go.mod | 2 +- apps/plugins/go.sum | 4 +- apps/provisioning/go.mod | 2 +- apps/provisioning/go.sum | 4 +- apps/shorturl/go.mod | 2 +- apps/shorturl/go.sum | 4 +- .../docker/blocks/prometheus_high_card/go.mod | 2 +- .../docker/blocks/prometheus_high_card/go.sum | 4 +- devenv/docker/blocks/prometheus_utf8/go.mod | 2 +- devenv/docker/blocks/prometheus_utf8/go.sum | 4 +- go.mod | 2 +- go.sum | 4 +- go.work.sum | 1 + hack/go.mod | 8 +- hack/go.sum | 16 +-- pkg/aggregator/go.mod | 2 +- pkg/aggregator/go.sum | 4 +- pkg/apimachinery/go.mod | 2 +- pkg/apimachinery/go.sum | 4 +- pkg/apiserver/go.mod | 2 +- pkg/apiserver/go.sum | 4 +- pkg/promlib/go.mod | 1 + pkg/promlib/go.sum | 4 +- 49 files changed, 197 insertions(+), 269 deletions(-) diff --git a/.citools/src/air/go.mod b/.citools/src/air/go.mod index 8619eb0641e..e2fa65b61a1 100644 --- a/.citools/src/air/go.mod +++ b/.citools/src/air/go.mod @@ -21,8 +21,8 @@ require ( github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.8.0 // indirect github.com/tdewolff/parse/v2 v2.8.1 // indirect - golang.org/x/net v0.45.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/.citools/src/air/go.sum b/.citools/src/air/go.sum index fbefab6f556..a18e035a8a3 100644 --- a/.citools/src/air/go.sum +++ b/.citools/src/air/go.sum @@ -167,19 +167,19 @@ golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjG golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/.citools/src/bra/go.mod b/.citools/src/bra/go.mod index 43a1908f9c4..f78a5eb40ca 100644 --- a/.citools/src/bra/go.mod +++ b/.citools/src/bra/go.mod @@ -17,6 +17,6 @@ require ( github.com/unknwon/com v1.0.1 // indirect github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a // indirect github.com/urfave/cli v1.22.16 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sys v0.38.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect ) diff --git a/.citools/src/bra/go.sum b/.citools/src/bra/go.sum index 723e36ed099..7c438626368 100644 --- a/.citools/src/bra/go.sum +++ b/.citools/src/bra/go.sum @@ -56,8 +56,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/.citools/src/cog/go.mod b/.citools/src/cog/go.mod index 0ee1004f974..9846c35b0f4 100644 --- a/.citools/src/cog/go.mod +++ b/.citools/src/cog/go.mod @@ -40,11 +40,11 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/yalue/merged_fs v1.3.0 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/.citools/src/cog/go.sum b/.citools/src/cog/go.sum index fc55dd4132c..1bcacb846b3 100644 --- a/.citools/src/cog/go.sum +++ b/.citools/src/cog/go.sum @@ -85,20 +85,20 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY= github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/.citools/src/cue/go.mod b/.citools/src/cue/go.mod index d9a143c75c2..54dd569c1c3 100644 --- a/.citools/src/cue/go.mod +++ b/.citools/src/cue/go.mod @@ -25,13 +25,13 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/tetratelabs/wazero v1.6.0 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/.citools/src/cue/go.sum b/.citools/src/cue/go.sum index 86c2675545b..9b95514fe0d 100644 --- a/.citools/src/cue/go.sum +++ b/.citools/src/cue/go.sum @@ -53,20 +53,20 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g= github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/.citools/src/golangci-lint/go.mod b/.citools/src/golangci-lint/go.mod index d3ac9b697aa..b62f7ee7d3a 100644 --- a/.citools/src/golangci-lint/go.mod +++ b/.citools/src/golangci-lint/go.mod @@ -198,12 +198,12 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.45.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.6.1 // indirect diff --git a/.citools/src/golangci-lint/go.sum b/.citools/src/golangci-lint/go.sum index 4748f406fb8..12b2e9cdab6 100644 --- a/.citools/src/golangci-lint/go.sum +++ b/.citools/src/golangci-lint/go.sum @@ -481,8 +481,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -496,8 +496,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -507,8 +507,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -529,8 +529,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -545,8 +545,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= @@ -559,8 +559,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= diff --git a/.citools/src/jb/go.mod b/.citools/src/jb/go.mod index 96fe587d14f..18bfb09d022 100644 --- a/.citools/src/jb/go.mod +++ b/.citools/src/jb/go.mod @@ -15,6 +15,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/stretchr/testify v1.10.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sys v0.38.0 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect ) diff --git a/.citools/src/jb/go.sum b/.citools/src/jb/go.sum index 82c114a77ad..6a11e4870ff 100644 --- a/.citools/src/jb/go.sum +++ b/.citools/src/jb/go.sum @@ -54,8 +54,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/.citools/src/lefthook/go.mod b/.citools/src/lefthook/go.mod index e37e46a5ea6..ddc74badabc 100644 --- a/.citools/src/lefthook/go.mod +++ b/.citools/src/lefthook/go.mod @@ -43,9 +43,9 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/term v0.35.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/.citools/src/lefthook/go.sum b/.citools/src/lefthook/go.sum index c0e796f4ae9..ba532505786 100644 --- a/.citools/src/lefthook/go.sum +++ b/.citools/src/lefthook/go.sum @@ -91,14 +91,14 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 h1:8ajkpB4hXVftY5ko905id+dOnmorcS2CHNxxHLLDcFM= gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61/go.mod h1:IfMagxm39Ys4ybJrDb7W3Ob8RwxftP0Yy+or/NVz1O8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/.citools/src/swagger/go.mod b/.citools/src/swagger/go.mod index d0ed2ce2bcf..0ac87ea77e3 100644 --- a/.citools/src/swagger/go.mod +++ b/.citools/src/swagger/go.mod @@ -51,12 +51,12 @@ require ( github.com/toqueteos/webbrowser v1.2.0 // indirect go.mongodb.org/mongo-driver v1.16.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/.citools/src/swagger/go.sum b/.citools/src/swagger/go.sum index dff45a5ee81..44c908540b7 100644 --- a/.citools/src/swagger/go.sum +++ b/.citools/src/swagger/go.sum @@ -101,19 +101,19 @@ go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4 go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 9a3fabfd18c..6611470fb8f 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -275,7 +275,7 @@ require ( go.uber.org/mock v0.6.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index ef7adf779d1..142646a8543 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -1302,8 +1302,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index c29ea0b00a8..24c17cc900a 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -84,6 +84,7 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index b67c48676b8..9b84e11f3d6 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -224,8 +224,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 9e98fe233ec..b281b21e4e4 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -106,7 +106,7 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index b4158332679..c0efb792788 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -280,8 +280,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/apps/example/go.mod b/apps/example/go.mod index acb714107b3..66b3b93f6df 100644 --- a/apps/example/go.mod +++ b/apps/example/go.mod @@ -73,7 +73,7 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect diff --git a/apps/example/go.sum b/apps/example/go.sum index 00f1a6a2620..aaf74c34d44 100644 --- a/apps/example/go.sum +++ b/apps/example/go.sum @@ -169,8 +169,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= diff --git a/apps/iam/go.mod b/apps/iam/go.mod index adb3755c58f..e764e2a6158 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -14,12 +14,16 @@ replace github.com/grafana/grafana/apps/provisioning => ../provisioning replace github.com/grafana/grafana/apps/advisor => ../advisor +replace github.com/grafana/grafana/apps/annotation => ../annotation + replace github.com/grafana/grafana/apps/alerting/alertenrichment => ../alerting/alertenrichment replace github.com/grafana/grafana/apps/alerting/notifications => ../alerting/notifications replace github.com/grafana/grafana/apps/alerting/rules => ../alerting/rules +replace github.com/grafana/grafana/apps/collections => ../collections + replace github.com/grafana/grafana/apps/correlations => ../correlations replace github.com/grafana/grafana/apps/investigations => ../investigations @@ -89,16 +93,14 @@ require ( github.com/Masterminds/squirrel v1.5.4 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect - github.com/RoaringBitmap/roaring v1.9.3 // indirect github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect github.com/Yiling-J/theine-go v0.6.2 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/arrow-go/v18 v18.4.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect - github.com/armon/go-radix v1.0.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/at-wat/mqtt-go v0.19.4 // indirect + github.com/at-wat/mqtt-go v0.19.6 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect @@ -119,29 +121,17 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect github.com/aws/smithy-go v1.23.1 // indirect - github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.22.0 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/blevesearch/go-porterstemmer v1.0.3 // indirect - github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/segment v0.9.1 // indirect - github.com/blevesearch/snowballstem v0.9.0 // indirect - github.com/blevesearch/vellum v1.1.0 // indirect github.com/bluele/gcache v0.0.2 // indirect - github.com/blugelabs/bluge v0.2.2 // indirect - github.com/blugelabs/bluge_segment_api v0.2.0 // indirect - github.com/blugelabs/ice v1.0.0 // indirect - github.com/blugelabs/ice/v2 v2.0.1 // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect github.com/bufbuild/protocompile v0.14.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/bwmarrin/snowflake v0.3.0 // indirect - github.com/caio/go-tdigest v3.1.0+incompatible // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -155,7 +145,6 @@ require ( github.com/dennwc/varint v1.0.0 // indirect github.com/dgraph-io/badger/v4 v4.7.0 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect - github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect github.com/dlmiddlecote/sqlstats v1.0.2 // indirect @@ -229,7 +218,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae // indirect + github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect @@ -318,7 +307,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/natefinch/wrap v0.2.0 // indirect @@ -349,7 +337,7 @@ require ( github.com/pressly/goose/v3 v3.25.0 // indirect github.com/prometheus/alertmanager v0.28.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.14.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect @@ -425,7 +413,7 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect gocloud.dev v0.43.0 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index b23eb7f0e4d..c1be3c78ff2 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -182,10 +182,6 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/RoaringBitmap/gocroaring v0.4.0/go.mod h1:NieMwz7ZqwU2DD73/vvYwv7r4eWBKuPVSXZIpsaMwCI= -github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76/go.mod h1:oM0MHmQ3nDsq609SS36p+oYbRi16+oVvU2Bw4Ipv0SE= -github.com/RoaringBitmap/roaring v0.9.1/go.mod h1:h1B7iIUOmnAeb5ytYMvnHJwxMc6LUrwBnzXWRuqTQUc= -github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM= github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= @@ -221,7 +217,6 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= @@ -231,8 +226,8 @@ github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/at-wat/mqtt-go v0.19.4 h1:R2cbCU7O5PHQ38unbe1Y51ncG3KsFEJV6QeipDoqdLQ= -github.com/at-wat/mqtt-go v0.19.4/go.mod h1:AsiWc9kqVOhqq7LzUeWT/AkKUBfx3Sw5cEe8lc06fqA= +github.com/at-wat/mqtt-go v0.19.6 h1:+jo59yGHB3oqBU8nSFYbq6KDgN4ngMpqts5Qtyq61pc= +github.com/at-wat/mqtt-go v0.19.6/go.mod h1:Y/YrYwUlLE4N53//699Zir3WJnxSdJojvLEAbZxJjgQ= github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= @@ -287,7 +282,6 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0= github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= -github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s= github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0= github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -307,8 +301,6 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= -github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= @@ -327,21 +319,16 @@ github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+ github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= -github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA= -github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs= github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= github.com/blevesearch/scorch_segment_api/v2 v2.3.9 h1:X6nJXnNHl7nasXW+U6y2Ns2Aw8F9STszkYkyBfQ+p0o= github.com/blevesearch/scorch_segment_api/v2 v2.3.9/go.mod h1:IrzspZlVjhf4X29oJiEhBxEteTqOY9RlYlk1lCmYHr4= -github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= -github.com/blevesearch/vellum v1.0.5/go.mod h1:atE0EH3fvk43zzS7t1YNdNC7DbmcC3uz+eMD5xZ2OyQ= -github.com/blevesearch/vellum v1.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE= github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w= github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= github.com/blevesearch/zapx/v11 v11.4.1 h1:qFCPlFbsEdwbbckJkysptSQOsHn4s6ZOHL5GMAIAVHA= @@ -433,9 +420,6 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= @@ -444,8 +428,6 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -833,12 +815,10 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae h1:NLPwY3tIP0lg0g9wTRiMcypm6VRXW6W+MOLBsq8JSVA= -github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM= +github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 h1:nbxKRtrbuhvOYmI2RhOYauHRJCtpR+vTNIgg1lFUCws= +github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181/go.mod h1:VtPNIFlEOJPPEc13Ax6ZTbNV3M/sAzLID72YjgzOPVA= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= @@ -853,8 +833,7 @@ github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f h1:5xkjl5Y/j2QefJKO github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f/go.mod h1:+O5QxOwwgP10jedZHapzXY+IPKTnzHBtIs5UUb9G+kI= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe h1:q+QaVANzNZxvTovycpQvDTfsNZ2rHh4XIIaccMnrIR4= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= -github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= -github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= @@ -868,8 +847,7 @@ github.com/grafana/grafana-google-sdk-go v0.4.2 h1:F44hQF1y6UVJhlJPi+Mz+GCJsioVg github.com/grafana/grafana-google-sdk-go v0.4.2/go.mod h1:U73+w9DlbEtUonhQUzERwlXnzWTtfRoyrtKH8d3VY40= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= -github.com/grafana/grafana-plugin-sdk-go v0.281.0 h1:V8dGyatzcOLQeivFhBV2JWMwTSZH/clDnpfKG9p3dTA= -github.com/grafana/grafana-plugin-sdk-go v0.281.0/go.mod h1:3I0g+v6jAwVmrt6BEjDUP4V6pkhGP5QKY5NkXY4Ayr4= +github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k= @@ -883,8 +861,8 @@ github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLm github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000/go.mod h1:/ZklAgE1i4f3Z8uriXwESmCr1VLF8lBGaJspuaGuf78= github.com/grafana/loki/v3 v3.2.1 h1:VB7u+KHfvL5aHAxgoVBvz5wVhsdGuqKC7uuOFOOe7jw= github.com/grafana/loki/v3 v3.2.1/go.mod h1:WvdLl6wOS+yahaeQY+xhD2m2XzkHDfKr5FZaX7D/X2Y= -github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b h1:rFjoqJFb2KxJ29K9ltuWRSsdA46SbN0GCxoQc36h5kg= -github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= +github.com/grafana/nanogit v0.3.0 h1:XNEef+4Vi+465ZITJs/g/xgnDRJbWhhJ7iQrAnWZ0oQ= +github.com/grafana/nanogit v0.3.0/go.mod h1:6s6CCTpyMOHPpcUZaLGI+rgBEKdmxVbhqSGgCK13j7Y= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U= @@ -903,8 +881,7 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDa github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= @@ -995,10 +972,8 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/influxdata/influxdb v1.7.6/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM= github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4= github.com/influxdata/influxql v1.4.0 h1:Lf62rbAF8KWQf+4Djqf4hVXgmQuGozUoSD6kNWjye44= @@ -1080,7 +1055,6 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= -github.com/klauspost/compress v1.15.2/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -1106,8 +1080,6 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353 h1:X/79QL0b4YJVO5+OsPH9rF2u428CIrGL/jLmPsoOQQ4= -github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= @@ -1123,7 +1095,6 @@ github.com/m3db/prometheus_remote_client_golang v0.4.4 h1:DsAIjVKoCp7Ym35tAOFL1O github.com/m3db/prometheus_remote_client_golang v0.4.4/go.mod h1:wHfVbA3eAK6dQvKjCkHhusWYegCk3bDGkA15zymSHdc= github.com/madflojo/testcerts v1.4.0 h1:I09gN0C1ly9IgeVNcAqKk8RAKIJTe3QnFrrPBDyvzN4= github.com/madflojo/testcerts v1.4.0/go.mod h1:MW8sh39gLnkKh4K0Nc55AyHEDl9l/FBLDUsQhpmkuo0= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= @@ -1318,7 +1289,6 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -1377,9 +1347,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= -github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg= @@ -1424,8 +1393,6 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -1471,25 +1438,19 @@ github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJ github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= @@ -1547,7 +1508,6 @@ github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaO github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= @@ -1570,7 +1530,6 @@ github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chq github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= @@ -1721,7 +1680,6 @@ gocloud.dev v0.43.0/go.mod h1:eD8rkg7LhKUHrzkEdLTZ+Ty/vgPHPCd+yMQdfelQVu4= gocloud.dev/secrets/hashivault v0.43.0 h1:A966rEMpCRUE9209/+k+A2HP2v2qDnrxGpQn+nIH5uY= gocloud.dev/secrets/hashivault v0.43.0/go.mod h1:KdWKL+TXDi0cXgEd/MTeaidKlotvyJtnTDi71B3rR9U= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -1734,9 +1692,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1778,8 +1735,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1835,8 +1791,7 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1860,8 +1815,7 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1875,8 +1829,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1884,8 +1837,6 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1898,7 +1849,6 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1970,16 +1920,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= -golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= +golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1990,8 +1937,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2059,8 +2005,7 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= @@ -2075,7 +2020,6 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6f gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= @@ -2319,31 +2263,24 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= -k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= -k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= -k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= -k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= -k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= -k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= -k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= +k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kms v0.34.1 h1:iCFOvewDPzWM9fMTfyIPO+4MeuZ0tcZbugxLNSHFG4w= -k8s.io/kms v0.34.1/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= +k8s.io/kms v0.34.2 h1:91rj4MDZLyIT9KxG8J5/CcMH666Z88CF/xJQeuPfJc8= k8s.io/kms v0.34.2/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= -k8s.io/kube-aggregator v0.34.1 h1:WNLV0dVNoFKmuyvdWLd92iDSyD/TSTjqwaPj0U9XAEU= -k8s.io/kube-aggregator v0.34.1/go.mod h1:RU8j+5ERfp0h+gIvWtxRPfsa5nK7rboDm8RST8BJfYQ= +k8s.io/kube-aggregator v0.34.2 h1:Nn0Vksj67WHBL2x7bJ6vuxL44RbMTK6uRtXX+3vMVJk= +k8s.io/kube-aggregator v0.34.2/go.mod h1:/tp4cc/1p2AvICsS4mjjSJakdrbhcGbRmj0mdHTdR2Q= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index 8fef8fe3e60..d2b2f2e8045 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -161,7 +161,7 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index 679aa7bdd49..1c479c59942 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -456,8 +456,8 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index a72aa77fe68..0fb19b230c8 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -68,7 +68,7 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index 92a4d53c282..db6bd3cf414 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -156,8 +156,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index 382cf8dd051..b3db27ba205 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -73,7 +73,7 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect diff --git a/apps/shorturl/go.sum b/apps/shorturl/go.sum index e31288da121..f8b394c0f37 100644 --- a/apps/shorturl/go.sum +++ b/apps/shorturl/go.sum @@ -169,8 +169,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= diff --git a/devenv/docker/blocks/prometheus_high_card/go.mod b/devenv/docker/blocks/prometheus_high_card/go.mod index 1e3709bbe66..a2608e2cc43 100644 --- a/devenv/docker/blocks/prometheus_high_card/go.mod +++ b/devenv/docker/blocks/prometheus_high_card/go.mod @@ -13,6 +13,6 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sys v0.38.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/devenv/docker/blocks/prometheus_high_card/go.sum b/devenv/docker/blocks/prometheus_high_card/go.sum index d2e136dfd38..46002ecb53d 100644 --- a/devenv/docker/blocks/prometheus_high_card/go.sum +++ b/devenv/docker/blocks/prometheus_high_card/go.sum @@ -24,8 +24,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/devenv/docker/blocks/prometheus_utf8/go.mod b/devenv/docker/blocks/prometheus_utf8/go.mod index 2ecc8267f24..b26177d8e9a 100644 --- a/devenv/docker/blocks/prometheus_utf8/go.mod +++ b/devenv/docker/blocks/prometheus_utf8/go.mod @@ -15,6 +15,6 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sys v0.38.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/devenv/docker/blocks/prometheus_utf8/go.sum b/devenv/docker/blocks/prometheus_utf8/go.sum index d2e136dfd38..46002ecb53d 100644 --- a/devenv/docker/blocks/prometheus_utf8/go.sum +++ b/devenv/docker/blocks/prometheus_utf8/go.sum @@ -24,8 +24,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go.mod b/go.mod index 576f6c0614f..936cf97655b 100644 --- a/go.mod +++ b/go.mod @@ -200,7 +200,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // @grafana/alerting-backend gocloud.dev v0.43.0 // @grafana/grafana-app-platform-squad gocloud.dev/secrets/hashivault v0.43.0 // @grafana/grafana-operator-experience-squad - golang.org/x/crypto v0.44.0 // @grafana/grafana-backend-group + golang.org/x/crypto v0.45.0 // @grafana/grafana-backend-group golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // @grafana/alerting-backend golang.org/x/mod v0.30.0 // indirect; @grafana/grafana-backend-group golang.org/x/net v0.47.0 // @grafana/oss-big-tent @grafana/partner-datasources diff --git a/go.sum b/go.sum index 1c294bc1fe2..f7c4c8e5578 100644 --- a/go.sum +++ b/go.sum @@ -2784,8 +2784,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= diff --git a/go.work.sum b/go.work.sum index bbfe799f72c..72f5dd7e97d 100644 --- a/go.work.sum +++ b/go.work.sum @@ -592,6 +592,7 @@ github.com/couchbase/ghistogram v0.1.0 h1:b95QcQTCzjTUocDXp/uMgSNQi8oj1tGwnJ4bOD github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k= github.com/couchbase/moss v0.2.0 h1:VCYrMzFwEryyhRSeI+/b3tRBSeTpi/8gn5Kf6dxqn+o= github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= diff --git a/hack/go.mod b/hack/go.mod index f9212c08bb8..b35dd7f5a76 100644 --- a/hack/go.mod +++ b/hack/go.mod @@ -7,10 +7,10 @@ require k8s.io/code-generator v0.34.2 require ( github.com/go-logr/logr v1.4.2 // indirect github.com/spf13/pflag v1.0.6 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f // indirect k8s.io/klog/v2 v2.130.1 // indirect ) diff --git a/hack/go.sum b/hack/go.sum index 1dd0408cf4e..25c035d777a 100644 --- a/hack/go.sum +++ b/hack/go.sum @@ -4,14 +4,14 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= k8s.io/code-generator v0.34.2 h1:9bG6jTxmsU3HXE5BNYJTC8AZ1D6hVVfkm8yYSkdkGY0= k8s.io/code-generator v0.34.2/go.mod h1:dnDDEd6S/z4uZ+PG1aE58ySCi/lR4+qT3a4DddE4/2I= k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index e822e4b6a62..32ba2addf5e 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -117,7 +117,7 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 8ab04c12ddf..2f44039e35c 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -314,8 +314,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index df34d602434..fdcf1562f1f 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -43,7 +43,7 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index f5123c3038a..3b59ab0b8b9 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -96,8 +96,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 6947fb20b67..4a3a9d7a84b 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -83,7 +83,7 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 49b4ab91e93..52c046f42a0 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -207,8 +207,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index c60a20cee8d..877b00ae2e4 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -99,6 +99,7 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index f8c29e54262..066b92e7746 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -321,8 +321,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= From b57e6383e460b6b84e208756f6ad880ff9856c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Tue, 25 Nov 2025 10:58:03 +0100 Subject: [PATCH 098/378] refactor(unified-storage): move builders in their own package (#114375) --- apps/advisor/go.sum | 47 ---- .../dashboard/legacysearcher/search_client.go | 14 +- .../legacysearcher/search_client_test.go | 24 +- pkg/registry/apis/dashboard/search.go | 6 +- pkg/registry/apis/iam/user/legacy_search.go | 6 +- pkg/server/wire_gen.go | 7 +- pkg/server/wireexts_oss.go | 5 +- .../dashboards/service/dashboard_service.go | 4 +- .../service/dashboard_service_test.go | 4 +- .../dashboards/service/search/search_test.go | 10 +- pkg/storage/unified/search/bleve.go | 9 +- .../unified/search/bleve_search_test.go | 5 +- pkg/storage/unified/search/bleve_test.go | 29 +-- .../search/{ => builders}/dashboard.go | 12 +- .../search/{ => builders}/dashboard_stats.go | 2 +- .../unified/search/builders/document.go | 66 ++++++ .../search/{ => builders}/document_test.go | 29 ++- .../testdata/doc/dashboard-aaa-out.json | 0 .../testdata/doc/dashboard-aaa.json | 0 .../testdata/doc/folder-aaa-out.json | 0 .../testdata/doc/folder-aaa.json | 0 .../testdata/doc/folder-bbb-out.json | 0 .../testdata/doc/folder-bbb.json | 0 .../testdata/doc/playlist-aaa-out.json | 0 .../testdata/doc/playlist-aaa.json | 0 .../testdata/doc/report-aaa-out.json | 0 .../testdata/doc/report-aaa.json | 0 .../testdata/doc/user-example-out.json | 0 .../testdata/doc/user-example.json | 0 .../user-user-with-login-and-email-out.json | 0 .../doc/user-user-with-login-and-email.json | 0 .../doc/user-user-with-login-only-out.json | 0 .../doc/user-user-with-login-only.json | 0 .../unified/search/{ => builders}/user.go | 3 +- pkg/storage/unified/search/document.go | 73 +----- pkg/storage/unified/search/user_test.go | 215 ------------------ 36 files changed, 162 insertions(+), 408 deletions(-) rename pkg/storage/unified/search/{ => builders}/dashboard.go (96%) rename pkg/storage/unified/search/{ => builders}/dashboard_stats.go (94%) create mode 100644 pkg/storage/unified/search/builders/document.go rename pkg/storage/unified/search/{ => builders}/document_test.go (79%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/dashboard-aaa-out.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/dashboard-aaa.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/folder-aaa-out.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/folder-aaa.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/folder-bbb-out.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/folder-bbb.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/playlist-aaa-out.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/playlist-aaa.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/report-aaa-out.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/report-aaa.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/user-example-out.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/user-example.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/user-user-with-login-and-email-out.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/user-user-with-login-and-email.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/user-user-with-login-only-out.json (100%) rename pkg/storage/unified/search/{ => builders}/testdata/doc/user-user-with-login-only.json (100%) rename pkg/storage/unified/search/{ => builders}/user.go (96%) delete mode 100644 pkg/storage/unified/search/user_test.go diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 142646a8543..79aea07d576 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -130,9 +130,6 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM= -github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= -github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f h1:HR5nRmUQgXrwqZOwZ2DAc/aCi3Bu3xENpspW935vxu0= @@ -222,48 +219,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= -github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= -github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/blevesearch/bleve/v2 v2.5.0 h1:HzYqBy/5/M9Ul9ESEmXzN/3Jl7YpmWBdHM/+zzv/3k4= -github.com/blevesearch/bleve/v2 v2.5.0/go.mod h1:PcJzTPnEynO15dCf9isxOga7YFRa/cMSsbnRwnszXUk= -github.com/blevesearch/bleve_index_api v1.2.7 h1:c8r9vmbaYQroAMSGag7zq5gEVPiuXrUQDqfnj7uYZSY= -github.com/blevesearch/bleve_index_api v1.2.7/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= -github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM= -github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w= -github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U= -github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= -github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= -github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= -github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= -github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= -github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= -github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= -github.com/blevesearch/scorch_segment_api/v2 v2.3.9 h1:X6nJXnNHl7nasXW+U6y2Ns2Aw8F9STszkYkyBfQ+p0o= -github.com/blevesearch/scorch_segment_api/v2 v2.3.9/go.mod h1:IrzspZlVjhf4X29oJiEhBxEteTqOY9RlYlk1lCmYHr4= -github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= -github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= -github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= -github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= -github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= -github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= -github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w= -github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= -github.com/blevesearch/zapx/v11 v11.4.1 h1:qFCPlFbsEdwbbckJkysptSQOsHn4s6ZOHL5GMAIAVHA= -github.com/blevesearch/zapx/v11 v11.4.1/go.mod h1:qNOGxIqdPC1MXauJCD9HBG487PxviTUUbmChFOAosGs= -github.com/blevesearch/zapx/v12 v12.4.1 h1:K77bhypII60a4v8mwvav7r4IxWA8qxhNjgF9xGdb9eQ= -github.com/blevesearch/zapx/v12 v12.4.1/go.mod h1:QRPrlPOzAxBNMI0MkgdD+xsTqx65zbuPr3Ko4Re49II= -github.com/blevesearch/zapx/v13 v13.4.1 h1:EnkEMZFUK0lsW/jOJJF2xOcp+W8TjEsyeN5BeAZEYYE= -github.com/blevesearch/zapx/v13 v13.4.1/go.mod h1:e6duBMlCvgbH9rkzNMnUa9hRI9F7ri2BRcHfphcmGn8= -github.com/blevesearch/zapx/v14 v14.4.1 h1:G47kGCshknBZzZAtjcnIAMn3oNx8XBLxp8DMq18ogyE= -github.com/blevesearch/zapx/v14 v14.4.1/go.mod h1:O7sDxiaL2r2PnCXbhh1Bvm7b4sP+jp4unE9DDPWGoms= -github.com/blevesearch/zapx/v15 v15.4.1 h1:B5IoTMUCEzFdc9FSQbhVOxAY+BO17c05866fNruiI7g= -github.com/blevesearch/zapx/v15 v15.4.1/go.mod h1:b/MreHjYeQoLjyY2+UaM0hGZZUajEbE0xhnr1A2/Q6Y= -github.com/blevesearch/zapx/v16 v16.2.2 h1:MifKJVRTEhMTgSlle2bDRTb39BGc9jXFRLPZc6r0Rzk= -github.com/blevesearch/zapx/v16 v16.2.2/go.mod h1:B9Pk4G1CqtErgQV9DyCSA9Lb7WZe4olYfGw7fVDZ4sk= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= @@ -526,8 +485,6 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-migrate/migrate/v4 v4.7.0 h1:gONcHxHApDTKXDyLH/H97gEHmpu1zcnnbAaq2zgrPrs= github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= -github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo= -github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -936,8 +893,6 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWu github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= -github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -1184,8 +1139,6 @@ github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtC github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= -go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= -go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo= go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk= diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go index dde82737e23..9782ce6326d 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -23,7 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - unisearch "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" ) type DashboardSearchClient struct { @@ -37,11 +37,11 @@ func NewDashboardSearchClient(dashboardStore dashboards.Store, sorter sort.Servi } var sortByMapping = map[string]string{ - unisearch.DASHBOARD_VIEWS_LAST_30_DAYS: "viewed-recently", - unisearch.DASHBOARD_VIEWS_TOTAL: "viewed", - unisearch.DASHBOARD_ERRORS_LAST_30_DAYS: "errors-recently", - unisearch.DASHBOARD_ERRORS_TOTAL: "errors", - "title": "alpha", + builders.DASHBOARD_VIEWS_LAST_30_DAYS: "viewed-recently", + builders.DASHBOARD_VIEWS_TOTAL: "viewed", + builders.DASHBOARD_ERRORS_LAST_30_DAYS: "errors-recently", + builders.DASHBOARD_ERRORS_TOTAL: "errors", + "title": "alpha", } func ParseSortName(sortName string) (string, bool, error) { @@ -231,7 +231,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resourcepb.Reso } query.ManagerIdentity = vals[0] - case unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE: + case builders.DASHBOARD_LIBRARY_PANEL_REFERENCE: if len(vals) != 1 { return nil, apierrors.NewBadRequest("only one library panel uid is supported") } diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go index 8f1974bee68..cb7ead459f6 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client_test.go @@ -19,7 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - unisearch "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" ) func TestDashboardSearchClient_Search(t *testing.T) { @@ -130,7 +130,7 @@ func TestDashboardSearchClient_Search(t *testing.T) { }, SortBy: []*resourcepb.ResourceSearchRequest_Sort{ { - Field: resource.SEARCH_FIELD_PREFIX + unisearch.DASHBOARD_VIEWS_TOTAL, // "fields." prefix should be removed + Field: resource.SEARCH_FIELD_PREFIX + builders.DASHBOARD_VIEWS_TOTAL, // "fields." prefix should be removed Desc: false, }, }, @@ -195,7 +195,7 @@ func TestDashboardSearchClient_Search(t *testing.T) { }, SortBy: []*resourcepb.ResourceSearchRequest_Sort{ { - Field: unisearch.DASHBOARD_ERRORS_LAST_30_DAYS, + Field: builders.DASHBOARD_ERRORS_LAST_30_DAYS, Desc: true, }, }, @@ -560,7 +560,7 @@ func TestDashboardSearchClient_Search(t *testing.T) { }, SortBy: []*resourcepb.ResourceSearchRequest_Sort{ { - Field: resource.SEARCH_FIELD_PREFIX + unisearch.DASHBOARD_VIEWS_TOTAL, + Field: resource.SEARCH_FIELD_PREFIX + builders.DASHBOARD_VIEWS_TOTAL, }, }, } @@ -591,7 +591,7 @@ func TestDashboardSearchClient_Search(t *testing.T) { Key: dashboardKey, Fields: []*resourcepb.Requirement{ { - Key: unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE, + Key: builders.DASHBOARD_LIBRARY_PANEL_REFERENCE, Operator: "=", Values: []string{"test-library-panel"}, }, @@ -633,7 +633,7 @@ func TestDashboardSearchClient_Search(t *testing.T) { Key: dashboardKey, Fields: []*resourcepb.Requirement{ { - Key: unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE, + Key: builders.DASHBOARD_LIBRARY_PANEL_REFERENCE, Operator: "=", Values: []string{"panel1", "panel2"}, }, @@ -652,7 +652,7 @@ func TestDashboardSearchClient_Search(t *testing.T) { Key: dashboardKey, Fields: []*resourcepb.Requirement{ { - Key: unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE, + Key: builders.DASHBOARD_LIBRARY_PANEL_REFERENCE, Operator: "=", Values: []string{"test-library-panel"}, }, @@ -679,7 +679,7 @@ func TestDashboardSearchClient_Search(t *testing.T) { Key: dashboardKey, Fields: []*resourcepb.Requirement{ { - Key: unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE, + Key: builders.DASHBOARD_LIBRARY_PANEL_REFERENCE, Operator: "=", Values: []string{"unused-library-panel"}, }, @@ -713,28 +713,28 @@ func TestParseSortName(t *testing.T) { { name: "viewed-recently with desc suffix", sortName: "viewed-recently-desc", - wantField: unisearch.DASHBOARD_VIEWS_LAST_30_DAYS, + wantField: builders.DASHBOARD_VIEWS_LAST_30_DAYS, wantDesc: true, wantErr: false, }, { name: "defaults to desc", sortName: "viewed", - wantField: unisearch.DASHBOARD_VIEWS_TOTAL, + wantField: builders.DASHBOARD_VIEWS_TOTAL, wantDesc: true, wantErr: false, }, { name: "errors-recentlyy with asc suffix", sortName: "errors-recently-asc", - wantField: unisearch.DASHBOARD_ERRORS_LAST_30_DAYS, + wantField: builders.DASHBOARD_ERRORS_LAST_30_DAYS, wantDesc: false, wantErr: false, }, { name: "errors - defaults to desc too", sortName: "errors", - wantField: unisearch.DASHBOARD_ERRORS_TOTAL, + wantField: builders.DASHBOARD_ERRORS_TOTAL, wantDesc: true, wantErr: false, }, diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 0f1b83a7e18..6d85a66b344 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -32,7 +32,7 @@ import ( "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" "github.com/grafana/grafana/pkg/util/errhttp" ) @@ -406,7 +406,7 @@ func convertHttpSearchRequestToResourceSearchRequest(queryParams url.Values, use // Add sorting if queryParams.Has("sort") { for _, sort := range queryParams["sort"] { - if slices.Contains(search.DashboardFields(), sort) { + if slices.Contains(builders.DashboardFields(), sort) { sort = resource.SEARCH_FIELD_PREFIX + sort } s := &resourcepb.ResourceSearchRequest_Sort{Field: sort} @@ -441,7 +441,7 @@ func convertHttpSearchRequestToResourceSearchRequest(queryParams url.Values, use // The libraryPanel filter if libraryPanel, ok := queryParams["libraryPanel"]; ok { searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{ - Key: search.DASHBOARD_LIBRARY_PANEL_REFERENCE, + Key: builders.DASHBOARD_LIBRARY_PANEL_REFERENCE, Operator: "=", Values: libraryPanel, }) diff --git a/pkg/registry/apis/iam/user/legacy_search.go b/pkg/registry/apis/iam/user/legacy_search.go index 759dc7f55a7..7459e7074e4 100644 --- a/pkg/registry/apis/iam/user/legacy_search.go +++ b/pkg/registry/apis/iam/user/legacy_search.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/user" res "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" ) const ( @@ -129,9 +129,9 @@ func getColumns(fields []string) []*resourcepb.ResourceTableColumnDefinition { for _, field := range fields { switch field { case "email": - columns = append(columns, search.TableColumnDefinitions[search.USER_EMAIL]) + columns = append(columns, builders.TableColumnDefinitions[builders.USER_EMAIL]) case "login": - columns = append(columns, search.TableColumnDefinitions[search.USER_LOGIN]) + columns = append(columns, builders.TableColumnDefinitions[builders.USER_LOGIN]) } } return columns diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 4dc7523cbb6..9d4dcab2f7e 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -265,6 +265,7 @@ import ( migrations2 "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" "github.com/grafana/grafana/pkg/tsdb/cloud-monitoring" @@ -455,7 +456,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - ossDashboardStats := search.ProvideDashboardStats() + ossDashboardStats := builders.ProvideDashboardStats() documentBuilderSupplier := search.ProvideDocumentBuilders(sqlStore, ossDashboardStats) databaseDatabase := database4.ProvideDatabase(sqlStore, tracer) clockClock := clock.ProvideClock() @@ -1102,7 +1103,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - ossDashboardStats := search.ProvideDashboardStats() + ossDashboardStats := builders.ProvideDashboardStats() documentBuilderSupplier := search.ProvideDocumentBuilders(sqlStore, ossDashboardStats) databaseDatabase := database4.ProvideDatabase(sqlStore, tracer) clockClock := clock.ProvideClock() @@ -1757,7 +1758,7 @@ func InitializeDocumentBuilders(cfg *setting.Cfg) (resource.DocumentBuilderSuppl if err != nil { return nil, err } - ossDashboardStats := search.ProvideDashboardStats() + ossDashboardStats := builders.ProvideDashboardStats() documentBuilderSupplier := search.ProvideDocumentBuilders(sqlStore, ossDashboardStats) return documentBuilderSupplier, nil } diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 9f069f807fc..4d70c3f2f4c 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -66,6 +66,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified" "github.com/grafana/grafana/pkg/storage/unified/resource" search2 "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" "github.com/grafana/grafana/pkg/storage/unified/sql" ) @@ -138,8 +139,8 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(auth.IDSigner), new(*idimpl.LocalSigner)), manager.ProvideInstaller, wire.Bind(new(plugins.Installer), new(*manager.PluginInstaller)), - search2.ProvideDashboardStats, - wire.Bind(new(search2.DashboardStats), new(*search2.OssDashboardStats)), + builders.ProvideDashboardStats, + wire.Bind(new(builders.DashboardStats), new(*builders.OssDashboardStats)), search2.ProvideDocumentBuilders, sandbox.ProvideService, wire.Bind(new(sandbox.Sandbox), new(*sandbox.Service)), diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index ebf3f24beb6..b63b4a40f96 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -57,7 +57,7 @@ import ( "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/retryer" ) @@ -479,7 +479,7 @@ func (dr *DashboardServiceImpl) GetDashboardsByLibraryPanelUID(ctx context.Conte Options: &resourcepb.ListOptions{ Fields: []*resourcepb.Requirement{ { - Key: search.DASHBOARD_LIBRARY_PANEL_REFERENCE, + Key: builders.DASHBOARD_LIBRARY_PANEL_REFERENCE, Operator: string(selection.Equals), Values: []string{libraryPanelUID}, }, diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 7aa0e07762c..5725cdf193b 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -46,7 +46,7 @@ import ( "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util/testutil" ) @@ -2654,7 +2654,7 @@ func TestGetDashboardsByLibraryPanelUID(t *testing.T) { k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool { return len(req.Options.Fields) == 1 && - req.Options.Fields[0].Key == search.DASHBOARD_LIBRARY_PANEL_REFERENCE && + req.Options.Fields[0].Key == builders.DASHBOARD_LIBRARY_PANEL_REFERENCE && req.Options.Fields[0].Values[0] == "test-library-panel" })).Return(searchResponse, nil).Once() diff --git a/pkg/services/dashboards/service/search/search_test.go b/pkg/services/dashboards/service/search/search_test.go index 11279799658..0683cbcb55b 100644 --- a/pkg/services/dashboards/service/search/search_test.go +++ b/pkg/services/dashboards/service/search/search_test.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" ) // regression test - parsing int32 values from search results was causing a panic @@ -25,11 +25,11 @@ func TestParseResults(t *testing.T) { Type: resourcepb.ResourceTableColumnDefinition_STRING, }, { - Name: search.DASHBOARD_ERRORS_LAST_1_DAYS, + Name: builders.DASHBOARD_ERRORS_LAST_1_DAYS, Type: resourcepb.ResourceTableColumnDefinition_INT64, }, { - Name: search.DASHBOARD_LINK_COUNT, + Name: builders.DASHBOARD_LINK_COUNT, Type: resourcepb.ResourceTableColumnDefinition_INT32, }, { @@ -75,11 +75,11 @@ func TestParseResults(t *testing.T) { Type: resourcepb.ResourceTableColumnDefinition_STRING, }, { - Name: search.DASHBOARD_ERRORS_LAST_1_DAYS, + Name: builders.DASHBOARD_ERRORS_LAST_1_DAYS, Type: resourcepb.ResourceTableColumnDefinition_INT64, }, { - Name: search.DASHBOARD_LINK_COUNT, + Name: builders.DASHBOARD_LINK_COUNT, Type: resourcepb.ResourceTableColumnDefinition_INT32, }, { diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 36c68183203..a8c1efb431c 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -32,6 +32,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" authlib "github.com/grafana/authlib/types" @@ -1178,10 +1179,11 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.R facets[f.Field] = bleve.NewFacetRequest(f.Field, int(f.Limit)) } - // Convert resource-specific fields to bleve fields (just considers dashboard fields for now) + // Convert resource-specific fields to bleve fields. + // TODO: use b.fields.Field(f) instead of builders.DashboardFields() to avoid dashboard-specific code in search server. fields := make([]string, 0, len(req.Fields)) for _, f := range req.Fields { - if slices.Contains(DashboardFields(), f) { + if slices.Contains(builders.DashboardFields(), f) { f = resource.SEARCH_FIELD_PREFIX + f } fields = append(fields, f) @@ -1534,7 +1536,8 @@ func getSortFields(req *resourcepb.ResourceSearchRequest) []string { input = field } - if slices.Contains(DashboardFields(), input) { + // TODO: pass fields parameter and use fields.Field(input) instead of builders.DashboardFields() to avoid dashboard-specific code. + if slices.Contains(builders.DashboardFields(), input) { input = resource.SEARCH_FIELD_PREFIX + input } diff --git a/pkg/storage/unified/search/bleve_search_test.go b/pkg/storage/unified/search/bleve_search_test.go index 600dd0cfe04..7374374fd25 100644 --- a/pkg/storage/unified/search/bleve_search_test.go +++ b/pkg/storage/unified/search/bleve_search_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" ) const threshold = 9999 @@ -265,8 +266,8 @@ func newTestDashboardsIndex(t testing.TB, threshold int64, size int64, writer re ctx := identity.WithRequester(context.Background(), &user.SignedInUser{Namespace: "ns"}) - info, err := search.DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) { - return &search.DashboardDocumentBuilder{ + info, err := builders.DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) { + return &builders.DashboardDocumentBuilder{ Namespace: namespace, Blob: blob, Stats: make(map[string]map[string]int64), // empty stats diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index 4c66fc4c1b5..2d2e7142eaf 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -32,6 +32,7 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" ) // This verifies that we close all indexes properly and shutdown all background goroutines from our tests. @@ -76,8 +77,8 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { t.Run("build dashboards", func(t *testing.T) { key := dashboardskey - info, err := DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) { - return &DashboardDocumentBuilder{ + info, err := builders.DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) { + return &builders.DashboardDocumentBuilder{ Namespace: namespace, Blob: blob, Stats: make(map[string]map[string]int64), // empty stats @@ -107,9 +108,9 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { Title: "aaa (dash)", Folder: "xxx", Fields: map[string]any{ - DASHBOARD_PANEL_TYPES: []string{"timeseries", "table"}, - DASHBOARD_ERRORS_TODAY: 25, - DASHBOARD_VIEWS_LAST_1_DAYS: 50, + builders.DASHBOARD_PANEL_TYPES: []string{"timeseries", "table"}, + builders.DASHBOARD_ERRORS_TODAY: 25, + builders.DASHBOARD_VIEWS_LAST_1_DAYS: 50, }, Labels: map[string]string{ utils.LabelKeyDeprecatedInternalID: "10", // nolint:staticcheck @@ -140,9 +141,9 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { Title: "bbb (dash)", Folder: "xxx", Fields: map[string]any{ - DASHBOARD_PANEL_TYPES: []string{"timeseries"}, - DASHBOARD_ERRORS_TODAY: 40, - DASHBOARD_VIEWS_LAST_1_DAYS: 100, + builders.DASHBOARD_PANEL_TYPES: []string{"timeseries"}, + builders.DASHBOARD_ERRORS_TODAY: 40, + builders.DASHBOARD_VIEWS_LAST_1_DAYS: 100, }, Tags: []string{"aa"}, Labels: map[string]string{ @@ -271,15 +272,15 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { Key: key, }, Limit: 100000, - Fields: []string{DASHBOARD_ERRORS_TODAY, DASHBOARD_VIEWS_LAST_1_DAYS, "fieldThatDoesntExist"}, + Fields: []string{builders.DASHBOARD_ERRORS_TODAY, builders.DASHBOARD_VIEWS_LAST_1_DAYS, "fieldThatDoesntExist"}, SortBy: []*resourcepb.ResourceSearchRequest_Sort{ - {Field: "fields." + DASHBOARD_VIEWS_LAST_1_DAYS, Desc: true}, + {Field: "fields." + builders.DASHBOARD_VIEWS_LAST_1_DAYS, Desc: true}, }, }, nil, nil) require.NoError(t, err) require.Equal(t, 2, len(rsp.Results.Columns)) - require.Equal(t, DASHBOARD_ERRORS_TODAY, rsp.Results.Columns[0].Name) - require.Equal(t, DASHBOARD_VIEWS_LAST_1_DAYS, rsp.Results.Columns[1].Name) + require.Equal(t, builders.DASHBOARD_ERRORS_TODAY, rsp.Results.Columns[0].Name) + require.Equal(t, builders.DASHBOARD_VIEWS_LAST_1_DAYS, rsp.Results.Columns[1].Name) // sorted descending so should start with highest dashboard_views_last_1_days (100) val, err := resource.DecodeCell(rsp.Results.Columns[1], 0, rsp.Results.Rows[0].Cells[1]) require.NoError(t, err) @@ -291,9 +292,9 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { Key: key, }, Limit: 100000, - Fields: []string{DASHBOARD_ERRORS_TODAY, DASHBOARD_VIEWS_LAST_1_DAYS, "fieldThatDoesntExist"}, + Fields: []string{builders.DASHBOARD_ERRORS_TODAY, builders.DASHBOARD_VIEWS_LAST_1_DAYS, "fieldThatDoesntExist"}, SortBy: []*resourcepb.ResourceSearchRequest_Sort{ - {Field: "fields." + DASHBOARD_VIEWS_LAST_1_DAYS, Desc: true}, + {Field: "fields." + builders.DASHBOARD_VIEWS_LAST_1_DAYS, Desc: true}, }, }, nil, nil) require.NoError(t, err) diff --git a/pkg/storage/unified/search/dashboard.go b/pkg/storage/unified/search/builders/dashboard.go similarity index 96% rename from pkg/storage/unified/search/dashboard.go rename to pkg/storage/unified/search/builders/dashboard.go index 44a822a1b4c..4f8d55111a9 100644 --- a/pkg/storage/unified/search/dashboard.go +++ b/pkg/storage/unified/search/builders/dashboard.go @@ -1,4 +1,4 @@ -package search +package builders import ( "bytes" @@ -15,10 +15,6 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) -//------------------------------------------------------------ -// Standard dashboard fields -//------------------------------------------------------------ - const DASHBOARD_SCHEMA_VERSION = "schema_version" const DASHBOARD_LINK_COUNT = "link_count" const DASHBOARD_PANEL_TYPES = "panel_types" @@ -26,10 +22,6 @@ const DASHBOARD_DS_TYPES = "ds_types" const DASHBOARD_TRANSFORMATIONS = "transformation" const DASHBOARD_LIBRARY_PANEL_REFERENCE = "reference.LibraryPanel" -//------------------------------------------------------------ -// The following fields are added in enterprise -//------------------------------------------------------------ - const DASHBOARD_VIEWS_LAST_1_DAYS = "views_last_1_days" const DASHBOARD_VIEWS_LAST_7_DAYS = "views_last_7_days" const DASHBOARD_VIEWS_LAST_30_DAYS = "views_last_30_days" @@ -268,7 +260,6 @@ func (s *DashboardDocumentBuilder) BuildDocument(ctx context.Context, key *resou if err != nil { return nil, err } - // metadata name is the dashboard uid summary.UID = obj.GetName() summary.ID = obj.GetDeprecatedInternalID() // nolint:staticcheck @@ -331,7 +322,6 @@ func (s *DashboardDocumentBuilder) BuildDocument(ctx context.Context, key *resou doc.Fields[DASHBOARD_TRANSFORMATIONS] = transformations } - // Add the stats fields for k, v := range s.Stats[summary.UID] { doc.Fields[k] = v } diff --git a/pkg/storage/unified/search/dashboard_stats.go b/pkg/storage/unified/search/builders/dashboard_stats.go similarity index 94% rename from pkg/storage/unified/search/dashboard_stats.go rename to pkg/storage/unified/search/builders/dashboard_stats.go index c82db38110e..56024275649 100644 --- a/pkg/storage/unified/search/dashboard_stats.go +++ b/pkg/storage/unified/search/builders/dashboard_stats.go @@ -1,4 +1,4 @@ -package search +package builders import ( "context" diff --git a/pkg/storage/unified/search/builders/document.go b/pkg/storage/unified/search/builders/document.go new file mode 100644 index 00000000000..24226cdcc9a --- /dev/null +++ b/pkg/storage/unified/search/builders/document.go @@ -0,0 +1,66 @@ +package builders + +import ( + "context" + + claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/store/kind/dashboard" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +// All returns all document builders from this package. +// These builders have dependencies on Grafana apps (dashboard and user). +func All(sql db.DB, sprinkles DashboardStats) ([]resource.DocumentBuilderInfo, error) { + dashboards, err := DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) { + logger := log.New("dashboard_builder", "namespace", namespace) + dsinfo := []*dashboard.DatasourceQueryResult{{}} + ns, err := claims.ParseNamespace(namespace) + if err != nil && sql != nil { + rows, err := sql.GetSqlxSession().Query(ctx, "SELECT uid,type,name,is_default FROM data_source WHERE org_id=?", ns.OrgID) + if err != nil { + return nil, err + } + + defer func() { + _ = rows.Close() + }() + + for rows.Next() { + info := &dashboard.DatasourceQueryResult{} + err = rows.Scan(&info.UID, &info.Type, &info.Name, &info.IsDefault) + if err != nil { + return nil, err + } + dsinfo = append(dsinfo, info) + } + } + + var stats map[string]map[string]int64 + if sprinkles != nil { + stats, err = sprinkles.GetStats(ctx, namespace) + if err != nil { + logger.Warn("Failed to get sprinkles", "error", err) + } + } + + return &DashboardDocumentBuilder{ + Namespace: namespace, + Blob: blob, + Stats: stats, + DatasourceLookup: dashboard.CreateDatasourceLookup(dsinfo), + }, nil + }) + + if err != nil { + return nil, err + } + + users, err := GetUserBuilder() + if err != nil { + return nil, err + } + + return []resource.DocumentBuilderInfo{dashboards, users}, nil +} diff --git a/pkg/storage/unified/search/document_test.go b/pkg/storage/unified/search/builders/document_test.go similarity index 79% rename from pkg/storage/unified/search/document_test.go rename to pkg/storage/unified/search/builders/document_test.go index 285f3178120..59c3302c105 100644 --- a/pkg/storage/unified/search/document_test.go +++ b/pkg/storage/unified/search/builders/document_test.go @@ -1,4 +1,4 @@ -package search_test +package builders import ( "context" @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/services/store/kind/dashboard" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - "github.com/grafana/grafana/pkg/storage/unified/search" ) func doSnapshotTests(t *testing.T, builder resource.DocumentBuilder, kind string, key *resourcepb.ResourceKey, names []string) { @@ -36,7 +35,6 @@ func doSnapshotTests(t *testing.T, builder resource.DocumentBuilder, kind string outpath := filepath.Join("testdata", "doc", prefix+"-out.json") - // test path // nolint:gosec expect, _ := os.ReadFile(outpath) if !assert.JSONEq(t, string(expect), string(out)) { @@ -47,6 +45,19 @@ func doSnapshotTests(t *testing.T, builder resource.DocumentBuilder, kind string } } +func TestUserDocumentBuilder(t *testing.T) { + info, err := GetUserBuilder() + require.NoError(t, err) + doSnapshotTests(t, info.Builder, "user", &resourcepb.ResourceKey{ + Namespace: "default", + Group: "iam.grafana.app", + Resource: "users", + }, []string{ + "user-with-login-and-email", + "user-with-login-only", + }) +} + func TestDashboardDocumentBuilder(t *testing.T) { key := &resourcepb.ResourceKey{ Namespace: "default", @@ -54,18 +65,18 @@ func TestDashboardDocumentBuilder(t *testing.T) { Resource: "dashboards", } - info, err := search.DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) { - return &search.DashboardDocumentBuilder{ + info, err := DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) { + return &DashboardDocumentBuilder{ Namespace: namespace, Blob: blob, Stats: map[string]map[string]int64{ "aaa": { - search.DASHBOARD_ERRORS_LAST_1_DAYS: 1, - search.DASHBOARD_ERRORS_LAST_7_DAYS: 1, + DASHBOARD_ERRORS_LAST_1_DAYS: 1, + DASHBOARD_ERRORS_LAST_7_DAYS: 1, }, }, DatasourceLookup: dashboard.CreateDatasourceLookup([]*dashboard.DatasourceQueryResult{{ - Name: "TheDisplayName", // used to be the unique ID! + Name: "TheDisplayName", Type: "my-custom-plugin", UID: "DSUID", }}), @@ -76,12 +87,10 @@ func TestDashboardDocumentBuilder(t *testing.T) { builder, err := info.Namespaced(context.Background(), key.Namespace, nil) require.NoError(t, err) - // Dashboards (custom) doSnapshotTests(t, builder, "dashboard", key, []string{ "aaa", }) - // Standard builder = resource.StandardDocumentBuilder() doSnapshotTests(t, builder, "folder", &resourcepb.ResourceKey{ Namespace: "default", diff --git a/pkg/storage/unified/search/testdata/doc/dashboard-aaa-out.json b/pkg/storage/unified/search/builders/testdata/doc/dashboard-aaa-out.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/dashboard-aaa-out.json rename to pkg/storage/unified/search/builders/testdata/doc/dashboard-aaa-out.json diff --git a/pkg/storage/unified/search/testdata/doc/dashboard-aaa.json b/pkg/storage/unified/search/builders/testdata/doc/dashboard-aaa.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/dashboard-aaa.json rename to pkg/storage/unified/search/builders/testdata/doc/dashboard-aaa.json diff --git a/pkg/storage/unified/search/testdata/doc/folder-aaa-out.json b/pkg/storage/unified/search/builders/testdata/doc/folder-aaa-out.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/folder-aaa-out.json rename to pkg/storage/unified/search/builders/testdata/doc/folder-aaa-out.json diff --git a/pkg/storage/unified/search/testdata/doc/folder-aaa.json b/pkg/storage/unified/search/builders/testdata/doc/folder-aaa.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/folder-aaa.json rename to pkg/storage/unified/search/builders/testdata/doc/folder-aaa.json diff --git a/pkg/storage/unified/search/testdata/doc/folder-bbb-out.json b/pkg/storage/unified/search/builders/testdata/doc/folder-bbb-out.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/folder-bbb-out.json rename to pkg/storage/unified/search/builders/testdata/doc/folder-bbb-out.json diff --git a/pkg/storage/unified/search/testdata/doc/folder-bbb.json b/pkg/storage/unified/search/builders/testdata/doc/folder-bbb.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/folder-bbb.json rename to pkg/storage/unified/search/builders/testdata/doc/folder-bbb.json diff --git a/pkg/storage/unified/search/testdata/doc/playlist-aaa-out.json b/pkg/storage/unified/search/builders/testdata/doc/playlist-aaa-out.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/playlist-aaa-out.json rename to pkg/storage/unified/search/builders/testdata/doc/playlist-aaa-out.json diff --git a/pkg/storage/unified/search/testdata/doc/playlist-aaa.json b/pkg/storage/unified/search/builders/testdata/doc/playlist-aaa.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/playlist-aaa.json rename to pkg/storage/unified/search/builders/testdata/doc/playlist-aaa.json diff --git a/pkg/storage/unified/search/testdata/doc/report-aaa-out.json b/pkg/storage/unified/search/builders/testdata/doc/report-aaa-out.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/report-aaa-out.json rename to pkg/storage/unified/search/builders/testdata/doc/report-aaa-out.json diff --git a/pkg/storage/unified/search/testdata/doc/report-aaa.json b/pkg/storage/unified/search/builders/testdata/doc/report-aaa.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/report-aaa.json rename to pkg/storage/unified/search/builders/testdata/doc/report-aaa.json diff --git a/pkg/storage/unified/search/testdata/doc/user-example-out.json b/pkg/storage/unified/search/builders/testdata/doc/user-example-out.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/user-example-out.json rename to pkg/storage/unified/search/builders/testdata/doc/user-example-out.json diff --git a/pkg/storage/unified/search/testdata/doc/user-example.json b/pkg/storage/unified/search/builders/testdata/doc/user-example.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/user-example.json rename to pkg/storage/unified/search/builders/testdata/doc/user-example.json diff --git a/pkg/storage/unified/search/testdata/doc/user-user-with-login-and-email-out.json b/pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-and-email-out.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/user-user-with-login-and-email-out.json rename to pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-and-email-out.json diff --git a/pkg/storage/unified/search/testdata/doc/user-user-with-login-and-email.json b/pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-and-email.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/user-user-with-login-and-email.json rename to pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-and-email.json diff --git a/pkg/storage/unified/search/testdata/doc/user-user-with-login-only-out.json b/pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-only-out.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/user-user-with-login-only-out.json rename to pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-only-out.json diff --git a/pkg/storage/unified/search/testdata/doc/user-user-with-login-only.json b/pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-only.json similarity index 100% rename from pkg/storage/unified/search/testdata/doc/user-user-with-login-only.json rename to pkg/storage/unified/search/builders/testdata/doc/user-user-with-login-only.json diff --git a/pkg/storage/unified/search/user.go b/pkg/storage/unified/search/builders/user.go similarity index 96% rename from pkg/storage/unified/search/user.go rename to pkg/storage/unified/search/builders/user.go index 6a09dade5f7..fdf1fce25e1 100644 --- a/pkg/storage/unified/search/user.go +++ b/pkg/storage/unified/search/builders/user.go @@ -1,4 +1,4 @@ -package search +package builders import ( "bytes" @@ -54,7 +54,6 @@ var _ resource.DocumentBuilder = new(userDocumentBuilder) type userDocumentBuilder struct{} -// BuildDocument implements resource.DocumentBuilder. func (u *userDocumentBuilder) BuildDocument(ctx context.Context, key *resourcepb.ResourceKey, rv int64, value []byte) (*resource.IndexableDocument, error) { user := &iamv0.User{} err := json.NewDecoder(bytes.NewReader(value)).Decode(user) diff --git a/pkg/storage/unified/search/document.go b/pkg/storage/unified/search/document.go index eb0059c010d..fff4c31ead9 100644 --- a/pkg/storage/unified/search/document.go +++ b/pkg/storage/unified/search/document.go @@ -1,87 +1,32 @@ package search import ( - "context" - - claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/store/kind/dashboard" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/search/builders" ) -// The default list of open source document builders +// StandardDocumentBuilders provides the default list of document builders for open source Grafana. +// It combines the standard document builder with external builders for dashboards and users. type StandardDocumentBuilders struct { sql db.DB - sprinkles DashboardStats + sprinkles builders.DashboardStats } -// Hooked up so wire can fill in different sprinkles -func ProvideDocumentBuilders(sql db.DB, sprinkles DashboardStats) resource.DocumentBuilderSupplier { +func ProvideDocumentBuilders(sql db.DB, sprinkles builders.DashboardStats) resource.DocumentBuilderSupplier { return &StandardDocumentBuilders{sql, sprinkles} } func (s *StandardDocumentBuilders) GetDocumentBuilders() ([]resource.DocumentBuilderInfo, error) { - dashboards, err := DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) { - logger := log.New("dashboard_builder", "namespace", namespace) - dsinfo := []*dashboard.DatasourceQueryResult{{}} - ns, err := claims.ParseNamespace(namespace) - if err != nil && s.sql != nil { - rows, err := s.sql.GetSqlxSession().Query(ctx, "SELECT uid,type,name,is_default FROM data_source WHERE org_id=?", ns.OrgID) - if err != nil { - return nil, err - } - - defer func() { - _ = rows.Close() - }() - - for rows.Next() { - info := &dashboard.DatasourceQueryResult{} - err = rows.Scan(&info.UID, &info.Type, &info.Name, &info.IsDefault) - if err != nil { - return nil, err - } - dsinfo = append(dsinfo, info) - } - } - - // Fetch dashboard sprinkles for the namespace - // This could take a while if namespace has a lot of dashboards - var stats map[string]map[string]int64 - if s.sprinkles != nil { - stats, err = s.sprinkles.GetStats(ctx, namespace) - if err != nil { - // only log a warning. Don't need to fail the indexer if we can't get sprinkles - logger.Warn("Failed to get sprinkles", "error", err) - } - } - - return &DashboardDocumentBuilder{ - Namespace: namespace, - Blob: blob, - Stats: stats, - DatasourceLookup: dashboard.CreateDatasourceLookup(dsinfo), - }, nil - }) - + all, err := builders.All(s.sql, s.sprinkles) if err != nil { return nil, err } - users, err := GetUserBuilder() - if err != nil { - return nil, err - } - - return []resource.DocumentBuilderInfo{ - // The default builder + result := []resource.DocumentBuilderInfo{ { Builder: resource.StandardDocumentBuilder(), }, - // Dashboard builder - dashboards, - // User builder - users, - }, err + } + return append(result, all...), nil } diff --git a/pkg/storage/unified/search/user_test.go b/pkg/storage/unified/search/user_test.go deleted file mode 100644 index 67b6777f895..00000000000 --- a/pkg/storage/unified/search/user_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package search_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/selection" - - iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - "github.com/grafana/grafana/pkg/storage/unified/search" -) - -func TestUserDocumentBuilder(t *testing.T) { - info, err := search.GetUserBuilder() - require.NoError(t, err) - doSnapshotTests(t, info.Builder, "user", &resourcepb.ResourceKey{ - Namespace: "default", - Group: "iam.grafana.app", - Resource: "users", - }, []string{ - "user-with-login-and-email", - "user-with-login-only", - }) -} - -func TestUserSearch(t *testing.T) { - key := resource.NamespacedResource{ - Namespace: "default", - Group: iamv0.UserResourceInfo.GroupResource().Group, - Resource: iamv0.UserResourceInfo.GroupResource().Resource, - } - - index := newTestUsersIndex(t, 100, 2, func(index resource.ResourceIndex) (int64, error) { - return 0, nil - }) - users := []iamv0.User{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "user1", - Namespace: "default", - }, - Spec: iamv0.UserSpec{ - Login: "user.one", - Email: "user.one@test.com", - Role: "Viewer", - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "user2", - Namespace: "default", - }, - Spec: iamv0.UserSpec{ - Login: "user.two", - Email: "user.two@test.com", - Role: "Viewer", - }, - }, - } - indexUserDocuments(t, index, key, users) - - // Sanity check - title search - checkUserSearchQuery(t, index, newTestsUserQueryWithTitle(key, "user2"), []string{"user2"}) - - t.Run("can search users by login", func(t *testing.T) { - // Search by login - checkUserSearchQuery(t, index, newTestUserQueryWithReqs(key, []*resourcepb.Requirement{ - { - Key: "fields.login", - Operator: string(selection.Equals), - Values: []string{"user.one"}, - }, - }), []string{"user1"}) - checkUserSearchQuery(t, index, newTestUserQueryWithReqs(key, []*resourcepb.Requirement{ - { - Key: "fields.login", - Operator: string(selection.Equals), - Values: []string{"user.two"}, - }, - }), []string{"user2"}) - }) - - t.Run("can search users by wildcard login", func(t *testing.T) { - checkUserSearchQuery(t, index, newTestUserQueryWithReqs(key, []*resourcepb.Requirement{ - { - Key: "fields.login", - Operator: string(selection.Equals), - Values: []string{"user.*"}, - }, - }), []string{"user1", "user2"}) - }) - - t.Run("can search users by email", func(t *testing.T) { - // Search by email - checkUserSearchQuery(t, index, newTestUserQueryWithReqs(key, []*resourcepb.Requirement{ - { - Key: "fields.email", - Operator: string(selection.Equals), - Values: []string{"user.one@test.com"}, - }, - }), []string{"user1"}) - - checkUserSearchQuery(t, index, newTestUserQueryWithReqs(key, []*resourcepb.Requirement{ - { - Key: "fields.email", - Operator: string(selection.Equals), - Values: []string{"user.two@test.com"}, - }, - }), []string{"user2"}) - }) -} - -func newTestUsersIndex(t testing.TB, threshold int64, size int64, writer resource.BuildFn) resource.ResourceIndex { - t.Helper() - gr := iamv0.UserResourceInfo.GroupResource() - key := &resourcepb.ResourceKey{ - Namespace: "default", - Group: gr.Group, - Resource: gr.Resource, - } - backend, err := search.NewBleveBackend(search.BleveOptions{ - Root: t.TempDir(), - FileThreshold: threshold, // use in-memory for tests - }, tracing.NewNoopTracerService(), nil) - require.NoError(t, err) - - t.Cleanup(backend.Stop) - - ctx := identity.WithRequester(context.Background(), &user.SignedInUser{Namespace: "ns"}) - - info, err := search.GetUserBuilder() - require.NoError(t, err) - - index, err := backend.BuildIndex(ctx, resource.NamespacedResource{ - Namespace: key.Namespace, - Group: key.Group, - Resource: key.Resource, - }, size, info.Fields, "test", writer, nil, false) - require.NoError(t, err) - - return index -} - -func indexUserDocuments(t *testing.T, index resource.ResourceIndex, key resource.NamespacedResource, users []iamv0.User) { - t.Helper() - items := make([]*resource.BulkIndexItem, 0, len(users)) - for _, user := range users { - items = append(items, &resource.BulkIndexItem{ - Action: resource.ActionIndex, - Doc: &resource.IndexableDocument{ - RV: 1, - Name: user.Name, - Key: &resourcepb.ResourceKey{ - Name: user.Name, - Namespace: key.Namespace, - Group: key.Group, - Resource: key.Resource, - }, - Title: user.Name, - Fields: map[string]any{search.USER_LOGIN: user.Spec.Login, search.USER_EMAIL: user.Spec.Email}, - }, - }) - } - req := &resource.BulkIndexRequest{Items: items} - require.NoError(t, index.BulkIndex(req)) -} - -func checkUserSearchQuery(t *testing.T, index resource.ResourceIndex, query *resourcepb.ResourceSearchRequest, orderedExpectedNames []string) { - t.Helper() - res, err := index.Search(context.Background(), nil, query, nil, nil) - require.NoError(t, err) - require.Equal(t, int64(len(orderedExpectedNames)), res.TotalHits) - names := make([]string, len(res.Results.Rows)) - for ix, row := range res.Results.Rows { - names[ix] = row.Key.Name - } - assert.ElementsMatch(t, orderedExpectedNames, names) -} - -func newTestUserQueryWithReqs(key resource.NamespacedResource, filterReqs []*resourcepb.Requirement) *resourcepb.ResourceSearchRequest { - return &resourcepb.ResourceSearchRequest{ - Options: &resourcepb.ListOptions{ - Key: &resourcepb.ResourceKey{ - Namespace: key.Namespace, - Group: key.Group, - Resource: key.Resource, - }, - Fields: filterReqs, - }, - Limit: 100, - } -} - -func newTestsUserQueryWithTitle(key resource.NamespacedResource, title string) *resourcepb.ResourceSearchRequest { - return &resourcepb.ResourceSearchRequest{ - Options: &resourcepb.ListOptions{ - Key: &resourcepb.ResourceKey{ - Namespace: key.Namespace, - Group: key.Group, - Resource: key.Resource, - }, - }, - Query: title, - Limit: 100, - } -} From d50e2a94a354e73d7c327ed89222676b24f6875d Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Tue, 25 Nov 2025 11:20:28 +0100 Subject: [PATCH 099/378] Alerting: Implement setting of version message in convert API. (#114331) Part 2 of adding version messages to the `alert_rule_version` able. This allows setting the message via a header when using the Prometheus conversion API, which can be useful for e.g. linking changes back to source control. --- .../loki/historian_store_test.go | 4 +- .../snapshot_mgmt_alerts_test.go | 2 +- .../ngalert/api/api_convert_prometheus.go | 21 +++- .../api/api_convert_prometheus_test.go | 62 ++++++++++++ pkg/services/ngalert/api/api_provisioning.go | 7 +- pkg/services/ngalert/api/api_ruler.go | 4 +- pkg/services/ngalert/api/persist.go | 2 +- pkg/services/ngalert/models/alert_rule.go | 6 ++ .../ngalert/provisioning/alert_rules.go | 31 +++--- .../ngalert/provisioning/alert_rules_test.go | 96 +++++++++---------- pkg/services/ngalert/provisioning/persist.go | 2 +- pkg/services/ngalert/store/alert_rule.go | 11 ++- pkg/services/ngalert/store/alert_rule_test.go | 49 ++++++---- pkg/services/ngalert/tests/fakes/rules.go | 4 +- pkg/services/ngalert/tests/util.go | 6 +- pkg/storage/unified/federated/stats_test.go | 6 +- 16 files changed, 212 insertions(+), 101 deletions(-) diff --git a/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go b/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go index 348aa38d477..66d447e8bf9 100644 --- a/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go +++ b/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go @@ -687,7 +687,7 @@ func createAlertRule(t *testing.T, sql *sqlstore.SQLStore, title string, generat rule.PanelID = nil ruleStore := store.SetupStoreForTesting(t, sql) - ids, err := ruleStore.InsertAlertRules(context.Background(), nil, []ngmodels.AlertRule{rule}) + ids, err := ruleStore.InsertAlertRules(context.Background(), nil, []ngmodels.InsertRule{{AlertRule: rule}}) require.NoError(t, err) result, err := ruleStore.GetAlertRuleByUID(context.Background(), &ngmodels.GetAlertRuleByUIDQuery{OrgID: rule.OrgID, UID: ids[0].UID}) require.NoError(t, err) @@ -719,7 +719,7 @@ func createAlertRuleFromDashboard(t *testing.T, sql *sqlstore.SQLStore, title st rule.PanelID = panelID } ruleStore := store.SetupStoreForTesting(t, sql) - ids, err := ruleStore.InsertAlertRules(context.Background(), nil, []ngmodels.AlertRule{rule}) + ids, err := ruleStore.InsertAlertRules(context.Background(), nil, []ngmodels.InsertRule{{AlertRule: rule}}) require.NoError(t, err) result, err := ruleStore.GetAlertRuleByUID(context.Background(), &ngmodels.GetAlertRuleByUIDQuery{OrgID: rule.OrgID, UID: ids[0].UID}) require.NoError(t, err) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go index ee2c9889c20..eaa95962a23 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -457,7 +457,7 @@ func createAlertRuleGroup(t *testing.T, ctx context.Context, service *Service, u Rules: rules, } - err := service.ngAlert.Api.AlertRules.ReplaceRuleGroup(ctx, user, group, "") + err := service.ngAlert.Api.AlertRules.ReplaceRuleGroup(ctx, user, group, "", "") require.NoError(t, err) return group diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index 7e999010795..588f6dee213 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -63,6 +63,9 @@ const ( // configIdentifierHeader is the header that specifies the identifier for imported Alertmanager config. configIdentifierHeader = "X-Grafana-Alerting-Config-Identifier" defaultConfigIdentifier = "default" + + // versionMessageHeader is the header that specifies an optional message for rule versions. + versionMessageHeader = "X-Grafana-Alerting-Version-Message" ) var ( @@ -407,6 +410,11 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context return errorToResponse(err) } + versionMessage, err := parseVersionMessageHeader(c) + if err != nil { + logger.Error("Failed to parse version message header", "error", err) + return errorToResponse(err) + } // 2. Convert Prometheus Rules to GMA grafanaGroups := make([]*models.AlertRuleGroup, 0, len(promNamespaces)) for ns, rgs := range promNamespaces { @@ -444,12 +452,13 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err) return errorToResponse(err) } + grafanaGroups = append(grafanaGroups, grafanaGroup) } } // 3. Update the GMA Rules in the DB - err = srv.alertRuleService.ReplaceRuleGroups(c.Req.Context(), c.SignedInUser, grafanaGroups, provenance) + err = srv.alertRuleService.ReplaceRuleGroups(c.Req.Context(), c.SignedInUser, grafanaGroups, provenance, versionMessage) if err != nil { logger.Error("Failed to replace rule groups", "error", err) return errorToResponse(err) @@ -856,6 +865,16 @@ func parseExtraLabelsHeader(c *contextmodel.ReqContext) (map[string]string, erro return parseKeyValuePairs(labelsStr, extraLabelsHeader) } +// parseVersionMessageHeader obtains and validates the message header value. +func parseVersionMessageHeader(c *contextmodel.ReqContext) (string, error) { + str := strings.TrimSpace(c.Req.Header.Get(versionMessageHeader)) + // Limit message to the same as the dashboards message. + if len(str) > 500 { + return "", errInvalidHeaderValue(versionMessageHeader, errors.New("must be less than 500 characters")) + } + return str, nil +} + func formatMergeMatchers(matchers amconfig.Matchers) string { var pairs []string for _, matcher := range matchers { diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index 9371ca6a230..08ce147a5c3 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -506,6 +506,68 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { } }) + t.Run("with version message header should pass message to rule store", func(t *testing.T) { + provenanceStore := fakes.NewFakeProvisioningStore() + folderService := foldertest.NewFakeService() + srv, _, ruleStore := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore), withFolderService(folderService)) + + // Create a folder in the root + fldr := randFolder() + fldr.ParentUID = "" + folderService.ExpectedFolder = fldr + folderService.ExpectedFolders = []*folder.Folder{fldr} + ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr) + + makeGroup := func(alertname string) apimodels.PrometheusRuleGroup { + return apimodels.PrometheusRuleGroup{ + Name: "Test Group", + Interval: prommodel.Duration(1 * time.Minute), + Rules: []apimodels.PrometheusRule{ + { + Alert: alertname, + Expr: "up == 0", + For: util.Pointer(prommodel.Duration(5 * time.Minute)), + Labels: map[string]string{ + "severity": "critical", + }, + }, + }, + } + } + + // Create a rule with the X-Grafana-Alerting-Version-Message to check it's passed to InsertRule. + + rc1 := createRequestCtx() + rc1.Req.Header.Set("X-Grafana-Alerting-Version-Message", "version #1") + response1 := srv.RouteConvertPrometheusPostRuleGroup(rc1, fldr.Title, makeGroup("AlertV1")) + require.Equal(t, http.StatusAccepted, response1.Status()) + + inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) { + a, ok := cmd.([]models.InsertRule) + return a, ok + }) + require.Len(t, inserts, 1) + cmd1 := inserts[0].([]models.InsertRule) + require.Len(t, cmd1, 1) + require.Equal(t, "version #1", cmd1[0].Message) + + // Now update the rule to check it gets passed to UpdateRule. + + rc2 := createRequestCtx() + rc2.Req.Header.Set("X-Grafana-Alerting-Version-Message", "version #2") + response2 := srv.RouteConvertPrometheusPostRuleGroup(rc2, fldr.Title, makeGroup("AlertV2")) + require.Equal(t, http.StatusAccepted, response2.Status()) + + updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) { + a, ok := cmd.([]models.UpdateRule) + return a, ok + }) + require.Len(t, updates, 1) + cmd2 := updates[0].([]models.UpdateRule) + require.Len(t, cmd2, 1) + require.Equal(t, "version #2", cmd2[0].Message) + }) + t.Run("returns error when target datasource does not exist", func(t *testing.T) { srv, _, _ := createConvertPrometheusSrv(t) rc := createRequestCtx() diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index dc8863e1fb6..8d24fafa351 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -75,7 +75,7 @@ type AlertRuleService interface { UpdateAlertRule(ctx context.Context, user identity.Requester, rule alerting_models.AlertRule, provenance alerting_models.Provenance) (alerting_models.AlertRule, error) DeleteAlertRule(ctx context.Context, user identity.Requester, ruleUID string, provenance alerting_models.Provenance) error GetRuleGroup(ctx context.Context, user identity.Requester, folder, group string) (alerting_models.AlertRuleGroup, error) - ReplaceRuleGroup(ctx context.Context, user identity.Requester, group alerting_models.AlertRuleGroup, provenance alerting_models.Provenance) error + ReplaceRuleGroup(ctx context.Context, user identity.Requester, group alerting_models.AlertRuleGroup, provenance alerting_models.Provenance, message string) error DeleteRuleGroup(ctx context.Context, user identity.Requester, folder, group string, provenance alerting_models.Provenance) error DeleteRuleGroups(ctx context.Context, user identity.Requester, provenance alerting_models.Provenance, opts *provisioning.FilterOptions) error GetAlertRuleWithFolderFullpath(ctx context.Context, u identity.Requester, ruleUID string) (provisioning.AlertRuleWithFolderFullpath, error) @@ -492,7 +492,10 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, a ErrResp(http.StatusBadRequest, err, "") } provenance := determineProvenance(c) - err = srv.alertRules.ReplaceRuleGroup(c.Req.Context(), c.SignedInUser, groupModel, alerting_models.Provenance(provenance)) + // TODO: https://github.com/grafana/grafana/issues/114197 + // Support passing change messages. + changeMessage := "" + err = srv.alertRules.ReplaceRuleGroup(c.Req.Context(), c.SignedInUser, groupModel, alerting_models.Provenance(provenance), changeMessage) if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) { return ErrResp(http.StatusBadRequest, err, "") } diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 268ed7b8c5d..d81f448f600 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -576,9 +576,9 @@ func (srv RulerSrv) performUpdateAlertRules(ctx context.Context, c *contextmodel } if len(finalChanges.New) > 0 { - inserts := make([]ngmodels.AlertRule, 0, len(finalChanges.New)) + inserts := make([]ngmodels.InsertRule, 0, len(finalChanges.New)) for _, rule := range finalChanges.New { - inserts = append(inserts, *rule) + inserts = append(inserts, ngmodels.InsertRule{AlertRule: *rule}) } added, err := srv.store.InsertAlertRules(tranCtx, ngmodels.NewUserUID(c.SignedInUser), inserts) if err != nil { diff --git a/pkg/services/ngalert/api/persist.go b/pkg/services/ngalert/api/persist.go index 9e7df81f48e..0123af54d52 100644 --- a/pkg/services/ngalert/api/persist.go +++ b/pkg/services/ngalert/api/persist.go @@ -28,7 +28,7 @@ type RuleStore interface { // InsertAlertRules will insert all alert rules passed into the function // and return the map of uuid to id. - InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.AlertRule) ([]ngmodels.AlertRuleKeyWithId, error) + InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.InsertRule) ([]ngmodels.AlertRuleKeyWithId, error) UpdateAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.UpdateRule) error DeleteAlertRulesByUID(ctx context.Context, orgID int64, user *ngmodels.UserUID, permanently bool, ruleUID ...string) error DeleteRuleFromTrashByGUID(ctx context.Context, orgID int64, ruleGUID string) (int64, error) diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index a4e1e8fcc04..e6177843e8a 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -1036,9 +1036,15 @@ type ListNamespaceAlertRulesQuery struct { NamespaceUID string } +type InsertRule struct { + AlertRule + Message string +} + type UpdateRule struct { Existing *AlertRule New AlertRule + Message string } // Condition contains backend expressions and queries and the RefID diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index d8cb5c9783d..aa6c4c55b34 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -304,8 +304,10 @@ func (service *AlertRuleService) CreateAlertRule(ctx context.Context, user ident } } err = service.xact.InTransaction(ctx, func(ctx context.Context) error { - ids, err := service.ruleStore.InsertAlertRules(ctx, userUidOrFallback(user), []models.AlertRule{ - rule, + ids, err := service.ruleStore.InsertAlertRules(ctx, userUidOrFallback(user), []models.InsertRule{ + { + AlertRule: rule, + }, }) if err != nil { return err @@ -464,7 +466,7 @@ func (service *AlertRuleService) UpdateRuleGroup(ctx context.Context, user ident }) } -func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, user identity.Requester, group models.AlertRuleGroup, provenance models.Provenance) error { +func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, user identity.Requester, group models.AlertRuleGroup, provenance models.Provenance, versionMessage string) error { if err := models.ValidateRuleGroupInterval(group.Interval, service.baseIntervalSeconds); err != nil { return err } @@ -518,13 +520,13 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, user iden } } - return service.persistDelta(ctx, user, delta, provenance) + return service.persistDelta(ctx, user, delta, provenance, versionMessage) } -func (service *AlertRuleService) ReplaceRuleGroups(ctx context.Context, user identity.Requester, groups []*models.AlertRuleGroup, provenance models.Provenance) error { +func (service *AlertRuleService) ReplaceRuleGroups(ctx context.Context, user identity.Requester, groups []*models.AlertRuleGroup, provenance models.Provenance, versionMessage string) error { err := service.xact.InTransaction(ctx, func(ctx context.Context) error { for _, group := range groups { - err := service.ReplaceRuleGroup(ctx, user, *group, provenance) + err := service.ReplaceRuleGroup(ctx, user, *group, provenance, versionMessage) if err != nil { return err } @@ -565,7 +567,7 @@ func (service *AlertRuleService) DeleteRuleGroups(ctx context.Context, user iden return err } } - err = service.persistDelta(ctx, user, delta, provenance) + err = service.persistDelta(ctx, user, delta, provenance, "") // Message not used for deletes. if err != nil { return err } @@ -621,7 +623,7 @@ func (service *AlertRuleService) calcDelta(ctx context.Context, user identity.Re return store.UpdateCalculatedRuleFields(delta), nil } -func (service *AlertRuleService) persistDelta(ctx context.Context, user identity.Requester, delta *store.GroupDelta, provenance models.Provenance) error { +func (service *AlertRuleService) persistDelta(ctx context.Context, user identity.Requester, delta *store.GroupDelta, provenance models.Provenance, versionMessage string) error { return service.xact.InTransaction(ctx, func(ctx context.Context) error { // Delete first as this could prevent future unique constraint violations. if len(delta.Delete) > 0 { @@ -669,6 +671,7 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity updates = append(updates, models.UpdateRule{ Existing: update.Existing, New: *update.New, + Message: versionMessage, }) } if err := service.ruleStore.UpdateAlertRules(ctx, userUidOrFallback(user), updates); err != nil { @@ -682,7 +685,7 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity } if len(delta.New) > 0 { - uids, err := service.ruleStore.InsertAlertRules(ctx, userUidOrFallback(user), withoutNilAlertRules(delta.New)) + uids, err := service.ruleStore.InsertAlertRules(ctx, userUidOrFallback(user), makeInsertRules(delta.New, versionMessage)) if err != nil { return fmt.Errorf("failed to insert alert rules: %w", err) } @@ -994,11 +997,15 @@ func syncGroupRuleFields(group *models.AlertRuleGroup, orgID int64) *models.Aler return group } -func withoutNilAlertRules(ptrs []*models.AlertRule) []models.AlertRule { - result := make([]models.AlertRule, 0, len(ptrs)) +// makeInsertRules converts a slice of AlertRule pointers to InsertRule, removing nils and adding version message. +func makeInsertRules(ptrs []*models.AlertRule, versionMessage string) []models.InsertRule { + result := make([]models.InsertRule, 0, len(ptrs)) for _, ptr := range ptrs { if ptr != nil { - result = append(result, *ptr) + result = append(result, models.InsertRule{ + AlertRule: *ptr, + Message: versionMessage, + }) } } return result diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index 145f1df2bb8..c552f797429 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -49,7 +49,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { t.Run("group creation should set the right provenance", func(t *testing.T) { group := createDummyGroup("group-test-1", orgID) - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-1") @@ -96,7 +96,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { group := createDummyGroup("namespace-test", orgID) group.Rules[0].NamespaceUID = "" - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "namespace-test") @@ -109,7 +109,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { group := createDummyGroup("group-test-3", orgID) group.Rules[0].RuleGroup = "something different" - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-3") @@ -169,13 +169,13 @@ func TestIntegrationAlertRuleService(t *testing.T) { t.Run("updating a group by updating a rule should bump that rule's data and version number", func(t *testing.T) { group := createDummyGroup("group-test-5", orgID) - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-5") require.NoError(t, err) updatedGroup.Rules[0].Title = "some-other-title-asdf" - err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI) + err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-5") @@ -198,7 +198,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { }, } rule.Metadata = ruleMetadata - r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.AlertRule{rule}) + r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}}) require.NoError(t, err) require.Len(t, r, 1) @@ -215,7 +215,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { Rules: []models.AlertRule{rule}, } - err = ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err = ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, namespaceUID, groupTitle) @@ -242,7 +242,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { }, } rule.Metadata = ruleMetadata - r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.AlertRule{rule}) + r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}}) require.NoError(t, err) require.Len(t, r, 1) @@ -263,7 +263,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { Rules: []models.AlertRule{rule}, } - err = ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err = ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, namespaceUID, groupTitle) @@ -289,7 +289,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { }, } rule.Metadata = ruleMetadata - r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.AlertRule{rule}) + r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}}) require.NoError(t, err) require.Len(t, r, 1) @@ -306,7 +306,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { Rules: []models.AlertRule{rule}, } - err = ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err = ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, namespaceUID, groupTitle) @@ -326,7 +326,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { }, } rule.Metadata = ruleMetadata - r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.AlertRule{rule}) + r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}}) require.NoError(t, err) require.Len(t, r, 1) @@ -356,14 +356,14 @@ func TestIntegrationAlertRuleService(t *testing.T) { dummyRule("overlap-test-rule-2", orgID), }, } - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "overlap-test") require.NoError(t, err) updatedGroup.Rules[0].Title = "overlap-test-rule-2" updatedGroup.Rules[1].Title = "overlap-test-rule-3" - err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI) + err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "overlap-test") @@ -387,14 +387,14 @@ func TestIntegrationAlertRuleService(t *testing.T) { dummyRule("swap-test-rule-2", orgID), }, } - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "swap-test") require.NoError(t, err) updatedGroup.Rules[0].Title = "swap-test-rule-2" updatedGroup.Rules[1].Title = "swap-test-rule-1" - err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI) + err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "swap-test") @@ -419,7 +419,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { dummyRule("cycle-test-rule-3", orgID), }, } - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "cycle-test") require.NoError(t, err) @@ -427,7 +427,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { updatedGroup.Rules[0].Title = "cycle-test-rule-2" updatedGroup.Rules[1].Title = "cycle-test-rule-3" updatedGroup.Rules[2].Title = "cycle-test-rule-1" - err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI) + err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "cycle-test") @@ -457,7 +457,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { dummyRule("multi-cycle-test-rule-5", orgID), }, } - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "multi-cycle-test") require.NoError(t, err) @@ -469,7 +469,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { updatedGroup.Rules[3].Title = "multi-cycle-test-rule-5" updatedGroup.Rules[4].Title = "multi-cycle-test-rule-3" - err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI) + err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "multi-cycle-test") @@ -498,7 +498,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { dummyRule("recreate-test-rule-1", orgID), }, } - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) updatedGroup := models.AlertRuleGroup{ Title: "recreate-test", @@ -508,7 +508,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { dummyRule("recreate-test-rule-1", orgID), }, } - err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI) + err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "recreate-test") @@ -529,14 +529,14 @@ func TestIntegrationAlertRuleService(t *testing.T) { dummyRule("create-overlap-test-rule-1", orgID), }, } - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "create-overlap-test") require.NoError(t, err) updatedGroup.Rules[0].Title = "create-overlap-test-rule-2" updatedGroup.Rules = append(updatedGroup.Rules, dummyRule("create-overlap-test-rule-1", orgID)) - err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI) + err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "") require.NoError(t, err) readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "create-overlap-test") @@ -558,7 +558,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { models.PanelIDAnnotation: strconv.FormatInt(panelId, 10), } - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-5") require.NoError(t, err) @@ -713,11 +713,11 @@ func TestIntegrationAlertRuleService(t *testing.T) { t.Run(test.name, func(t *testing.T) { var orgID int64 = 1 group := createDummyGroup(t.Name(), orgID) - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, test.from) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, test.from, "") require.NoError(t, err) group.Rules[0].Title = t.Name() - err = ruleService.ReplaceRuleGroup(context.Background(), u, group, test.to) + err = ruleService.ReplaceRuleGroup(context.Background(), u, group, test.to, "") if test.errNil { require.NoError(t, err) } else { @@ -745,7 +745,7 @@ func TestIntegrationAlertRuleService(t *testing.T) { ruleService.quotas = checker group := createDummyGroup("quota-reached", orgID) - err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.ErrorIs(t, err, models.ErrQuotaReached) }) @@ -912,11 +912,11 @@ func TestIntegrationCreateAlertRule(t *testing.T) { t.Run("inserts to database", func(t *testing.T) { inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) { - a, ok := cmd.([]models.AlertRule) + a, ok := cmd.([]models.InsertRule) return a, ok }) require.Len(t, inserts, 1) - cmd := inserts[0].([]models.AlertRule) + cmd := inserts[0].([]models.InsertRule) require.Len(t, cmd, 1) }) @@ -946,11 +946,11 @@ func TestIntegrationCreateAlertRule(t *testing.T) { t.Run("inserts to database", func(t *testing.T) { inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) { - a, ok := cmd.([]models.AlertRule) + a, ok := cmd.([]models.InsertRule) return a, ok }) require.Len(t, inserts, 1) - cmd := inserts[0].([]models.AlertRule) + cmd := inserts[0].([]models.InsertRule) require.Len(t, cmd, 1) }) @@ -993,11 +993,11 @@ func TestIntegrationCreateAlertRule(t *testing.T) { t.Run("inserts to database", func(t *testing.T) { inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) { - a, ok := cmd.([]models.AlertRule) + a, ok := cmd.([]models.InsertRule) return a, ok }) require.Len(t, inserts, 1) - cmd := inserts[0].([]models.AlertRule) + cmd := inserts[0].([]models.InsertRule) require.Len(t, cmd, 1) }) @@ -1040,11 +1040,11 @@ func TestIntegrationCreateAlertRule(t *testing.T) { t.Run("inserts to database", func(t *testing.T) { inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) { - a, ok := cmd.([]models.AlertRule) + a, ok := cmd.([]models.InsertRule) return a, ok }) require.Len(t, inserts, 1) - cmd := inserts[0].([]models.AlertRule) + cmd := inserts[0].([]models.InsertRule) require.Len(t, cmd, 1) }) @@ -1075,7 +1075,7 @@ func TestIntegrationCreateAlertRule(t *testing.T) { assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method) inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) { - a, ok := cmd.([]models.AlertRule) + a, ok := cmd.([]models.InsertRule) return a, ok }) require.Empty(t, inserts) @@ -1276,7 +1276,7 @@ func TestUpdateAlertRule(t *testing.T) { rule := models.CopyRule(rules[0]) - _, err := service.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.AlertRule{*rule}) + _, err := service.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: *rule}}) require.NoError(t, err) ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { @@ -1300,7 +1300,7 @@ func TestUpdateAlertRule(t *testing.T) { ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return true, nil } rule := createNoGroupRule("nogroup-update", orgID, "my-namespace") - _, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.AlertRule{rule}) + _, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}}) require.NoError(t, err) require.NoError(t, provenanceStore.SetProvenance(context.Background(), &rule, orgID, models.ProvenanceNone)) @@ -1418,7 +1418,7 @@ func TestDeleteAlertRule(t *testing.T) { // create two NoGroup rules in the same namespace (distinct sentinel groups) r1 := createNoGroupRule("nogroup-del-1", orgID, "my-namespace") r2 := createNoGroupRule("nogroup-del-2", orgID, "my-namespace") - _, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.AlertRule{r1, r2}) + _, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: r1}, {AlertRule: r2}}) require.NoError(t, err) require.NoError(t, provenanceStore.SetProvenance(context.Background(), &r1, orgID, models.ProvenanceNone)) require.NoError(t, provenanceStore.SetProvenance(context.Background(), &r2, orgID, models.ProvenanceNone)) @@ -1444,7 +1444,7 @@ func TestDeleteAlertRule(t *testing.T) { ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return false, nil } r := createNoGroupRule("nogroup-del-auth", orgID, "my-namespace") - _, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.AlertRule{r}) + _, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: r}}) require.NoError(t, err) require.NoError(t, provenanceStore.SetProvenance(context.Background(), &r, orgID, models.ProvenanceNone)) @@ -1917,7 +1917,7 @@ func TestReplaceGroup(t *testing.T) { return true, nil } - err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) require.Len(t, ac.Calls, 1) @@ -1953,7 +1953,7 @@ func TestReplaceGroup(t *testing.T) { return expectedErr } - err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.ErrorIs(t, err, expectedErr) require.Len(t, ac.Calls, 2) @@ -1976,7 +1976,7 @@ func TestReplaceGroup(t *testing.T) { return nil } - err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI) + err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "") require.NoError(t, err) require.Len(t, ac.Calls, 2) @@ -2013,11 +2013,11 @@ func TestReplaceGroup(t *testing.T) { Rules: []models.AlertRule{rule}, } - err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone) + err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone, "") require.NoError(t, err) rule.Metadata.PrometheusStyleRule.OriginalRuleDefinition = "new" - err = service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone) + err = service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone, "") require.NoError(t, err) rule, _, err = service.GetAlertRule(context.Background(), u, rule.UID) @@ -2036,7 +2036,7 @@ func TestReplaceGroup(t *testing.T) { second.RuleGroup = group.Title group.Rules = append(group.Rules, second) - err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone) + err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone, "") require.Error(t, err) require.ErrorContains(t, err, "cannot be used for rule groups with multiple rules") }) @@ -2050,7 +2050,7 @@ func TestReplaceGroup(t *testing.T) { // change the group name away from the sentinel value groupSeed.Title = "some-other-group" // not the sentinel group name - err := service.ReplaceRuleGroup(context.Background(), u, groupSeed, models.ProvenanceNone) + err := service.ReplaceRuleGroup(context.Background(), u, groupSeed, models.ProvenanceNone, "") require.Error(t, err) require.ErrorContains(t, err, "cannot move rule out of this group") }) diff --git a/pkg/services/ngalert/provisioning/persist.go b/pkg/services/ngalert/provisioning/persist.go index 753c874cce3..914a3644984 100644 --- a/pkg/services/ngalert/provisioning/persist.go +++ b/pkg/services/ngalert/provisioning/persist.go @@ -34,7 +34,7 @@ type RuleStore interface { ListAlertRules(ctx context.Context, query *models.ListAlertRulesQuery) (models.RulesGroup, error) ListAlertRulesPaginated(ctx context.Context, query *models.ListAlertRulesExtendedQuery) (models.RulesGroup, string, error) GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error) - InsertAlertRules(ctx context.Context, user *models.UserUID, rule []models.AlertRule) ([]models.AlertRuleKeyWithId, error) + InsertAlertRules(ctx context.Context, user *models.UserUID, rule []models.InsertRule) ([]models.AlertRuleKeyWithId, error) UpdateAlertRules(ctx context.Context, user *models.UserUID, rule []models.UpdateRule) error DeleteAlertRulesByUID(ctx context.Context, orgID int64, user *models.UserUID, permanently bool, ruleUID ...string) error GetAlertRulesGroupByRuleUID(ctx context.Context, query *models.GetAlertRulesGroupByRuleUIDQuery) ([]*models.AlertRule, error) diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 076974ea9c9..3feefac6d73 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -336,7 +336,7 @@ func (st DBstore) GetAlertRulesGroupByRuleUID(ctx context.Context, query *ngmode // InsertAlertRules is a handler for creating/updating alert rules. // Returns the UID and ID of rules that were created in the same order as the input rules. -func (st DBstore) InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.AlertRule) ([]ngmodels.AlertRuleKeyWithId, error) { +func (st DBstore) InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.InsertRule) ([]ngmodels.AlertRuleKeyWithId, error) { ids := make([]ngmodels.AlertRuleKeyWithId, 0, len(rules)) keys := make([]ngmodels.AlertRuleKey, 0, len(rules)) return ids, st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { @@ -352,14 +352,14 @@ func (st DBstore) InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, r.UID = uid } r.Version = 1 - if err := st.validateAlertRule(r); err != nil { + if err := st.validateAlertRule(r.AlertRule); err != nil { return err } if err := (&r).PreSave(TimeNow, user); err != nil { return err } - converted, err := alertRuleFromModelsAlertRule(r) + converted, err := alertRuleFromModelsAlertRule(r.AlertRule) if err != nil { return fmt.Errorf("failed to convert alert rule %q to storage model: %w", r.Title, err) } @@ -369,7 +369,9 @@ func (st DBstore) InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, converted.GUID = uuid.NewString() newRules = append(newRules, converted) - ruleVersions = append(ruleVersions, alertRuleToAlertRuleVersion(converted)) + v := alertRuleToAlertRuleVersion(converted) + v.Message = r.Message + ruleVersions = append(ruleVersions, v) } if len(newRules) > 0 { // we have to insert the rules one by one as otherwise we are @@ -444,6 +446,7 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, user *ngmodels.UserUID, v := alertRuleToAlertRuleVersion(converted) v.Version++ v.ParentVersion = r.Existing.Version + v.Message = r.Message // check if there is diff between existing and new, and if no, skip saving version. existingConverted, err := alertRuleFromModelsAlertRule(*r.Existing) diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index b0b7592c2d1..b9a574d33fc 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -38,6 +38,17 @@ import ( tutil "github.com/grafana/grafana/pkg/util/testutil" ) +func toInsertRules(rules []models.AlertRule) []models.InsertRule { + result := make([]models.InsertRule, len(rules)) + for i := range rules { + result[i] = models.InsertRule{ + AlertRule: rules[i], + Message: "", + } + } + return result +} + func TestIntegrationUpdateAlertRules(t *testing.T) { tutil.SkipIntegrationTestInShortMode(t) @@ -567,7 +578,7 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) { store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) store.FeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagAlertRuleRestore) - result, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, gen.GenerateMany(3)) + result, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, toInsertRules(gen.GenerateMany(3))) uids := make([]string, 0, len(result)) for _, rule := range result { uids = append(uids, rule.UID) @@ -634,7 +645,7 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) { store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) store.FeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagAlertRuleRestore) - result, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, gen.GenerateMany(3)) + result, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, toInsertRules(gen.GenerateMany(3))) uids := make([]string, 0, len(result)) for _, rule := range result { uids = append(uids, rule.UID) @@ -687,7 +698,7 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) { store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) store.FeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagAlertRuleRestore) - result, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, gen.GenerateMany(3)) + result, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, toInsertRules(gen.GenerateMany(3))) uids := make([]string, 0, len(result)) for _, rule := range result { uids = append(uids, rule.UID) @@ -754,7 +765,7 @@ func TestIntegrationInsertAlertRules(t *testing.T) { rules := append(gen.GenerateMany(5), recordingRulesGen.GenerateMany(5)...) - ids, err := store.InsertAlertRules(context.Background(), &usr, rules) + ids, err := store.InsertAlertRules(context.Background(), &usr, toInsertRules(rules)) require.NoError(t, err) require.Len(t, ids, len(rules)) @@ -819,7 +830,7 @@ func TestIntegrationInsertAlertRules(t *testing.T) { invalidMetric := "my_metric\x80" invalidRule := recordingRulesGen.Generate() invalidRule.Record.Metric = invalidMetric - _, err := store.InsertAlertRules(context.Background(), &usr, []models.AlertRule{invalidRule}) + _, err := store.InsertAlertRules(context.Background(), &usr, []models.InsertRule{{AlertRule: invalidRule}}) require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation) require.ErrorContains(t, err, "metric name for recording rule must be a valid utf8 string") }) @@ -827,7 +838,7 @@ func TestIntegrationInsertAlertRules(t *testing.T) { t.Run("clears fields that should not exist on recording rules", func(t *testing.T) { rule := recordingRulesGen.Generate() - rules, err := store.InsertAlertRules(context.Background(), &usr, []models.AlertRule{rule}) + rules, err := store.InsertAlertRules(context.Background(), &usr, []models.InsertRule{{AlertRule: rule}}) require.NoError(t, err) require.Len(t, rules, 1) ruleUID := rules[0].UID @@ -844,19 +855,19 @@ func TestIntegrationInsertAlertRules(t *testing.T) { }) t.Run("fail to insert rules with same ID", func(t *testing.T) { - _, err = store.InsertAlertRules(context.Background(), &usr, []models.AlertRule{rules[0]}) + _, err = store.InsertAlertRules(context.Background(), &usr, []models.InsertRule{{AlertRule: rules[0]}}) require.ErrorIs(t, err, models.ErrAlertRuleConflictBase) }) t.Run("should not fail insert rules with the same title in a folder", func(t *testing.T) { cp := models.CopyRule(&rules[0]) cp.UID = cp.UID + "-new" - _, err = store.InsertAlertRules(context.Background(), &usr, []models.AlertRule{*cp}) + _, err = store.InsertAlertRules(context.Background(), &usr, []models.InsertRule{{AlertRule: *cp}}) require.NoError(t, err) }) t.Run("should not let insert rules with the same UID", func(t *testing.T) { cp := models.CopyRule(&rules[0]) cp.Title = "unique-test-title" - _, err = store.InsertAlertRules(context.Background(), &usr, []models.AlertRule{*cp}) + _, err = store.InsertAlertRules(context.Background(), &usr, []models.InsertRule{{AlertRule: *cp}}) require.ErrorIs(t, err, models.ErrAlertRuleConflictBase) require.ErrorContains(t, err, fmt.Sprintf("Failed to save alert rule '%s' in organization %d due to conflict", cp.UID, cp.OrgID)) }) @@ -877,7 +888,7 @@ func TestIntegrationInsertAlertRules(t *testing.T) { return nil } - rules, err := store.InsertAlertRules(context.Background(), &usr, []models.AlertRule{rule}) + rules, err := store.InsertAlertRules(context.Background(), &usr, []models.InsertRule{{AlertRule: rule}}) require.NoError(t, err) require.Len(t, rules, 1) require.True(t, called) @@ -885,7 +896,7 @@ func TestIntegrationInsertAlertRules(t *testing.T) { t.Run("nil identity should be handled correctly", func(t *testing.T) { rule := gen.Generate() - ids, err = store.InsertAlertRules(context.Background(), nil, []models.AlertRule{rule}) + ids, err = store.InsertAlertRules(context.Background(), nil, []models.InsertRule{{AlertRule: rule}}) require.NoError(t, err) insertedRule, err := store.GetRuleByID(context.Background(), models.GetAlertRuleByIDQuery{ ID: ids[0].ID, @@ -942,7 +953,7 @@ func TestIntegrationAlertRulesNotificationSettings(t *testing.T) { require.NoError(t, store.SetProvenance(context.Background(), rule, rule.OrgID, p)) } - _, err := store.InsertAlertRules(context.Background(), &usr, deref) + _, err := store.InsertAlertRules(context.Background(), &usr, toInsertRules(deref)) require.NoError(t, err) t.Run("should find rules by receiver name", func(t *testing.T) { @@ -1220,7 +1231,7 @@ func TestIntegrationListNotificationSettings(t *testing.T) { orgRules := append(rulesWithNotificationsAndReceiver, rulesWithNotificationsAndTimeInterval...) - _, err := store.InsertAlertRules(context.Background(), &usr, deref) + _, err := store.InsertAlertRules(context.Background(), &usr, toInsertRules(deref)) require.NoError(t, err) result, err := store.ListNotificationSettings(context.Background(), models.ListNotificationSettingsQuery{OrgID: 1}) @@ -1319,7 +1330,7 @@ func TestIntegrationGetNamespacesByRuleUID(t *testing.T) { store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) rules := models.RuleGen.With(models.RuleMuts.WithOrgID(1), models.RuleMuts.WithRandomRecordingRules()).GenerateMany(5) - _, err := store.InsertAlertRules(context.Background(), &usr, rules) + _, err := store.InsertAlertRules(context.Background(), &usr, toInsertRules(rules)) require.NoError(t, err) uids := make([]string, 0, len(rules)) @@ -1381,7 +1392,7 @@ func TestIntegrationRuleGroupsCaseSensitive(t *testing.T) { group2 := gen.With(gen.WithGroupKey(groupKey2)).GenerateMany(1, 3) group3 := gen.With(gen.WithGroupKey(groupKey3)).GenerateMany(1, 3) - _, err := store.InsertAlertRules(context.Background(), &usr, append(append(append(misc, group1...), group2...), group3...)) + _, err := store.InsertAlertRules(context.Background(), &usr, toInsertRules(append(append(append(misc, group1...), group2...), group3...))) require.NoError(t, err) t.Run("GetAlertRulesGroupByRuleUID", func(t *testing.T) { @@ -1497,7 +1508,7 @@ func TestIntegrationListAlertRulesByGroupCaseSensitiveOrdering(t *testing.T) { // Insert all rules allRules := append(append(groupUpper, groupMixed...), groupLower...) - _, err := store.InsertAlertRules(context.Background(), &usr, allRules) + _, err := store.InsertAlertRules(context.Background(), &usr, toInsertRules(allRules)) require.NoError(t, err) t.Run("should order groups case-sensitively", func(t *testing.T) { @@ -1661,7 +1672,7 @@ func TestIntegrationGetRuleVersions(t *testing.T) { gen := models.RuleGen gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval), gen.WithOrgID(orgID), gen.WithVersion(1)) - inserted, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, []models.AlertRule{gen.Generate()}) + inserted, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, []models.InsertRule{{AlertRule: gen.Generate()}}) require.NoError(t, err) ruleV1, err := store.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{UID: inserted[0].UID}) require.NoError(t, err) @@ -2377,7 +2388,7 @@ func TestIntegration_ListDeletedRules(t *testing.T) { gen := models.RuleGen gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval), gen.WithOrgID(orgID)) - result, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, []models.AlertRule{gen.Generate(), gen.Generate()}) + result, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, []models.InsertRule{{AlertRule: gen.Generate()}, {AlertRule: gen.Generate()}}) require.NoError(t, err) rule1, err := store.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{UID: result[0].UID}) require.NoError(t, err) @@ -2465,7 +2476,7 @@ func TestIntegration_CleanUpDeletedAlertRules(t *testing.T) { gen = gen.With(gen.WithOrgID(orgID)) - result, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, gen.GenerateMany(3)) + result, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, toInsertRules(gen.GenerateMany(3))) uids := make([]string, 0, len(result)) for _, rule := range result { uids = append(uids, rule.UID) diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index 09abeaffba3..cfe790b9853 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -464,7 +464,7 @@ func (f *RuleStore) UpdateAlertRules(_ context.Context, _ *models.UserUID, q []m return nil } -func (f *RuleStore) InsertAlertRules(_ context.Context, _ *models.UserUID, q []models.AlertRule) ([]models.AlertRuleKeyWithId, error) { +func (f *RuleStore) InsertAlertRules(_ context.Context, _ *models.UserUID, q []models.InsertRule) ([]models.AlertRuleKeyWithId, error) { f.mtx.Lock() defer f.mtx.Unlock() f.RecordedOps = append(f.RecordedOps, q) @@ -475,7 +475,7 @@ func (f *RuleStore) InsertAlertRules(_ context.Context, _ *models.UserUID, q []m AlertRuleKey: rule.GetKey(), ID: rand.Int63(), }) - rulesPerOrg[rule.OrgID] = append(rulesPerOrg[rule.OrgID], rule) + rulesPerOrg[rule.OrgID] = append(rulesPerOrg[rule.OrgID], rule.AlertRule) } for orgID, rules := range rulesPerOrg { diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 60674e8153c..4a8434d5616 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -134,8 +134,8 @@ func CreateTestAlertRuleWithLabels(t testing.TB, ctx context.Context, dbstore *s } require.NoError(t, err) - _, err = dbstore.InsertAlertRules(ctx, models.NewUserUID(user), []models.AlertRule{ - { + _, err = dbstore.InsertAlertRules(ctx, models.NewUserUID(user), []models.InsertRule{{ + AlertRule: models.AlertRule{ ID: 0, OrgID: orgID, @@ -162,7 +162,7 @@ func CreateTestAlertRuleWithLabels(t testing.TB, ctx context.Context, dbstore *s RuleGroup: ruleGroup, NoDataState: models.NoData, ExecErrState: models.AlertingErrState, - }, + }}, }) require.NoError(t, err) diff --git a/pkg/storage/unified/federated/stats_test.go b/pkg/storage/unified/federated/stats_test.go index 89d50d1da87..04812084f65 100644 --- a/pkg/storage/unified/federated/stats_test.go +++ b/pkg/storage/unified/federated/stats_test.go @@ -79,8 +79,8 @@ func TestIntegrationDirectSQLStats(t *testing.T) { ruleStore := ngalertstore.SetupStoreForTesting(t, db) dashboardUID := "test" - _, err = ruleStore.InsertAlertRules(context.Background(), ngmodels.NewUserUID(tempUser), []ngmodels.AlertRule{ - { + _, err = ruleStore.InsertAlertRules(context.Background(), ngmodels.NewUserUID(tempUser), []ngmodels.InsertRule{{ + AlertRule: ngmodels.AlertRule{ DashboardUID: &dashboardUID, UID: "test", Title: "test", @@ -102,7 +102,7 @@ func TestIntegrationDirectSQLStats(t *testing.T) { ExecErrState: ngmodels.ExecutionErrorState(ngmodels.Alerting), NoDataState: ngmodels.Alerting, IntervalSeconds: 60, - }}) + }}}) require.NoError(t, err) _, err = dashStore.SaveDashboard(ctx, dashboards.SaveDashboardCommand{ From f7a19be381c3dab9ec832e74e5748fe6b9f30eba Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Tue, 25 Nov 2025 10:38:21 +0000 Subject: [PATCH 100/378] Folders: Revert changes to listFolder (#114388) --- .../browse-dashboards/api/services.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/public/app/features/browse-dashboards/api/services.ts b/public/app/features/browse-dashboards/api/services.ts index 2bb1ec6d39e..98be615583b 100644 --- a/public/app/features/browse-dashboards/api/services.ts +++ b/public/app/features/browse-dashboards/api/services.ts @@ -1,7 +1,8 @@ +import { getBackendSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { GENERAL_FOLDER_UID } from 'app/features/search/constants'; import { getGrafanaSearcher } from 'app/features/search/service/searcher'; -import { DashboardQueryResult } from 'app/features/search/service/types'; +import { NestedFolderDTO } from 'app/features/search/service/types'; import { queryResultToViewItem } from 'app/features/search/service/utils'; import { DashboardViewItem } from 'app/features/search/types'; import { AccessControlAction } from 'app/types/accessControl'; @@ -16,24 +17,22 @@ export async function listFolders( page = 1, pageSize = PAGE_SIZE ): Promise { - const searcher = getGrafanaSearcher(); + const backendSrv = getBackendSrv(); - let folders: DashboardQueryResult[] = []; + // TODO: what to do here for unified search? + let folders: NestedFolderDTO[] = []; if (contextSrv.hasPermission(AccessControlAction.FoldersRead)) { - const foldersResults = await searcher.search({ - kind: ['folder'], - location: parentUID || 'general', - from: (page - 1) * pageSize, // our pages are 1-indexed, so we need to -1 to convert that to correct value to skip + folders = await backendSrv.get('/api/folders', { + parentUid: parentUID, + page, limit: pageSize, - offset: (page - 1) * pageSize, }); - folders = foldersResults.view.toArray(); } return folders.map((item) => ({ kind: 'folder', uid: item.uid, - title: item.name, + title: item.title, parentTitle, parentUID, managedBy: item.managedBy, From 1d4067216db58a862d3237ea7f8c6f82f4387a1d Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 25 Nov 2025 12:19:31 +0100 Subject: [PATCH 101/378] Alerting: Add search.folder filter to the Prometheus rules API (#114358) --- .../ngalert/api/api_prometheus_test.go | 96 +++++++++++++ .../ngalert/api/prometheus/api_prometheus.go | 17 +++ pkg/services/ngalert/api/prometheus/util.go | 41 ++++++ .../ngalert/api/prometheus/util_test.go | 132 ++++++++++++++++++ 4 files changed, 286 insertions(+) create mode 100644 pkg/services/ngalert/api/prometheus/util.go create mode 100644 pkg/services/ngalert/api/prometheus/util_test.go diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index e31e713fb33..25acf56f7d3 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "slices" "testing" "time" @@ -2790,6 +2791,101 @@ func TestRouteGetRuleStatuses(t *testing.T) { require.NotEmpty(t, res.Data.NextToken) }) }) + + t.Run("with search.folder filter", func(t *testing.T) { + fakeStore, fakeAIM, api := setupAPI(t) + + // Create folders with different paths + folder1 := &folder.Folder{UID: "prod-uid", Title: "Production", Fullpath: "Production", OrgID: orgID} + folder2 := &folder.Folder{UID: "prod-alerts-uid", Title: "Alerts", Fullpath: "Production/Alerts", OrgID: orgID} + folder3 := &folder.Folder{UID: "dev-uid", Title: "Monitoring", Fullpath: "Development/Monitoring", OrgID: orgID} + folder4 := &folder.Folder{UID: "prod-crit-uid", Title: "Critical", Fullpath: "Production/Critical", OrgID: orgID} + fakeStore.Folders[orgID] = []*folder.Folder{folder1, folder2, folder3, folder4} + + // Create rules in different folders + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithNamespaceUID("prod-uid"), gen.WithUID("rule1"), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithNamespaceUID("prod-alerts-uid"), gen.WithUID("rule2"), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithNamespaceUID("dev-uid"), gen.WithUID("rule3"), gen.WithNoNotificationSettings()) + generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(), + gen.WithNamespaceUID("prod-crit-uid"), gen.WithUID("rule4"), gen.WithNoNotificationSettings()) + + testCases := []struct { + name string + searchFolder string + expectedUIDs []string + }{ + { + name: "search 'production' matches Production folders", + searchFolder: "production", + expectedUIDs: []string{"rule1", "rule2", "rule4"}, + }, + { + name: "search 'prod alerts' matches Production/Alerts", + searchFolder: "prod alerts", + expectedUIDs: []string{"rule2"}, + }, + { + name: "search 'dev' matches Development", + searchFolder: "dev", + expectedUIDs: []string{"rule3"}, + }, + { + name: "search 'critical' matches Critical folder", + searchFolder: "critical", + expectedUIDs: []string{"rule4"}, + }, + { + name: "case insensitive search", + searchFolder: "PRODUCTION", + expectedUIDs: []string{"rule1", "rule2", "rule4"}, + }, + { + name: "empty search returns all rules", + searchFolder: "", + expectedUIDs: []string{"rule1", "rule2", "rule3", "rule4"}, + }, + { + name: "non-matching search returns no rules", + searchFolder: "nonexistent", + expectedUIDs: []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + reqURL := "/api/v1/rules" + if tc.searchFolder != "" { + reqURL += "?search.folder=" + url.QueryEscape(tc.searchFolder) + } + req, err := http.NewRequest("GET", reqURL, nil) + require.NoError(t, err) + ctx := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{OrgID: orgID, Permissions: queryPermissions}, + } + + resp := api.RouteGetRuleStatuses(ctx) + require.Equal(t, http.StatusOK, resp.Status()) + + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + require.Equal(t, "success", res.Status) + + // Collect rule UIDs from response + actualUIDs := []string{} + for _, group := range res.Data.RuleGroups { + for _, rule := range group.Rules { + actualUIDs = append(actualUIDs, rule.UID) + } + } + + require.ElementsMatch(t, tc.expectedUIDs, actualUIDs) + }) + } + }) } func setupAPI(t *testing.T) (*fakes.RuleStore, *fakeAlertInstanceManager, PrometheusSrv) { diff --git a/pkg/services/ngalert/api/prometheus/api_prometheus.go b/pkg/services/ngalert/api/prometheus/api_prometheus.go index 082c1bfd333..d07e07d41fe 100644 --- a/pkg/services/ngalert/api/prometheus/api_prometheus.go +++ b/pkg/services/ngalert/api/prometheus/api_prometheus.go @@ -689,9 +689,20 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt namespaceUIDs := make([]string, 0, len(opts.AllowedNamespaces)) folderUID := opts.Query.Get("folder_uid") + searchFolder := opts.Query.Get("search.folder") + _, exists := opts.AllowedNamespaces[folderUID] if folderUID != "" && exists { + // Exact folder UID match namespaceUIDs = append(namespaceUIDs, folderUID) + } else if searchFolder != "" { + // Search folders by full path + matcher := NewTextMatcher(searchFolder) + for uid, fullpath := range opts.AllowedNamespaces { + if matcher.Match(fullpath) { + namespaceUIDs = append(namespaceUIDs, uid) + } + } } else { for k := range opts.AllowedNamespaces { namespaceUIDs = append(namespaceUIDs, k) @@ -700,9 +711,15 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt span.SetAttributes( attribute.Bool("folder_uid_set", folderUID != ""), + attribute.Bool("search_folder_set", searchFolder != ""), attribute.Int("namespace_count", len(namespaceUIDs)), ) + if searchFolder != "" && len(namespaceUIDs) == 0 { + log.Debug("No folders matched search.folder, returning empty response") + return ruleResponse + } + ruleGroups := opts.Query["rule_group"] ruleUIDs := opts.Query["rule_uid"] diff --git a/pkg/services/ngalert/api/prometheus/util.go b/pkg/services/ngalert/api/prometheus/util.go new file mode 100644 index 00000000000..9c5ecb50d82 --- /dev/null +++ b/pkg/services/ngalert/api/prometheus/util.go @@ -0,0 +1,41 @@ +package api + +import ( + "strings" +) + +// TextMatcher performs case-insensitive sequential substring matching. +// It checks if text contains all search words in the order provided, +// but not necessarily consecutively. +// +// Example: NewTextMatcher("api time").Match("API Response Time") returns true +type TextMatcher struct { + words []string +} + +func NewTextMatcher(search string) *TextMatcher { + words := strings.Fields(search) + for i := range words { + words[i] = strings.ToLower(words[i]) + } + return &TextMatcher{words: words} +} + +func (m *TextMatcher) Match(text string) bool { + if len(m.words) == 0 { + return true + } + lowerText := strings.ToLower(text) + start := 0 + for _, word := range m.words { + if start > len(lowerText) { + return false + } + idx := strings.Index(lowerText[start:], word) + if idx == -1 { + return false + } + start += idx + len(word) + } + return true +} diff --git a/pkg/services/ngalert/api/prometheus/util_test.go b/pkg/services/ngalert/api/prometheus/util_test.go new file mode 100644 index 00000000000..21940a5a8b8 --- /dev/null +++ b/pkg/services/ngalert/api/prometheus/util_test.go @@ -0,0 +1,132 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewTextMatcher(t *testing.T) { + tests := []struct { + name string + input string + expectedWords []string + }{ + { + name: "empty string", + input: "", + expectedWords: []string{}, + }, + { + name: "whitespace only", + input: " ", + expectedWords: []string{}, + }, + { + name: "single word", + input: "alerts", + expectedWords: []string{"alerts"}, + }, + { + name: "multiple words", + input: "parent alerts", + expectedWords: []string{"parent", "alerts"}, + }, + { + name: "mixed case normalized to lowercase", + input: "Parent Alerts", + expectedWords: []string{"parent", "alerts"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matcher := NewTextMatcher(tt.input) + assert.Equal(t, tt.expectedWords, matcher.words) + }) + } +} + +func TestTextMatcher_Match(t *testing.T) { + tests := []struct { + name string + search string + text string + expected bool + }{ + { + name: "empty search matches everything", + search: "", + text: "any text", + expected: true, + }, + { + name: "whitespace search matches everything", + search: " ", + text: "any text", + expected: true, + }, + { + name: "exact match", + search: "alerts", + text: "alerts", + expected: true, + }, + { + name: "case insensitive match", + search: "parent alerts", + text: "Parent Folder/Alerts", + expected: true, + }, + { + name: "partial match", + search: "folder", + text: "Parent Folder/Alerts", + expected: true, + }, + { + name: "sequential words match", + search: "api time", + text: "API Response Time", + expected: true, + }, + { + name: "non-consecutive but sequential words match", + search: "parent rules", + text: "Parent Folder/Alert Rules", + expected: true, + }, + { + name: "word not found", + search: "missing", + text: "Parent Folder/Alerts", + expected: false, + }, + { + name: "words out of order", + search: "alerts parent", + text: "Parent Folder/Alerts", + expected: false, + }, + { + name: "folder path search", + search: "prod alerts", + text: "Monitoring/Production/Alerts", + expected: true, + }, + { + name: "multiple spaces in search", + search: "api response time", + text: "API Response Time", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matcher := NewTextMatcher(tt.search) + result := matcher.Match(tt.text) + assert.Equal(t, tt.expected, result) + }) + } +} From 1672bbada0367ea22ad551360ac5c2e4d32f9697 Mon Sep 17 00:00:00 2001 From: Lauren <61048546+laurenashleigh@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:39:18 +0000 Subject: [PATCH 102/378] Alerting: Change group filtering to search-based using lightweight BE endpoint (#114347) * change group filtering from load-all to search-based * generate translations * refactoring * Resolve design comments * resolve PR comment --- .../Filter/useRuleFilterAutocomplete.test.ts | 124 ++++++++++++++++++ .../rules/Filter/useRuleFilterAutocomplete.ts | 66 +++++++--- .../rule-list/filter/RulesFilter.v2.tsx | 10 +- public/locales/en-US/grafana.json | 8 +- 4 files changed, 177 insertions(+), 31 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rules/Filter/useRuleFilterAutocomplete.test.ts diff --git a/public/app/features/alerting/unified/components/rules/Filter/useRuleFilterAutocomplete.test.ts b/public/app/features/alerting/unified/components/rules/Filter/useRuleFilterAutocomplete.test.ts new file mode 100644 index 00000000000..0f4c2b49741 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/Filter/useRuleFilterAutocomplete.test.ts @@ -0,0 +1,124 @@ +import { renderHook } from 'test/test-utils'; + +import { GrafanaPromRuleGroupDTO } from 'app/types/unified-alerting-dto'; + +import { prometheusApi } from '../../../api/prometheusApi'; +import { setupMswServer } from '../../../mockApi'; + +import { useNamespaceAndGroupOptions } from './useRuleFilterAutocomplete'; + +setupMswServer(); + +jest.mock('../../../api/prometheusApi', () => ({ + prometheusApi: { + useLazyGetGrafanaGroupsQuery: jest.fn(), + useLazyGetGroupsQuery: jest.fn(), + }, +})); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getDataSourceSrv: () => ({ + getList: jest.fn().mockReturnValue([]), + }), +})); + +describe('useNamespaceAndGroupOptions', () => { + let mockFetchGrafanaGroups: jest.Mock; + let mockFetchExternalGroups: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + + mockFetchGrafanaGroups = jest.fn(); + mockFetchExternalGroups = jest.fn(); + + (prometheusApi.useLazyGetGrafanaGroupsQuery as jest.Mock).mockReturnValue([mockFetchGrafanaGroups]); + (prometheusApi.useLazyGetGroupsQuery as jest.Mock).mockReturnValue([mockFetchExternalGroups]); + }); + + describe('groupOptions', () => { + it('should require minimum 3 characters before searching', async () => { + const { result } = renderHook(() => useNamespaceAndGroupOptions()); + + const options = await result.current.groupOptions('ab'); + + expect(options).toHaveLength(1); + expect(options[0]).toEqual({ + label: 'Type at least 3 characters to search groups', + value: '__GRAFANA_INFO_OPTION__', + infoOption: true, + }); + expect(mockFetchGrafanaGroups).not.toHaveBeenCalled(); + }); + + it('should call API with searchGroupName when 3+ characters entered', async () => { + const mockGroups: GrafanaPromRuleGroupDTO[] = [ + { name: 'cpu-alerts', file: 'folder1', folderUid: 'uid1', interval: 60, rules: [] }, + { name: 'cpu-usage', file: 'folder2', folderUid: 'uid2', interval: 60, rules: [] }, + ]; + + mockFetchGrafanaGroups.mockReturnValue({ + unwrap: () => + Promise.resolve({ + data: { groups: mockGroups }, + }), + }); + + const { result } = renderHook(() => useNamespaceAndGroupOptions()); + + const options = await result.current.groupOptions('cpu'); + + expect(mockFetchGrafanaGroups).toHaveBeenCalledWith({ + limitAlerts: 0, + searchGroupName: 'cpu', + groupLimit: 100, + }); + + expect(options).toHaveLength(2); + expect(options[0]).toEqual({ label: 'cpu-alerts', value: 'cpu-alerts' }); + expect(options[1]).toEqual({ label: 'cpu-usage', value: 'cpu-usage' }); + }); + + it('should show message when no groups match search', async () => { + mockFetchGrafanaGroups.mockReturnValue({ + unwrap: () => + Promise.resolve({ + data: { groups: [] }, + }), + }); + + const { result } = renderHook(() => useNamespaceAndGroupOptions()); + + const options = await result.current.groupOptions('xyz123'); + + expect(options).toHaveLength(1); + expect(options[0]).toEqual({ + label: 'No groups found matching "xyz123"', + value: '__GRAFANA_INFO_OPTION__', + infoOption: true, + }); + }); + + it('should handle API errors gracefully', async () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + mockFetchGrafanaGroups.mockReturnValue({ + unwrap: () => Promise.reject(new Error('API Error')), + }); + + const { result } = renderHook(() => useNamespaceAndGroupOptions()); + + const options = await result.current.groupOptions('cpu'); + + expect(options).toHaveLength(1); + expect(options[0]).toEqual({ + label: 'Error searching groups', + value: '__GRAFANA_INFO_OPTION__', + infoOption: true, + }); + + consoleErrorSpy.mockRestore(); + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/rules/Filter/useRuleFilterAutocomplete.ts b/public/app/features/alerting/unified/components/rules/Filter/useRuleFilterAutocomplete.ts index df6a317d071..67b3591eae0 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/useRuleFilterAutocomplete.ts +++ b/public/app/features/alerting/unified/components/rules/Filter/useRuleFilterAutocomplete.ts @@ -1,3 +1,4 @@ +import { chain } from 'lodash'; import { useCallback } from 'react'; import { DataSourceInstanceSettings } from '@grafana/data'; @@ -15,7 +16,9 @@ function getExternalRuleDataSources() { return getRulesDataSources().filter((ds: DataSourceInstanceSettings) => !!ds?.url); } -const THRESHOLD_LIMIT = 500; +const NAMESPACE_THRESHOLD_LIMIT = 500; +const MIN_GROUP_SEARCH_CHARACTERS = 3; +const GROUP_SEARCH_LIMIT = 100; function createInfoOption(message: string): ComboboxOption { return { @@ -60,7 +63,10 @@ export function useNamespaceAndGroupOptions(): { const namespaceOptions = useCallback( async (inputValue: string) => { // Grafana namespaces - fetch with limit to check threshold - const grafanaResponse = await fetchGrafanaGroups({ limitAlerts: 0, groupLimit: THRESHOLD_LIMIT + 1 }).unwrap(); + const grafanaResponse = await fetchGrafanaGroups({ + limitAlerts: 0, + groupLimit: NAMESPACE_THRESHOLD_LIMIT + 1, + }).unwrap(); const grafanaFolderNames = Array.from( new Set(grafanaResponse.data.groups.map((g: GrafanaPromRuleGroupDTO) => g.file || 'default')) ); @@ -71,7 +77,7 @@ export function useNamespaceAndGroupOptions(): { fetchExternalGroups({ ruleSource: { uid: ds.uid }, excludeAlerts: true, - groupLimit: THRESHOLD_LIMIT + 1, + groupLimit: NAMESPACE_THRESHOLD_LIMIT + 1, notificationOptions: { showErrorAlert: false }, }).unwrap() ); @@ -84,8 +90,8 @@ export function useNamespaceAndGroupOptions(): { const totalNamespaces = grafanaFolderNames.length + namespaceNameSet.size; - // If we have more than THRESHOLD_LIMIT unique namespaces, show info message - if (totalNamespaces > THRESHOLD_LIMIT) { + // If we have more than NAMESPACE_THRESHOLD_LIMIT unique namespaces, show info message + if (totalNamespaces > NAMESPACE_THRESHOLD_LIMIT) { return [ createInfoOption( t( @@ -117,33 +123,53 @@ export function useNamespaceAndGroupOptions(): { const groupOptions = useCallback( async (inputValue: string) => { - const grafanaResponse = await fetchGrafanaGroups({ limitAlerts: 0, groupLimit: THRESHOLD_LIMIT + 1 }).unwrap(); - const groupNames = Array.from(new Set(grafanaResponse.data.groups.map((g: GrafanaPromRuleGroupDTO) => g.name))); - - // If we have more than THRESHOLD_LIMIT unique groups, show info message - if (groupNames.length > THRESHOLD_LIMIT) { + // Require minimum characters for search + const trimmedInput = inputValue?.trim() || ''; + if (trimmedInput.length < MIN_GROUP_SEARCH_CHARACTERS) { return [ createInfoOption( - t( - 'alerting.rules-filter.group-autocomplete-unavailable', - 'Due to large number of groups, autocomplete is not available' - ) + t('alerting.rules-filter.group-search-prompt', 'Type at least 3 characters to search groups') ), ]; } - const options: Array> = groupNames - .map((name) => ({ label: name, value: name })) - .sort((a, b) => collator.compare(a.label ?? '', b.label ?? '')); + try { + // Use the backend search with lightweight response + const grafanaResponse = await fetchGrafanaGroups({ + limitAlerts: 0, // Lightweight - no alert data + searchGroupName: trimmedInput, // Backend filtering via search.rule_group parameter + groupLimit: GROUP_SEARCH_LIMIT, // Reasonable limit for dropdown results + }).unwrap(); - const filtered = filterBySearch(options, inputValue); - return filtered; + // Deduplicate group names + const groupNames = chain(grafanaResponse.data.groups).map('name').compact().uniq().value(); + + // No results found + if (groupNames.length === 0) { + return [ + createInfoOption( + t('alerting.rules-filter.group-no-results', 'No groups found matching "{{search}}"', { + search: trimmedInput, + }) + ), + ]; + } + + const options: Array> = groupNames + .map((name) => ({ label: name, value: name })) + .sort((a, b) => collator.compare(a.label ?? '', b.label ?? '')); + + return options; + } catch (error) { + console.error('Error fetching groups:', error); + return [createInfoOption(t('alerting.rules-filter.group-search-error', 'Error searching groups'))]; + } }, [fetchGrafanaGroups] ); const namespacePlaceholder = t('alerting.rules-filter.filter-options.placeholder-namespace', 'Select namespace'); - const groupPlaceholder = t('grafana.select-group', 'Select group'); + const groupPlaceholder = t('alerting.rules-filter.placeholder-group-search', 'Search group'); return { namespaceOptions, groupOptions, namespacePlaceholder, groupPlaceholder }; } diff --git a/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.tsx b/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.tsx index 1cc702038ac..a2a1566be29 100644 --- a/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.tsx +++ b/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.tsx @@ -439,13 +439,7 @@ function GroupField({ groupPlaceholder: string; portalContainer?: HTMLElement; }) { - const { control, setValue } = useFormContext(); - - const wrappedOptions = useCallback( - (inputValue: string) => - createThresholdAwareOptions(groupOptions, (value) => setValue('groupName', value), 'groupName')(inputValue), - [groupOptions, setValue] - ); + const { control } = useFormContext(); return ( <> @@ -458,7 +452,7 @@ function GroupField({ render={({ field }) => ( placeholder={groupPlaceholder} - options={wrappedOptions} + options={groupOptions} onChange={(option) => { if (!option?.infoOption) { field.onChange(option?.value || null); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index fd2b50edb93..e8d4b15d05f 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Search by name or enter filter query..." }, "grafana-folder": "Grafana folder", - "group-autocomplete-unavailable": "Due to large number of groups, autocomplete is not available", + "group-no-results": "No groups found matching \"{{search}}\"", + "group-search-error": "Error searching groups", + "group-search-prompt": "Type at least 3 characters to search groups", "health": "Health", "label": { "hide": "Hide", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "All data sources", "placeholder-contact-point": "Select contact point", "placeholder-data-sources": "Select data sources", + "placeholder-group-search": "Search group", "placeholder-labels": "Select labels", "plugin-rules": "Plugin rules", "rule-source": { @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Go back" } - }, - "select-group": "Select group" + } }, "grafana-data": { "datetime": { From eb25d9f4a8596dd0eed11f44d111d4d99117f038 Mon Sep 17 00:00:00 2001 From: Denis Vodopianov Date: Tue, 25 Nov 2025 13:41:02 +0100 Subject: [PATCH 103/378] chore: Add retries to the setup enterprise action (#114399) * add retries to the setup enterprise action * add fail pipeline after the last retry * fix a dyslectic typo * fix syntax errors in the script * fix syntax errors in the script and the issue detected by zizmor * cd to grafana enterprise dir * update the go ws --- .github/actions/setup-enterprise/action.yml | 48 +++++++++++++++++---- apps/iam/go.sum | 6 --- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/.github/actions/setup-enterprise/action.yml b/.github/actions/setup-enterprise/action.yml index 134a7e7f004..2ccd8ef57da 100644 --- a/.github/actions/setup-enterprise/action.yml +++ b/.github/actions/setup-enterprise/action.yml @@ -33,16 +33,48 @@ runs: env: GH_TOKEN: ${{ steps.generate_token.outputs.token }} run: | - git clone https://x-access-token:${GH_TOKEN}@github.com/grafana/grafana-enterprise.git ../grafana-enterprise; + RETRIES="5" + + for i in $(seq 1 $RETRIES); do + echo "Attempt $i to clone..." + + if git clone https://x-access-token:${GH_TOKEN}@github.com/grafana/grafana-enterprise.git ../grafana-enterprise; then + echo "Clone succeeded on attempt $i" + break + fi + + if [ $i -eq $RETRIES ]; then + echo "Clone failed after $RETRIES attempts, failing pipeline." + exit 1 + fi + + sleep "$i" + done cd ../grafana-enterprise - if git checkout ${GITHUB_HEAD_REF}; then - echo "checked out ${GITHUB_HEAD_REF}" - elif git checkout ${GITHUB_BASE_REF}; then - echo "checked out ${GITHUB_BASE_REF}" - else - git checkout main - fi + for i in $(seq 1 $RETRIES); do + echo "Attempt $i to checkout..." + + if git checkout ${GITHUB_HEAD_REF}; then + echo "checked out ${GITHUB_HEAD_REF}" + elif git checkout ${GITHUB_BASE_REF}; then + echo "checked out ${GITHUB_BASE_REF}" + else + git checkout main + fi + + if [ $? -eq 0 ]; then + echo "Checkout succeeded, breaking retry loop" + break + fi + + if [ $i -eq $RETRIES ]; then + echo "Checkout failed after $RETRIES attempts, failing pipeline." + exit 1 + fi + + sleep "$i" + done QUIET=1 ./build.sh diff --git a/apps/iam/go.sum b/apps/iam/go.sum index c1be3c78ff2..4cba06c00ec 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -1410,8 +1410,6 @@ github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQcc github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/sercand/kuberesolver/v6 v6.0.0 h1:ScvS2Ga9snVkpOahln/BCLySr3/iBAHJf25u66DweZ0= github.com/sercand/kuberesolver/v6 v6.0.0/go.mod h1:Dxkqms3OJadP5zirIBPLi9FV8Qpys3T3w40XPEcVsu0= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3 h1:/4/IJi5iyTdh6mqOUaASW148HQpujYiHl0Wl78dSOSc= @@ -1534,10 +1532,6 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= -github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= From 9091ac6f5c56ac0684a03ed8c3c3e55fbe7dcec6 Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 25 Nov 2025 10:04:43 -0300 Subject: [PATCH 104/378] Secrets: add basic namespace and name checks to keeper store and secure value store (#114355) --- ...alue_get_latest_version_and_created_at.sql | 4 ++- .../data/secure_value_list_by_lease_token.sql | 3 +- pkg/storage/secret/metadata/keeper_store.go | 12 ++++++++ .../secret/metadata/secure_value_store.go | 30 ++++++++++++++++--- ...ted_at-get latest secure value version.sql | 4 ++- ...ist_by_lease_token-list by lease token.sql | 3 +- ...ted_at-get latest secure value version.sql | 4 ++- ...ist_by_lease_token-list by lease token.sql | 3 +- ...ted_at-get latest secure value version.sql | 4 ++- ...ist_by_lease_token-list by lease token.sql | 3 +- 10 files changed, 58 insertions(+), 12 deletions(-) diff --git a/pkg/storage/secret/metadata/data/secure_value_get_latest_version_and_created_at.sql b/pkg/storage/secret/metadata/data/secure_value_get_latest_version_and_created_at.sql index 1c804c2d031..c7349f27a03 100644 --- a/pkg/storage/secret/metadata/data/secure_value_get_latest_version_and_created_at.sql +++ b/pkg/storage/secret/metadata/data/secure_value_get_latest_version_and_created_at.sql @@ -1,7 +1,9 @@ SELECT {{ .Ident "created" }}, {{ .Ident "version" }}, - {{ .Ident "active" }} + {{ .Ident "active" }}, + {{ .Ident "namespace" }}, + {{ .Ident "name" }} FROM {{ .Ident "secret_secure_value" }} WHERE diff --git a/pkg/storage/secret/metadata/data/secure_value_list_by_lease_token.sql b/pkg/storage/secret/metadata/data/secure_value_list_by_lease_token.sql index 275143a9672..fc62c271df4 100644 --- a/pkg/storage/secret/metadata/data/secure_value_list_by_lease_token.sql +++ b/pkg/storage/secret/metadata/data/secure_value_list_by_lease_token.sql @@ -18,7 +18,8 @@ SELECT {{ .Ident "owner_reference_api_group" }}, {{ .Ident "owner_reference_api_version" }}, {{ .Ident "owner_reference_kind" }}, - {{ .Ident "owner_reference_name" }} + {{ .Ident "owner_reference_name" }}, + {{ .Ident "lease_token" }} FROM {{ .Ident "secret_secure_value" }} WHERE diff --git a/pkg/storage/secret/metadata/keeper_store.go b/pkg/storage/secret/metadata/keeper_store.go index 6036bfe2502..d4516158c81 100644 --- a/pkg/storage/secret/metadata/keeper_store.go +++ b/pkg/storage/secret/metadata/keeper_store.go @@ -201,6 +201,10 @@ func (s *keeperMetadataStorage) read(ctx context.Context, namespace, name string if err := res.Err(); err != nil { return nil, fmt.Errorf("read rows error: %w", err) } + if keeper.Namespace != namespace || keeper.Name != name { + return nil, fmt.Errorf("bug: expected to find keeper namespace=%+v name=%+v but got keeper namespace=%+v name%+v", + namespace, name, keeper.Namespace, keeper.Name) + } return &keeper, nil } @@ -405,6 +409,10 @@ func (s *keeperMetadataStorage) List(ctx context.Context, namespace xkube.Namesp return nil, fmt.Errorf("error reading keeper row: %w", err) } + if row.Namespace != namespace.String() { + return nil, fmt.Errorf("bug: expected to list keepers for namespace %+v but got one from namespace %+v", namespace, row.Namespace) + } + keeper, err := row.toKubernetes() if err != nil { return nil, fmt.Errorf("failed to convert to kubernetes object: %w", err) @@ -706,6 +714,10 @@ func (s *keeperMetadataStorage) GetActiveKeeper(ctx context.Context, namespace s return keeper, fmt.Errorf("converting from keeperDB to kubernetes struct: %w", err) } + if keeperDB.Namespace != namespace { + return nil, fmt.Errorf("bug: expected to find keeper to namespace %+v but got one for namespace %+v", namespace, keeperDB.Namespace) + } + return keeper, nil } diff --git a/pkg/storage/secret/metadata/secure_value_store.go b/pkg/storage/secret/metadata/secure_value_store.go index d3a137ea611..1fc3a788cf2 100644 --- a/pkg/storage/secret/metadata/secure_value_store.go +++ b/pkg/storage/secret/metadata/secure_value_store.go @@ -199,14 +199,21 @@ func (s *secureValueMetadataStorage) getLatestVersionAndCreatedAt(ctx context.Co } var ( - createdAt int64 - version int64 - active bool + createdAt int64 + version int64 + active bool + namespaceFromDB string + nameFromDB string ) - if err := rows.Scan(&createdAt, &version, &active); err != nil { + if err := rows.Scan(&createdAt, &version, &active, &namespaceFromDB, &nameFromDB); err != nil { return versionAndCreatedAt{}, fmt.Errorf("scanning version from returned rows: %w", err) } + if namespaceFromDB != namespace.String() || nameFromDB != name { + return versionAndCreatedAt{}, fmt.Errorf("bug: expected to find latest version for namespace=%+v name=%+v but got version for namespace=%+v name=%+v", + namespace, name, namespaceFromDB, nameFromDB) + } + if !active { createdAt = 0 } @@ -255,6 +262,11 @@ func (s *secureValueMetadataStorage) readActiveVersion(ctx context.Context, name if err := res.Err(); err != nil { return secureValueDB{}, fmt.Errorf("read rows error: %w", err) } + + if secureValue.Namespace != namespace.String() || secureValue.Name != name { + return secureValueDB{}, fmt.Errorf("bug: expected to read secure value %+v from namespace %+v, but got a different row", name, namespace) + } + return secureValue, nil } @@ -364,6 +376,10 @@ func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.N return nil, fmt.Errorf("bug: read an inactive version: row=%+v", row) } + if row.Namespace != namespace.String() { + return nil, fmt.Errorf("bug: expected to list secure values from namespace %+v but got one from namespace %+v", namespace.String(), row.Namespace) + } + secureValue, err := row.toKubernetes() if err != nil { return nil, fmt.Errorf("convert to kubernetes object: %w", err) @@ -644,6 +660,7 @@ func (s *secureValueMetadataStorage) listByLeaseToken(ctx context.Context, lease secureValues := make([]secretv1beta1.SecureValue, 0) for rows.Next() { row := secureValueDB{} + var leaseTokenDB string err = rows.Scan(&row.GUID, &row.Name, &row.Namespace, &row.Annotations, @@ -653,12 +670,17 @@ func (s *secureValueMetadataStorage) listByLeaseToken(ctx context.Context, lease &row.Description, &row.Keeper, &row.Decrypters, &row.Ref, &row.ExternalID, &row.Version, &row.Active, &row.OwnerReferenceAPIGroup, &row.OwnerReferenceAPIVersion, &row.OwnerReferenceKind, &row.OwnerReferenceName, + &leaseTokenDB, ) if err != nil { return nil, fmt.Errorf("error reading secure value row: %w", err) } + if leaseTokenDB != leaseToken { + return nil, fmt.Errorf("bug: expected to list secure values with lease token %+v but got a secure value with another lease token %+v", leaseToken, leaseToken) + } + secureValue, err := row.toKubernetes() if err != nil { return nil, fmt.Errorf("convert to kubernetes object: %w", err) diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version_and_created_at-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version_and_created_at-get latest secure value version.sql index 661c6474221..2e88ee079c2 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version_and_created_at-get latest secure value version.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version_and_created_at-get latest secure value version.sql @@ -1,7 +1,9 @@ SELECT `created`, `version`, - `active` + `active`, + `namespace`, + `name` FROM `secret_secure_value` WHERE diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_list_by_lease_token-list by lease token.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_list_by_lease_token-list by lease token.sql index 07534407789..0485a5c94c1 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_list_by_lease_token-list by lease token.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_list_by_lease_token-list by lease token.sql @@ -18,7 +18,8 @@ SELECT `owner_reference_api_group`, `owner_reference_api_version`, `owner_reference_kind`, - `owner_reference_name` + `owner_reference_name`, + `lease_token` FROM `secret_secure_value` WHERE diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version_and_created_at-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version_and_created_at-get latest secure value version.sql index e1c3834ecbb..a9f21aafc0a 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version_and_created_at-get latest secure value version.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version_and_created_at-get latest secure value version.sql @@ -1,7 +1,9 @@ SELECT "created", "version", - "active" + "active", + "namespace", + "name" FROM "secret_secure_value" WHERE diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_list_by_lease_token-list by lease token.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_list_by_lease_token-list by lease token.sql index 55077bff201..0829c0a7ceb 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_list_by_lease_token-list by lease token.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_list_by_lease_token-list by lease token.sql @@ -18,7 +18,8 @@ SELECT "owner_reference_api_group", "owner_reference_api_version", "owner_reference_kind", - "owner_reference_name" + "owner_reference_name", + "lease_token" FROM "secret_secure_value" WHERE diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version_and_created_at-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version_and_created_at-get latest secure value version.sql index e1c3834ecbb..a9f21aafc0a 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version_and_created_at-get latest secure value version.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version_and_created_at-get latest secure value version.sql @@ -1,7 +1,9 @@ SELECT "created", "version", - "active" + "active", + "namespace", + "name" FROM "secret_secure_value" WHERE diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list_by_lease_token-list by lease token.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list_by_lease_token-list by lease token.sql index 55077bff201..0829c0a7ceb 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list_by_lease_token-list by lease token.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list_by_lease_token-list by lease token.sql @@ -18,7 +18,8 @@ SELECT "owner_reference_api_group", "owner_reference_api_version", "owner_reference_kind", - "owner_reference_name" + "owner_reference_name", + "lease_token" FROM "secret_secure_value" WHERE From 93ec32dd6adfbbe621fcb4598d0ac0303df767d2 Mon Sep 17 00:00:00 2001 From: Misi Date: Tue, 25 Nov 2025 14:19:57 +0100 Subject: [PATCH 105/378] IAM: Add teams/{id}/groups as a custom endpoint to Teams API (#114228) * Add teams/{id}/groups as a custom endpoint * TeamGroupsHandler OSS and Ent registration * Update OpenAPI spec * Add indexer tests for external group mapping * Remove noopsearch * Remove unnecessary interface declaration, fixes * Chores * fmt * Rename constant * Align the rest to the changes of main * Update workspace * Add missing file --- apps/iam/go.mod | 8 +- apps/iam/kinds/team.cue | 14 ++ apps/iam/pkg/apis/iam/v0alpha1/register.go | 1 + .../pkg/apis/iam/v0alpha1/team_client_gen.go | 24 ++ .../team_getgroups_response_body_types_gen.go | 26 ++ ...eam_getgroups_response_object_types_gen.go | 37 +++ .../pkg/apis/iam/v0alpha1/zz_openapi_gen.go | 230 +++++++++++++----- apps/iam/pkg/apis/iam_manifest.go | 56 ++++- .../rtkq/iam/v0alpha1/endpoints.gen.ts | 22 ++ .../src/clients/rtkq/legacy/endpoints.gen.ts | 1 + .../apis/iam/externalgroupmapping/models.go | 10 + .../externalgroupmapping/team_groups_noop.go | 58 +++++ pkg/registry/apis/iam/models.go | 3 + pkg/registry/apis/iam/register.go | 6 +- pkg/registry/apis/iam/user/legacy_search.go | 6 +- pkg/registry/apis/wireset.go | 4 + pkg/server/wire_gen.go | 7 +- .../unified/search/builders/document.go | 7 +- .../unified/search/builders/document_test.go | 12 + .../search/builders/external_group_mapping.go | 80 ++++++ ...pping-mapping-with-team-and-group-out.json | 17 ++ ...p_mapping-mapping-with-team-and-group.json | 14 ++ pkg/storage/unified/search/builders/user.go | 6 +- .../iam.grafana.app-v0alpha1.json | 153 ++++++++++++ public/api-enterprise-spec.json | 3 + public/api-merged.json | 3 + public/openapi3.json | 3 + 27 files changed, 732 insertions(+), 79 deletions(-) create mode 100644 apps/iam/pkg/apis/iam/v0alpha1/team_getgroups_response_body_types_gen.go create mode 100644 apps/iam/pkg/apis/iam/v0alpha1/team_getgroups_response_object_types_gen.go create mode 100644 pkg/registry/apis/iam/externalgroupmapping/models.go create mode 100644 pkg/registry/apis/iam/externalgroupmapping/team_groups_noop.go create mode 100644 pkg/storage/unified/search/builders/external_group_mapping.go create mode 100644 pkg/storage/unified/search/builders/testdata/doc/external_group_mapping-mapping-with-team-and-group-out.json create mode 100644 pkg/storage/unified/search/builders/testdata/doc/external_group_mapping-mapping-with-team-and-group.json diff --git a/apps/iam/go.mod b/apps/iam/go.mod index e764e2a6158..73891acd454 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -14,16 +14,12 @@ replace github.com/grafana/grafana/apps/provisioning => ../provisioning replace github.com/grafana/grafana/apps/advisor => ../advisor -replace github.com/grafana/grafana/apps/annotation => ../annotation - replace github.com/grafana/grafana/apps/alerting/alertenrichment => ../alerting/alertenrichment replace github.com/grafana/grafana/apps/alerting/notifications => ../alerting/notifications replace github.com/grafana/grafana/apps/alerting/rules => ../alerting/rules -replace github.com/grafana/grafana/apps/collections => ../collections - replace github.com/grafana/grafana/apps/correlations => ../correlations replace github.com/grafana/grafana/apps/investigations => ../investigations @@ -46,6 +42,10 @@ replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver replace github.com/grafana/grafana/pkg/aggregator => ../../pkg/aggregator +replace github.com/grafana/grafana/apps/annotation => ../annotation + +replace github.com/grafana/grafana/apps/collections => ../collections + replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 require ( diff --git a/apps/iam/kinds/team.cue b/apps/iam/kinds/team.cue index 9acf9d60c01..412d8215999 100644 --- a/apps/iam/kinds/team.cue +++ b/apps/iam/kinds/team.cue @@ -18,4 +18,18 @@ teamv0alpha1: teamKind & { schema: { spec: v0alpha1.TeamSpec } + routes: { + "/groups": { + "GET": { + response: { + #ExternalGroupMapping: { + name: string + externalGroup: string + } + items: [...#ExternalGroupMapping] + } + responseMetadata: objectMeta: false + } + } + } } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/register.go b/apps/iam/pkg/apis/iam/v0alpha1/register.go index 54d60c7bd6f..be70a0e1714 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/register.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/register.go @@ -321,6 +321,7 @@ func AddAuthNKnownTypes(scheme *runtime.Scheme) error { &TeamBindingList{}, &ExternalGroupMapping{}, &ExternalGroupMappingList{}, + &GetGroups{}, // For now these are registered in pkg/apis/iam/v0alpha1/register.go // &UserTeamList{}, // &ServiceAccountTokenList{}, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_client_gen.go index 807c68dc0f6..2a6ce6caedb 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/team_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_client_gen.go @@ -2,6 +2,9 @@ package v0alpha1 import ( "context" + "encoding/json" + "fmt" + "net/http" "github.com/grafana/grafana-app-sdk/resource" ) @@ -78,3 +81,24 @@ func (c *TeamClient) Patch(ctx context.Context, identifier resource.Identifier, func (c *TeamClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } + +type GetGroupsRequest struct { + Headers http.Header +} + +func (c *TeamClient) GetGroups(ctx context.Context, identifier resource.Identifier, request GetGroupsRequest) (*GetGroups, error) { + resp, err := c.client.SubresourceRequest(ctx, identifier, resource.CustomRouteRequestOptions{ + Path: "/groups", + Verb: "GET", + Headers: request.Headers, + }) + if err != nil { + return nil, err + } + cast := GetGroups{} + err = json.Unmarshal(resp, &cast) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal response bytes into GetGroups: %w", err) + } + return &cast, nil +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_getgroups_response_body_types_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_getgroups_response_body_types_gen.go new file mode 100644 index 00000000000..553d7260c94 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_getgroups_response_body_types_gen.go @@ -0,0 +1,26 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping struct { + Name string `json:"name"` + ExternalGroup string `json:"externalGroup"` +} + +// NewVersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping creates a new VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping object. +func NewVersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping() *VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping { + return &VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping{} +} + +// +k8s:openapi-gen=true +type GetGroupsBody struct { + Items []VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping `json:"items"` +} + +// NewGetGroupsBody creates a new GetGroupsBody object. +func NewGetGroupsBody() *GetGroupsBody { + return &GetGroupsBody{ + Items: []VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping{}, + } +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_getgroups_response_object_types_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_getgroups_response_object_types_gen.go new file mode 100644 index 00000000000..3c8aa40a239 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_getgroups_response_object_types_gen.go @@ -0,0 +1,37 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +k8s:openapi-gen=true +type GetGroups struct { + metav1.TypeMeta `json:",inline"` + GetGroupsBody `json:",inline"` +} + +func NewGetGroups() *GetGroups { + return &GetGroups{} +} + +func (t *GetGroupsBody) DeepCopyInto(dst *GetGroupsBody) { + _ = resource.CopyObjectInto(dst, t) +} + +func (o *GetGroups) DeepCopyObject() runtime.Object { + dst := NewGetGroups() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetGroups) DeepCopyInto(dst *GetGroups) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.GetGroupsBody.DeepCopyInto(&dst.GetGroupsBody) +} + +var _ runtime.Object = NewGetGroups() diff --git a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go index f0c517afd38..e3b024799d7 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -12,71 +12,74 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRole": schema_pkg_apis_iam_v0alpha1_CoreRole(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleList": schema_pkg_apis_iam_v0alpha1_CoreRoleList(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleSpec": schema_pkg_apis_iam_v0alpha1_CoreRoleSpec(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleStatus": schema_pkg_apis_iam_v0alpha1_CoreRoleStatus(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRolespecPermission": schema_pkg_apis_iam_v0alpha1_CoreRolespecPermission(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_CoreRolestatusOperatorState(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMapping": schema_pkg_apis_iam_v0alpha1_ExternalGroupMapping(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingList": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingList(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingSpec": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingSpec(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingTeamRef": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingTeamRef(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": schema_pkg_apis_iam_v0alpha1_GlobalRole(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBinding": schema_pkg_apis_iam_v0alpha1_GlobalRoleBinding(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingList": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingList(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingSpec": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingSpec(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingStatus": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingStatus(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingspecRoleRef": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingspecRoleRef(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingspecSubject": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingspecSubject(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingstatusOperatorState(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleList": schema_pkg_apis_iam_v0alpha1_GlobalRoleList(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleSpec": schema_pkg_apis_iam_v0alpha1_GlobalRoleSpec(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleStatus": schema_pkg_apis_iam_v0alpha1_GlobalRoleStatus(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRolespecPermission": schema_pkg_apis_iam_v0alpha1_GlobalRolespecPermission(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_GlobalRolestatusOperatorState(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermission": schema_pkg_apis_iam_v0alpha1_ResourcePermission(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionList": schema_pkg_apis_iam_v0alpha1_ResourcePermissionList(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionSpec": schema_pkg_apis_iam_v0alpha1_ResourcePermissionSpec(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionStatus": schema_pkg_apis_iam_v0alpha1_ResourcePermissionStatus(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionspecPermission": schema_pkg_apis_iam_v0alpha1_ResourcePermissionspecPermission(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionspecResource": schema_pkg_apis_iam_v0alpha1_ResourcePermissionspecResource(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionstatusOperatorState": schema_pkg_apis_iam_v0alpha1_ResourcePermissionstatusOperatorState(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Role": schema_pkg_apis_iam_v0alpha1_Role(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBinding": schema_pkg_apis_iam_v0alpha1_RoleBinding(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingList": schema_pkg_apis_iam_v0alpha1_RoleBindingList(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingSpec": schema_pkg_apis_iam_v0alpha1_RoleBindingSpec(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingStatus": schema_pkg_apis_iam_v0alpha1_RoleBindingStatus(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingspecRoleRef": schema_pkg_apis_iam_v0alpha1_RoleBindingspecRoleRef(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingspecSubject": schema_pkg_apis_iam_v0alpha1_RoleBindingspecSubject(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_RoleBindingstatusOperatorState(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleList": schema_pkg_apis_iam_v0alpha1_RoleList(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleSpec": schema_pkg_apis_iam_v0alpha1_RoleSpec(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleStatus": schema_pkg_apis_iam_v0alpha1_RoleStatus(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RolespecPermission": schema_pkg_apis_iam_v0alpha1_RolespecPermission(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_RolestatusOperatorState(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccount": schema_pkg_apis_iam_v0alpha1_ServiceAccount(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountList": schema_pkg_apis_iam_v0alpha1_ServiceAccountList(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountSpec": schema_pkg_apis_iam_v0alpha1_ServiceAccountSpec(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountStatus": schema_pkg_apis_iam_v0alpha1_ServiceAccountStatus(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountstatusOperatorState": schema_pkg_apis_iam_v0alpha1_ServiceAccountstatusOperatorState(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Team": schema_pkg_apis_iam_v0alpha1_Team(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBinding": schema_pkg_apis_iam_v0alpha1_TeamBinding(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingList": schema_pkg_apis_iam_v0alpha1_TeamBindingList(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec": schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingStatus": schema_pkg_apis_iam_v0alpha1_TeamBindingStatus(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingTeamRef": schema_pkg_apis_iam_v0alpha1_TeamBindingTeamRef(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject": schema_pkg_apis_iam_v0alpha1_TeamBindingspecSubject(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamBindingstatusOperatorState(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamList": schema_pkg_apis_iam_v0alpha1_TeamList(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec": schema_pkg_apis_iam_v0alpha1_TeamSpec(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus": schema_pkg_apis_iam_v0alpha1_TeamStatus(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamstatusOperatorState(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.User": schema_pkg_apis_iam_v0alpha1_User(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserList": schema_pkg_apis_iam_v0alpha1_UserList(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec": schema_pkg_apis_iam_v0alpha1_UserSpec(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus": schema_pkg_apis_iam_v0alpha1_UserStatus(ref), - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState": schema_pkg_apis_iam_v0alpha1_UserstatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRole": schema_pkg_apis_iam_v0alpha1_CoreRole(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleList": schema_pkg_apis_iam_v0alpha1_CoreRoleList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleSpec": schema_pkg_apis_iam_v0alpha1_CoreRoleSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleStatus": schema_pkg_apis_iam_v0alpha1_CoreRoleStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRolespecPermission": schema_pkg_apis_iam_v0alpha1_CoreRolespecPermission(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_CoreRolestatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMapping": schema_pkg_apis_iam_v0alpha1_ExternalGroupMapping(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingList": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingSpec": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingTeamRef": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingTeamRef(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetGroups": schema_pkg_apis_iam_v0alpha1_GetGroups(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetGroupsBody": schema_pkg_apis_iam_v0alpha1_GetGroupsBody(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": schema_pkg_apis_iam_v0alpha1_GlobalRole(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBinding": schema_pkg_apis_iam_v0alpha1_GlobalRoleBinding(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingList": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingSpec": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingStatus": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingspecRoleRef": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingspecRoleRef(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingspecSubject": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingspecSubject(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingstatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleList": schema_pkg_apis_iam_v0alpha1_GlobalRoleList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleSpec": schema_pkg_apis_iam_v0alpha1_GlobalRoleSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleStatus": schema_pkg_apis_iam_v0alpha1_GlobalRoleStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRolespecPermission": schema_pkg_apis_iam_v0alpha1_GlobalRolespecPermission(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_GlobalRolestatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermission": schema_pkg_apis_iam_v0alpha1_ResourcePermission(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionList": schema_pkg_apis_iam_v0alpha1_ResourcePermissionList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionSpec": schema_pkg_apis_iam_v0alpha1_ResourcePermissionSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionStatus": schema_pkg_apis_iam_v0alpha1_ResourcePermissionStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionspecPermission": schema_pkg_apis_iam_v0alpha1_ResourcePermissionspecPermission(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionspecResource": schema_pkg_apis_iam_v0alpha1_ResourcePermissionspecResource(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionstatusOperatorState": schema_pkg_apis_iam_v0alpha1_ResourcePermissionstatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Role": schema_pkg_apis_iam_v0alpha1_Role(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBinding": schema_pkg_apis_iam_v0alpha1_RoleBinding(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingList": schema_pkg_apis_iam_v0alpha1_RoleBindingList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingSpec": schema_pkg_apis_iam_v0alpha1_RoleBindingSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingStatus": schema_pkg_apis_iam_v0alpha1_RoleBindingStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingspecRoleRef": schema_pkg_apis_iam_v0alpha1_RoleBindingspecRoleRef(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingspecSubject": schema_pkg_apis_iam_v0alpha1_RoleBindingspecSubject(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_RoleBindingstatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleList": schema_pkg_apis_iam_v0alpha1_RoleList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleSpec": schema_pkg_apis_iam_v0alpha1_RoleSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleStatus": schema_pkg_apis_iam_v0alpha1_RoleStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RolespecPermission": schema_pkg_apis_iam_v0alpha1_RolespecPermission(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_RolestatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccount": schema_pkg_apis_iam_v0alpha1_ServiceAccount(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountList": schema_pkg_apis_iam_v0alpha1_ServiceAccountList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountSpec": schema_pkg_apis_iam_v0alpha1_ServiceAccountSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountStatus": schema_pkg_apis_iam_v0alpha1_ServiceAccountStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountstatusOperatorState": schema_pkg_apis_iam_v0alpha1_ServiceAccountstatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Team": schema_pkg_apis_iam_v0alpha1_Team(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBinding": schema_pkg_apis_iam_v0alpha1_TeamBinding(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingList": schema_pkg_apis_iam_v0alpha1_TeamBindingList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec": schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingStatus": schema_pkg_apis_iam_v0alpha1_TeamBindingStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingTeamRef": schema_pkg_apis_iam_v0alpha1_TeamBindingTeamRef(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject": schema_pkg_apis_iam_v0alpha1_TeamBindingspecSubject(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamBindingstatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamList": schema_pkg_apis_iam_v0alpha1_TeamList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec": schema_pkg_apis_iam_v0alpha1_TeamSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus": schema_pkg_apis_iam_v0alpha1_TeamStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamstatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.User": schema_pkg_apis_iam_v0alpha1_User(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserList": schema_pkg_apis_iam_v0alpha1_UserList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec": schema_pkg_apis_iam_v0alpha1_UserSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus": schema_pkg_apis_iam_v0alpha1_UserStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState": schema_pkg_apis_iam_v0alpha1_UserstatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping(ref), } } @@ -491,6 +494,76 @@ func schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingTeamRef(ref common.Referen } } +func schema_pkg_apis_iam_v0alpha1_GetGroups(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_GetGroupsBody(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping"}, + } +} + func schema_pkg_apis_iam_v0alpha1_GlobalRole(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -2856,3 +2929,30 @@ func schema_pkg_apis_iam_v0alpha1_UserstatusOperatorState(ref common.ReferenceCa }, } } + +func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "externalGroup": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "externalGroup"}, + }, + }, + } +} diff --git a/apps/iam/pkg/apis/iam_manifest.go b/apps/iam/pkg/apis/iam_manifest.go index 388c0dca609..9fe171525b4 100644 --- a/apps/iam/pkg/apis/iam_manifest.go +++ b/apps/iam/pkg/apis/iam_manifest.go @@ -81,6 +81,58 @@ var appManifestData = app.ManifestData{ Plural: "Teams", Scope: "Namespaced", Conversion: false, + Routes: map[string]spec3.PathProps{ + "/groups": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getGroups", + + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + }, + }, + }, + Required: []string{ + "items", + "apiVersion", + "kind", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, }, { @@ -142,7 +194,9 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist return goType, exists } -var customRouteToGoResponseType = map[string]any{} +var customRouteToGoResponseType = map[string]any{ + "v0alpha1|Team|groups|GET": v0alpha1.GetGroups{}, +} // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. // kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. diff --git a/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts index 851680b4be4..88ce5985631 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts @@ -561,6 +561,10 @@ const injectedRtkApi = api }), invalidatesTags: ['Team'], }), + getTeamGroups: build.query({ + query: (queryArg) => ({ url: `/teams/${queryArg.name}/groups` }), + providesTags: ['Team'], + }), getTeamMembers: build.query({ query: (queryArg) => ({ url: `/teams/${queryArg.name}/members` }), providesTags: ['Team'], @@ -1461,6 +1465,11 @@ export type UpdateTeamApiArg = { force?: boolean; patch: Patch; }; +export type GetTeamGroupsApiResponse = /** status 200 OK */ GetGroups; +export type GetTeamGroupsApiArg = { + /** name of the GetGroups */ + name: string; +}; export type GetTeamMembersApiResponse = /** status 200 OK */ TeamMemberList; export type GetTeamMembersApiArg = { /** name of the TeamMemberList */ @@ -1978,6 +1987,17 @@ export type TeamList = { kind?: string; metadata: ListMeta; }; +export type VersionsV0Alpha1Kinds7RoutesGroupsGetResponseExternalGroupMapping = { + externalGroup: string; + name: string; +}; +export type GetGroups = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: VersionsV0Alpha1Kinds7RoutesGroupsGetResponseExternalGroupMapping[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; +}; export type TeamMember = { /** AvatarURL is the url where we can get the avatar for identity */ avatarURL?: string; @@ -2100,6 +2120,8 @@ export const { useReplaceTeamMutation, useDeleteTeamMutation, useUpdateTeamMutation, + useGetTeamGroupsQuery, + useLazyGetTeamGroupsQuery, useGetTeamMembersQuery, useLazyGetTeamMembersQuery, useListUserQuery, diff --git a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts index b9be8da0043..45ff0d1e91e 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts @@ -6039,6 +6039,7 @@ export type TeamGroupDto = { groupId?: string; orgId?: number; teamId?: number; + uid?: string; }; export type TeamGroupMapping = { groupId?: string; diff --git a/pkg/registry/apis/iam/externalgroupmapping/models.go b/pkg/registry/apis/iam/externalgroupmapping/models.go new file mode 100644 index 00000000000..4a8f2e5df08 --- /dev/null +++ b/pkg/registry/apis/iam/externalgroupmapping/models.go @@ -0,0 +1,10 @@ +package externalgroupmapping + +import "k8s.io/apiserver/pkg/registry/rest" + +type TeamGroupsHandler interface { + rest.Storage + rest.Scoper + rest.StorageMetadata + rest.Connecter +} diff --git a/pkg/registry/apis/iam/externalgroupmapping/team_groups_noop.go b/pkg/registry/apis/iam/externalgroupmapping/team_groups_noop.go new file mode 100644 index 00000000000..9333c50f666 --- /dev/null +++ b/pkg/registry/apis/iam/externalgroupmapping/team_groups_noop.go @@ -0,0 +1,58 @@ +package externalgroupmapping + +import ( + "context" + "net/http" + + iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" +) + +var _ TeamGroupsHandler = (*NoopTeamGroupsREST)(nil) + +type NoopTeamGroupsREST struct{} + +func ProvideNoopTeamGroupsREST() *NoopTeamGroupsREST { + return &NoopTeamGroupsREST{} +} + +// Connect implements rest.Connecter. +func (n *NoopTeamGroupsREST) Connect(ctx context.Context, id string, options runtime.Object, r rest.Responder) (http.Handler, error) { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "functionality not available", http.StatusForbidden) + }), nil +} + +// New implements rest.Storage. +func (s *NoopTeamGroupsREST) New() runtime.Object { + return iamv0alpha1.NewGetGroups() +} + +// Destroy implements rest.Storage. +func (s *NoopTeamGroupsREST) Destroy() {} + +// NamespaceScoped implements rest.Scoper. +func (s *NoopTeamGroupsREST) NamespaceScoped() bool { + return true +} + +// ProducesMIMETypes implements rest.StorageMetadata. +func (s *NoopTeamGroupsREST) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +// ProducesObject implements rest.StorageMetadata. +func (s *NoopTeamGroupsREST) ProducesObject(verb string) interface{} { + return s.New() +} + +// NewConnectOptions implements rest.Connecter. +func (s *NoopTeamGroupsREST) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +// ConnectMethods implements rest.Connecter. +func (s *NoopTeamGroupsREST) ConnectMethods() []string { + return []string{http.MethodGet} +} diff --git a/pkg/registry/apis/iam/models.go b/pkg/registry/apis/iam/models.go index 309b266ccb4..35efb2f2155 100644 --- a/pkg/registry/apis/iam/models.go +++ b/pkg/registry/apis/iam/models.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/registry/apis/iam/externalgroupmapping" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount" "github.com/grafana/grafana/pkg/registry/apis/iam/sso" @@ -80,6 +81,8 @@ type IdentityAccessManagementAPIBuilder struct { unified resource.ResourceClient userSearchClient resourcepb.ResourceIndexClient + teamGroupsHandler externalgroupmapping.TeamGroupsHandler + // non-k8s api route display *user.LegacyDisplayREST diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 2f214fd0630..a464568c12f 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -65,6 +65,7 @@ func RegisterAPIService( rolesStorage RoleStorageBackend, roleBindingsStorage RoleBindingStorageBackend, externalGroupMappingStorageBackend ExternalGroupMappingStorageBackend, + teamGroupsHandlerImpl externalgroupmapping.TeamGroupsHandler, dual dualwrite.Service, unified resource.ResourceClient, userService legacyuser.Service, @@ -90,6 +91,7 @@ func RegisterAPIService( resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider), roleBindingsStorage: roleBindingsStorage, externalGroupMappingStorage: externalGroupMappingStorageBackend, + teamGroupsHandler: teamGroupsHandlerImpl, sso: ssoService, authorizer: authorizer, legacyAccessClient: legacyAccessClient, @@ -102,7 +104,8 @@ func RegisterAPIService( features: features, dual: dual, unified: unified, - userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), unified, user.NewUserLegacySearchClient(userService), features), + userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), + unified, user.NewUserLegacySearchClient(userService), features), } apiregistration.RegisterAPI(builder) @@ -216,6 +219,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge } storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store) + storage[teamResource.StoragePath("groups")] = b.teamGroupsHandler teamBindingResource := iamv0.TeamBindingResourceInfo teamBindingUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamBindingResource, opts.OptsGetter) diff --git a/pkg/registry/apis/iam/user/legacy_search.go b/pkg/registry/apis/iam/user/legacy_search.go index 7459e7074e4..534a4f016eb 100644 --- a/pkg/registry/apis/iam/user/legacy_search.go +++ b/pkg/registry/apis/iam/user/legacy_search.go @@ -20,6 +20,8 @@ const ( UserResourceGroup = "iam.grafana.com" ) +var _ resourcepb.ResourceIndexClient = (*UserLegacySearchClient)(nil) + // UserLegacySearchClient is a client for searching for users in the legacy search engine. type UserLegacySearchClient struct { resourcepb.ResourceIndexClient @@ -129,9 +131,9 @@ func getColumns(fields []string) []*resourcepb.ResourceTableColumnDefinition { for _, field := range fields { switch field { case "email": - columns = append(columns, builders.TableColumnDefinitions[builders.USER_EMAIL]) + columns = append(columns, builders.UserTableColumnDefinitions[builders.USER_EMAIL]) case "login": - columns = append(columns, builders.TableColumnDefinitions[builders.USER_LOGIN]) + columns = append(columns, builders.UserTableColumnDefinitions[builders.USER_LOGIN]) } } return columns diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 5d76f64dfcd..d19b405c6c9 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/datasource" "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/registry/apis/iam" + "github.com/grafana/grafana/pkg/registry/apis/iam/externalgroupmapping" "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/ofrep" "github.com/grafana/grafana/pkg/registry/apis/preferences" @@ -30,6 +31,9 @@ var WireSetExts = wire.NewSet( wire.Bind(new(iam.RoleStorageBackend), new(*noopstorage.StorageBackendImpl)), wire.Bind(new(iam.RoleBindingStorageBackend), new(*noopstorage.StorageBackendImpl)), wire.Bind(new(iam.ExternalGroupMappingStorageBackend), new(*noopstorage.StorageBackendImpl)), + + externalgroupmapping.ProvideNoopTeamGroupsREST, + wire.Bind(new(externalgroupmapping.TeamGroupsHandler), new(*externalgroupmapping.NoopTeamGroupsREST)), ) var provisioningExtras = wire.NewSet( diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 9d4dcab2f7e..279a67f4134 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -55,6 +55,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/datasource" "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/registry/apis/iam" + "github.com/grafana/grafana/pkg/registry/apis/iam/externalgroupmapping" "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/ofrep" "github.com/grafana/grafana/pkg/registry/apis/preferences" @@ -868,7 +869,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, accessClient, registerer, resourceClient, zanzanaClient) storageBackendImpl := noopstorage.ProvideStorageBackend() - identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl, storageBackendImpl, dualwriteService, resourceClient, userService) + noopTeamGroupsREST := externalgroupmapping.ProvideNoopTeamGroupsREST() + identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl, storageBackendImpl, noopTeamGroupsREST, dualwriteService, resourceClient, userService) if err != nil { return nil, err } @@ -1517,7 +1519,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, accessClient, registerer, resourceClient, zanzanaClient) storageBackendImpl := noopstorage.ProvideStorageBackend() - identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl, storageBackendImpl, dualwriteService, resourceClient, userService) + noopTeamGroupsREST := externalgroupmapping.ProvideNoopTeamGroupsREST() + identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl, storageBackendImpl, noopTeamGroupsREST, dualwriteService, resourceClient, userService) if err != nil { return nil, err } diff --git a/pkg/storage/unified/search/builders/document.go b/pkg/storage/unified/search/builders/document.go index 24226cdcc9a..675f95b4b78 100644 --- a/pkg/storage/unified/search/builders/document.go +++ b/pkg/storage/unified/search/builders/document.go @@ -62,5 +62,10 @@ func All(sql db.DB, sprinkles DashboardStats) ([]resource.DocumentBuilderInfo, e return nil, err } - return []resource.DocumentBuilderInfo{dashboards, users}, nil + extGroupMappings, err := GetExternalGroupMappingBuilder() + if err != nil { + return nil, err + } + + return []resource.DocumentBuilderInfo{dashboards, users, extGroupMappings}, nil } diff --git a/pkg/storage/unified/search/builders/document_test.go b/pkg/storage/unified/search/builders/document_test.go index 59c3302c105..bee719aeefa 100644 --- a/pkg/storage/unified/search/builders/document_test.go +++ b/pkg/storage/unified/search/builders/document_test.go @@ -58,6 +58,18 @@ func TestUserDocumentBuilder(t *testing.T) { }) } +func TestExternalGroupMappingDocumentBuilder(t *testing.T) { + info, err := GetExternalGroupMappingBuilder() + require.NoError(t, err) + doSnapshotTests(t, info.Builder, "external_group_mapping", &resourcepb.ResourceKey{ + Namespace: "default", + Group: "iam.grafana.app", + Resource: "externalgroupmappings", + }, []string{ + "mapping-with-team-and-group", + }) +} + func TestDashboardDocumentBuilder(t *testing.T) { key := &resourcepb.ResourceKey{ Namespace: "default", diff --git a/pkg/storage/unified/search/builders/external_group_mapping.go b/pkg/storage/unified/search/builders/external_group_mapping.go new file mode 100644 index 00000000000..809075c2ec2 --- /dev/null +++ b/pkg/storage/unified/search/builders/external_group_mapping.go @@ -0,0 +1,80 @@ +package builders + +import ( + "bytes" + "context" + "encoding/json" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" +) + +const ( + EXTERNAL_GROUP_MAPPING_TEAM = "team" + EXTERNAL_GROUP_MAPPING_EXTERNAL_GROUP = "external_group" +) + +var ExternalGroupMappingTableColumnDefinitions = map[string]*resourcepb.ResourceTableColumnDefinition{ + EXTERNAL_GROUP_MAPPING_TEAM: { + Name: EXTERNAL_GROUP_MAPPING_TEAM, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + Description: "The team name associated with the external group mapping", + Properties: &resourcepb.ResourceTableColumnDefinition_Properties{ + Filterable: true, + }, + }, + EXTERNAL_GROUP_MAPPING_EXTERNAL_GROUP: { + Name: EXTERNAL_GROUP_MAPPING_EXTERNAL_GROUP, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + Description: "The external group name/id associated with the external group mapping", + Properties: &resourcepb.ResourceTableColumnDefinition_Properties{ + Filterable: true, + }, + }, +} + +func GetExternalGroupMappingBuilder() (resource.DocumentBuilderInfo, error) { + values := make([]*resourcepb.ResourceTableColumnDefinition, 0, len(ExternalGroupMappingTableColumnDefinitions)) + for _, v := range ExternalGroupMappingTableColumnDefinitions { + values = append(values, v) + } + + fields, err := resource.NewSearchableDocumentFields(values) + return resource.DocumentBuilderInfo{ + GroupResource: iamv0.ExternalGroupMappingResourceInfo.GroupResource(), + Fields: fields, + Builder: new(extGroupMappingDocumentBuilder), + }, err +} + +var _ resource.DocumentBuilder = new(extGroupMappingDocumentBuilder) + +type extGroupMappingDocumentBuilder struct{} + +// BuildDocument implements resource.DocumentBuilder. +func (u *extGroupMappingDocumentBuilder) BuildDocument(ctx context.Context, key *resourcepb.ResourceKey, rv int64, value []byte) (*resource.IndexableDocument, error) { + extGroupMapping := &iamv0.ExternalGroupMapping{} + err := json.NewDecoder(bytes.NewReader(value)).Decode(extGroupMapping) + if err != nil { + return nil, err + } + + obj, err := utils.MetaAccessor(extGroupMapping) + if err != nil { + return nil, err + } + + doc := resource.NewIndexableDocument(key, rv, obj) + + doc.Fields = make(map[string]any) + if extGroupMapping.Spec.TeamRef.Name != "" { + doc.Fields[EXTERNAL_GROUP_MAPPING_TEAM] = extGroupMapping.Spec.TeamRef.Name + } + if extGroupMapping.Spec.ExternalGroupId != "" { + doc.Fields[EXTERNAL_GROUP_MAPPING_EXTERNAL_GROUP] = extGroupMapping.Spec.ExternalGroupId + } + + return doc, nil +} diff --git a/pkg/storage/unified/search/builders/testdata/doc/external_group_mapping-mapping-with-team-and-group-out.json b/pkg/storage/unified/search/builders/testdata/doc/external_group_mapping-mapping-with-team-and-group-out.json new file mode 100644 index 00000000000..935ff7b34b7 --- /dev/null +++ b/pkg/storage/unified/search/builders/testdata/doc/external_group_mapping-mapping-with-team-and-group-out.json @@ -0,0 +1,17 @@ +{ + "key": { + "namespace": "default", + "group": "iam.grafana.app", + "resource": "externalgroupmappings", + "name": "mapping-with-team-and-group" + }, + "name": "mapping-with-team-and-group", + "rv": 1234, + "fields": { + "team": "team1", + "external_group": "group1" + }, + "title": "mapping-with-team-and-group", + "title_ngram": "mapping-with-team-and-group", + "title_phrase": "mapping-with-team-and-group" +} \ No newline at end of file diff --git a/pkg/storage/unified/search/builders/testdata/doc/external_group_mapping-mapping-with-team-and-group.json b/pkg/storage/unified/search/builders/testdata/doc/external_group_mapping-mapping-with-team-and-group.json new file mode 100644 index 00000000000..c34f8ae04ab --- /dev/null +++ b/pkg/storage/unified/search/builders/testdata/doc/external_group_mapping-mapping-with-team-and-group.json @@ -0,0 +1,14 @@ +{ + "apiVersion": "iam.grafana.app/v0alpha1", + "kind": "ExternalGroupMapping", + "metadata": { + "name": "mapping-with-team-and-group", + "namespace": "default" + }, + "spec": { + "teamRef": { + "name": "team1" + }, + "externalGroupId": "group1" + } +} diff --git a/pkg/storage/unified/search/builders/user.go b/pkg/storage/unified/search/builders/user.go index fdf1fce25e1..b17cdb99b15 100644 --- a/pkg/storage/unified/search/builders/user.go +++ b/pkg/storage/unified/search/builders/user.go @@ -16,7 +16,7 @@ const ( USER_LOGIN = "login" ) -var TableColumnDefinitions = map[string]*resourcepb.ResourceTableColumnDefinition{ +var UserTableColumnDefinitions = map[string]*resourcepb.ResourceTableColumnDefinition{ USER_EMAIL: { Name: USER_EMAIL, Type: resourcepb.ResourceTableColumnDefinition_STRING, @@ -38,8 +38,8 @@ var TableColumnDefinitions = map[string]*resourcepb.ResourceTableColumnDefinitio } func GetUserBuilder() (resource.DocumentBuilderInfo, error) { - values := make([]*resourcepb.ResourceTableColumnDefinition, 0, len(TableColumnDefinitions)) - for _, v := range TableColumnDefinitions { + values := make([]*resourcepb.ResourceTableColumnDefinition, 0, len(UserTableColumnDefinitions)) + for _, v := range UserTableColumnDefinitions { values = append(values, v) } fields, err := resource.NewSearchableDocumentFields(values) diff --git a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json index d0ac7bbfff3..8449498a061 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -4001,6 +4001,55 @@ } ] }, + "/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/teams/{name}/groups": { + "get": { + "tags": [ + "Team" + ], + "description": "connect GET requests to groups of Team", + "operationId": "getTeamGroups", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.GetGroups" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "iam.grafana.app", + "version": "v0alpha1", + "kind": "GetGroups" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the GetGroups", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, "/apis/iam.grafana.app/v0alpha1/namespaces/{namespace}/teams/{name}/members": { "get": { "tags": [ @@ -5050,6 +5099,40 @@ } } }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.GetGroups": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "iam.grafana.app", + "kind": "GetGroups", + "version": "v0alpha1" + } + ] + }, "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccount": { "type": "object", "required": [ @@ -5549,6 +5632,23 @@ } } }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping": { + "type": "object", + "required": [ + "name", + "externalGroup" + ], + "properties": { + "externalGroup": { + "type": "string", + "default": "" + }, + "name": { + "type": "string", + "default": "" + } + } + }, "com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured": { "type": "object", "additionalProperties": true, @@ -6237,6 +6337,42 @@ } } }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetGroups": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetGroupsBody": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "default": {} + } + } + } + }, "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": { "type": "object", "required": [ @@ -7573,6 +7709,23 @@ } } }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping": { + "type": "object", + "required": [ + "name", + "externalGroup" + ], + "properties": { + "externalGroup": { + "type": "string", + "default": "" + }, + "name": { + "type": "string", + "default": "" + } + } + }, "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { "description": "APIResource specifies the name of a resource and whether it is namespaced.", "type": "object", diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index a7dfb2004ec..c976dcb9e7d 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -7967,6 +7967,9 @@ "teamId": { "type": "integer", "format": "int64" + }, + "uid": { + "type": "string" } } }, diff --git a/public/api-merged.json b/public/api-merged.json index ace7476c7d5..0d9c4129adc 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -22070,6 +22070,9 @@ "teamId": { "type": "integer", "format": "int64" + }, + "uid": { + "type": "string" } } }, diff --git a/public/openapi3.json b/public/openapi3.json index eaa4082ab48..20b97b3f14d 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -11609,6 +11609,9 @@ "teamId": { "format": "int64", "type": "integer" + }, + "uid": { + "type": "string" } }, "type": "object" From 2e524483ac844ac989366f5c4a45d9e1758a0c69 Mon Sep 17 00:00:00 2001 From: Misi Date: Tue, 25 Nov 2025 14:21:23 +0100 Subject: [PATCH 106/378] IAM: Add kubernetesExternalGroupMapping feature toggle (#114386) Add kubernetesExternalGroupMapping ft --- .../grafana-data/src/types/featureToggles.gen.ts | 4 ++++ pkg/services/featuremgmt/registry.go | 7 +++++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++++ pkg/services/featuremgmt/toggles_gen.json | 16 ++++++++++++++++ 5 files changed, 32 insertions(+) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 509beeb56c9..0d20e44b19e 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -963,6 +963,10 @@ export interface FeatureToggles { */ kubernetesAuthnMutation?: boolean; /** + * Routes external group mapping requests from /api to the /apis endpoint + */ + kubernetesExternalGroupMapping?: boolean; + /** * Enables restore deleted dashboards feature * @default false */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 24e057cd882..65f94c24eac 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1589,6 +1589,13 @@ var ( Owner: identityAccessTeam, HideFromDocs: true, }, + { + Name: "kubernetesExternalGroupMapping", + Description: "Routes external group mapping requests from /api to the /apis endpoint", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, + }, { Name: "restoreDashboards", Description: "Enables restore deleted dashboards feature", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index fd1fe6619f9..17fdf27f33d 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -218,6 +218,7 @@ kubernetesAuthZHandlerRedirect,experimental,@grafana/identity-access-team,false, kubernetesAuthzResourcePermissionApis,experimental,@grafana/identity-access-team,false,false,false kubernetesAuthzZanzanaSync,experimental,@grafana/identity-access-team,false,false,false kubernetesAuthnMutation,experimental,@grafana/identity-access-team,false,false,false +kubernetesExternalGroupMapping,experimental,@grafana/identity-access-team,false,false,false restoreDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,false alertEnrichment,experimental,@grafana/alerting-squad,false,false,false alertEnrichmentMultiStep,experimental,@grafana/alerting-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index f68009ab2e3..b88204db708 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -638,6 +638,10 @@ const ( // Enables create, delete, and update mutations for resources owned by IAM identity FlagKubernetesAuthnMutation = "kubernetesAuthnMutation" + // FlagKubernetesExternalGroupMapping + // Routes external group mapping requests from /api to the /apis endpoint + FlagKubernetesExternalGroupMapping = "kubernetesExternalGroupMapping" + // FlagRestoreDashboards // Enables restore deleted dashboards feature FlagRestoreDashboards = "restoreDashboards" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 0e7dba8b5ec..1093b643232 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1910,6 +1910,22 @@ "expression": "true" } }, + { + "metadata": { + "name": "kubernetesExternalGroupMapping", + "resourceVersion": "1764062806326", + "creationTimestamp": "2025-11-25T09:17:49Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-25 09:26:46.326986 +0000 UTC" + } + }, + "spec": { + "description": "Routes external group mapping requests from /api to the /apis endpoint", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team", + "hideFromDocs": true + } + }, { "metadata": { "name": "kubernetesFeatureToggles", From c42d6a53a6f7fcb41f8a9ac9d4778fcc801f0578 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 25 Nov 2025 14:33:50 +0100 Subject: [PATCH 107/378] Logs: Fixed prettify JSON behavior with unescaped content (#114403) * processing: move escaping to a later stage * Regression test --- .../logs/components/panel/processing.test.ts | 39 +++++++++++++++++++ .../logs/components/panel/processing.ts | 9 +++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/public/app/features/logs/components/panel/processing.test.ts b/public/app/features/logs/components/panel/processing.test.ts index 2b58efeee15..5998ff36d21 100644 --- a/public/app/features/logs/components/panel/processing.test.ts +++ b/public/app/features/logs/components/panel/processing.test.ts @@ -190,6 +190,45 @@ describe('preProcessLogs', () => { expect(logListModel.body).not.toBe(entry); }); + test('Prettifies and escapes wrapped JSON', () => { + const entry = '{"key": "value", "otherKey": "other\\nValue"}'; + const logListModel = createLogLine( + { entry, hasUnescapedContent: true }, + { + escape: true, // escape + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrapLogMessage: true, // wrapped + prettifyJSON: true, + } + ); + expect(logListModel.entry).toBe(entry); + expect(logListModel.body).toBe(`{ + "key": "value", + "otherKey": "other +Value" +}`); + }); + + test('Prettifies JSON without escaping', () => { + const entry = '{"key": "value", "otherKey": "other\\nValue"}'; + const logListModel = createLogLine( + { entry, hasUnescapedContent: true }, + { + escape: false, // escape = false + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrapLogMessage: true, // wrapped + prettifyJSON: true, + } + ); + expect(logListModel.entry).toBe(entry); + expect(logListModel.body).toBe(`{ + "key": "value", + "otherKey": "other\\nValue" +}`); + }); + test('Uses lossless parsing', () => { const entry = '{"number": 90071992547409911}'; const logListModel = createLogLine( diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 21324f942ef..0f3ea0ac6b1 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -62,6 +62,7 @@ export class LogListModel implements LogRowModel { private _highlightedBody: string | undefined = undefined; private _highlightedLogAttributesTokens: Array | undefined = undefined; private _highlightTokens: Array | undefined = undefined; + private _escapeUnescapedString = false; private _fields: FieldDef[] | undefined = undefined; private _getFieldLinks: GetFieldLinksFn | undefined = undefined; private _prettifyJSON: boolean; @@ -111,11 +112,10 @@ export class LogListModel implements LogRowModel { this._virtualization = virtualization; this._wrapLogMessage = wrapLogMessage; - let raw = log.raw; if (escape && log.hasUnescapedContent) { - raw = escapeUnescapedString(raw); + this._escapeUnescapedString = true; } - this.raw = raw; + this.raw = log.raw; if (config.featureToggles.otelLogsFormatting && this.otelLanguage) { this.labels[OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME] = getOtelAttributesField(this, wrapLogMessage); @@ -143,6 +143,9 @@ export class LogListModel implements LogRowModel { if (reStringified) { this.raw = reStringified; } + if (this._escapeUnescapedString) { + this.raw = escapeUnescapedString(this.raw); + } } catch (error) {} const raw = this.raw; this._body = this.collapsed From 82b81c2b75b60eee02de067a08c14aacb1762cce Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Tue, 25 Nov 2025 09:48:57 -0500 Subject: [PATCH 108/378] Rudderstack Event: When click dashboard list item, add uid to rudderstack event (#114374) NameCell: add uid to rudderstack event --- public/app/features/browse-dashboards/components/NameCell.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/browse-dashboards/components/NameCell.tsx b/public/app/features/browse-dashboards/components/NameCell.tsx index 4ee8ceba594..0049fb8f829 100644 --- a/public/app/features/browse-dashboards/components/NameCell.tsx +++ b/public/app/features/browse-dashboards/components/NameCell.tsx @@ -97,6 +97,7 @@ export function NameCell({ row: { original: data }, onFolderClick, treeID }: Nam itemKind: item.kind, parent: item.parentUID ? 'folder' : 'root', source: 'browseDashboardsPage_BrowseView', + uid: item.uid, }); }} href={item.url} From 5694d6371ee9989409f7259295b1cf821cd1f022 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Tue, 25 Nov 2025 15:46:07 +0000 Subject: [PATCH 109/378] Plugins: Upgrade `github.com/grafana/grafana-plugin-sdk-go` v0.283.0 => v0.284.0 (#114400) * upgraded github.com/grafana/grafana-plugin-sdk-go v0.283.0 => v0.284.0 * make update-workspace * apply fixes * nolint for FieldTypeNullableJSON instead * fmt * fix lint issues --- apps/advisor/go.mod | 2 +- apps/advisor/go.sum | 4 ++-- apps/dashboard/go.mod | 2 +- apps/dashboard/go.sum | 4 ++-- apps/iam/go.mod | 2 +- apps/iam/go.sum | 4 ++-- apps/plugins/go.mod | 2 +- apps/plugins/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- go.work.sum | 9 +++++++++ pkg/aggregator/go.mod | 2 +- pkg/aggregator/go.sum | 4 ++-- pkg/expr/sql/frame_db_conv.go | 2 +- pkg/expr/sql/frame_table.go | 2 +- pkg/promlib/go.mod | 2 +- pkg/promlib/go.sum | 4 ++-- pkg/services/searchV2/bluge.go | 4 ++-- .../loganalytics/azure-response-table-frame.go | 2 +- pkg/tsdb/azuremonitor/metrics/url-builder.go | 8 ++++---- .../azure-resource-graph-datasource.go | 11 +++++++---- pkg/tsdb/azuremonitor/time/time-grain.go | 4 ++-- pkg/tsdb/azuremonitor/utils/utils.go | 15 +++++++++------ pkg/tsdb/elasticsearch/response_parser_test.go | 8 ++++---- .../sqleng/sql_engine.go | 4 ++-- pkg/tsdb/graphite/query.go | 7 +++++-- pkg/tsdb/influxdb/influxql/converter/converter.go | 12 ++++++------ .../influxdb/influxql/converter/converter_test.go | 12 ++++++------ pkg/tsdb/mssql/sqleng/sql_engine.go | 2 +- pkg/tsdb/mysql/sqleng/sql_engine.go | 2 +- 30 files changed, 82 insertions(+), 64 deletions(-) diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 6611470fb8f..6e944d56c3b 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -10,7 +10,7 @@ require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 github.com/grafana/grafana-app-sdk v0.48.2 github.com/grafana/grafana-app-sdk/logging v0.48.1 - github.com/grafana/grafana-plugin-sdk-go v0.283.0 + github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 79aea07d576..61d3d00b8c2 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -624,8 +624,8 @@ github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc= -github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= +github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index b281b21e4e4..e5a1352e7d8 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -7,7 +7,7 @@ require ( github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana-app-sdk v0.48.2 github.com/grafana/grafana-app-sdk/logging v0.48.1 - github.com/grafana/grafana-plugin-sdk-go v0.283.0 + github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index c0efb792788..135eec59afc 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -89,8 +89,8 @@ github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJH github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= -github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= +github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 73891acd454..ad6c6039cc5 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -225,7 +225,7 @@ require ( github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect github.com/grafana/grafana-aws-sdk v1.3.0 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect - github.com/grafana/grafana-plugin-sdk-go v0.283.0 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect github.com/grafana/grafana/apps/dashboard v0.0.0 // indirect github.com/grafana/grafana/apps/plugins v0.0.0 // indirect github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 4cba06c00ec..c91c571fa7d 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -847,8 +847,8 @@ github.com/grafana/grafana-google-sdk-go v0.4.2 h1:F44hQF1y6UVJhlJPi+Mz+GCJsioVg github.com/grafana/grafana-google-sdk-go v0.4.2/go.mod h1:U73+w9DlbEtUonhQUzERwlXnzWTtfRoyrtKH8d3VY40= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= -github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= +github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index d2b2f2e8045..ef1b58517a3 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -74,7 +74,7 @@ require ( github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect github.com/grafana/grafana-aws-sdk v1.3.0 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect - github.com/grafana/grafana-plugin-sdk-go v0.283.0 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index 1c479c59942..06683624e65 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -169,8 +169,8 @@ github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc= -github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= +github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= diff --git a/go.mod b/go.mod index 936cf97655b..e1ed6dddd56 100644 --- a/go.mod +++ b/go.mod @@ -104,7 +104,7 @@ require ( github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.4.2 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group - github.com/grafana/grafana-plugin-sdk-go v0.283.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.284.0 // @grafana/plugins-platform-backend github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 // @grafana/alerting-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs github.com/grafana/nanogit v0.3.0 // indirect; @grafana/grafana-git-ui-sync-team diff --git a/go.sum b/go.sum index f7c4c8e5578..971b7b06f74 100644 --- a/go.sum +++ b/go.sum @@ -1647,8 +1647,8 @@ github.com/grafana/grafana-google-sdk-go v0.4.2 h1:F44hQF1y6UVJhlJPi+Mz+GCJsioVg github.com/grafana/grafana-google-sdk-go v0.4.2/go.mod h1:U73+w9DlbEtUonhQUzERwlXnzWTtfRoyrtKH8d3VY40= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= -github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= +github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= diff --git a/go.work.sum b/go.work.sum index 72f5dd7e97d..c4b732eaadb 100644 --- a/go.work.sum +++ b/go.work.sum @@ -896,6 +896,7 @@ github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagI github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= github.com/grafana/grafana-plugin-sdk-go v0.279.0/go.mod h1:/7oGN6Z7DGTGaLHhgIYrRr6Wvmdsb3BLw5hL4Kbjy88= github.com/grafana/grafana-plugin-sdk-go v0.280.0/go.mod h1:Z15Wiq3c4I0tzHYrLYpOqrO8u3+2RJ+HN2Q9uiZTILA= +github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= github.com/grafana/grafana/apps/advisor v0.0.0-20250220154326-6e5de80ef295/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d/go.mod h1:AvleS6icyPmcBjihtx5jYEvdzLmHGBp66NuE0AMR57A= @@ -1323,6 +1324,7 @@ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkq github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8= github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ= @@ -1862,6 +1864,7 @@ golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632 golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= @@ -1890,6 +1893,7 @@ golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= @@ -1912,6 +1916,7 @@ golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -1920,6 +1925,7 @@ golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= @@ -1948,6 +1954,7 @@ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1967,6 +1974,7 @@ golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= @@ -1989,6 +1997,7 @@ golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index 32ba2addf5e..24f0d25efae 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -4,7 +4,7 @@ go 1.25.3 require ( github.com/emicklei/go-restful/v3 v3.13.0 - github.com/grafana/grafana-plugin-sdk-go v0.283.0 + github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 2f44039e35c..313c0b7c200 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -101,8 +101,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= +github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e h1:vheR6iPO1np+G/ARjcWx9yiWd7BnTDgyTgsnMhOvx70= diff --git a/pkg/expr/sql/frame_db_conv.go b/pkg/expr/sql/frame_db_conv.go index fe94ae79266..451b7c4f9a8 100644 --- a/pkg/expr/sql/frame_db_conv.go +++ b/pkg/expr/sql/frame_db_conv.go @@ -187,7 +187,7 @@ func fieldValFromRowVal(fieldType data.FieldType, val interface{}) (interface{}, case data.FieldTypeBool, data.FieldTypeNullableBool: return parseBoolFromInt8(val, nullable) - case data.FieldTypeJSON, data.FieldTypeNullableJSON: + case data.FieldTypeJSON, data.FieldTypeNullableJSON: //nolint:staticcheck switch v := val.(type) { case types.JSONDocument: raw := json.RawMessage(v.String()) diff --git a/pkg/expr/sql/frame_table.go b/pkg/expr/sql/frame_table.go index 245efc6e655..3e24457ef3b 100644 --- a/pkg/expr/sql/frame_table.go +++ b/pkg/expr/sql/frame_table.go @@ -182,7 +182,7 @@ func convertDataType(fieldType data.FieldType) mysql.Type { return types.Boolean case data.FieldTypeTime, data.FieldTypeNullableTime: return types.Timestamp - case data.FieldTypeJSON, data.FieldTypeNullableJSON: + case data.FieldTypeJSON, data.FieldTypeNullableJSON: //nolint:staticcheck return types.JSON default: fmt.Printf("------- Unsupported field type: %v", fieldType) diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 877b00ae2e4..314d19580a4 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -4,7 +4,7 @@ go 1.25.3 require ( github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 - github.com/grafana/grafana-plugin-sdk-go v0.283.0 + github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/apps/scope v0.0.0-20251007093103-792853df9134 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_golang v1.23.2 diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 066b92e7746..85d212bc772 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -130,8 +130,8 @@ github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81 github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= +github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/apps/scope v0.0.0-20251007093103-792853df9134 h1:xly75v5lFNR37q+wXnwA5yU/fPW9IOSYbhFpt4tQyt8= github.com/grafana/grafana/apps/scope v0.0.0-20251007093103-792853df9134/go.mod h1:zijsUNa1zi476JJIR2Lcm/Paz1nRCno9XCp6hbS6G9o= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251007081214-26e147d01f0a h1:L7xgV9mP6MRF3L2/vDOjNR7heaBPbXPMGTDN9/jXSFQ= diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go index d1fdeac5424..bc670677fb6 100644 --- a/pkg/services/searchV2/bluge.go +++ b/pkg/services/searchV2/bluge.go @@ -513,9 +513,9 @@ func doSearchQuery( fName := data.NewFieldFromFieldType(data.FieldTypeString, 0) fURL := data.NewFieldFromFieldType(data.FieldTypeString, 0) fLocation := data.NewFieldFromFieldType(data.FieldTypeString, 0) - fTags := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) + fTags := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) //nolint:staticcheck fDSUIDs := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) - fExplain := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) + fExplain := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) //nolint:staticcheck fScore.Name = "score" fUID.Name = "uid" diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go b/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go index 2181d832dcf..94fcad61819 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go @@ -138,7 +138,7 @@ type KeyValue struct { } var tagsConverter = data.FieldConverter{ - OutputFieldType: data.FieldTypeNullableJSON, + OutputFieldType: data.FieldTypeNullableJSON, //nolint:staticcheck Converter: func(v any) (any, error) { if v == nil { return nil, nil diff --git a/pkg/tsdb/azuremonitor/metrics/url-builder.go b/pkg/tsdb/azuremonitor/metrics/url-builder.go index 32b268eeca5..1f3ef37cabd 100644 --- a/pkg/tsdb/azuremonitor/metrics/url-builder.go +++ b/pkg/tsdb/azuremonitor/metrics/url-builder.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" + "github.com/grafana/grafana-plugin-sdk-go/backend" ) // urlBuilder builds the URL for calling the Azure Monitor API @@ -35,7 +35,7 @@ func (params *urlBuilder) buildResourceURI() (*string, error) { if metricNamespace == nil || *metricNamespace == "" { if params.MetricDefinition == nil || *params.MetricDefinition == "" { - return nil, errorsource.DownstreamError(fmt.Errorf("no metricNamespace or metricDefiniton value provided"), false) + return nil, backend.DownstreamError(fmt.Errorf("no metricNamespace or metricDefiniton value provided")) } metricNamespace = params.MetricDefinition } @@ -47,7 +47,7 @@ func (params *urlBuilder) buildResourceURI() (*string, error) { provider = metricNamespaceArray[0] metricNamespaceArray = metricNamespaceArray[1:] } else { - return nil, errorsource.DownstreamError(fmt.Errorf("metricNamespace is not in the correct format"), false) + return nil, backend.DownstreamError(fmt.Errorf("metricNamespace is not in the correct format")) } var resourceNameArray []string @@ -78,7 +78,7 @@ func (params *urlBuilder) buildResourceURI() (*string, error) { if i < len(resourceNameArray) { urlArray = append(urlArray, namespace, resourceNameArray[i]) } else { - return nil, errorsource.DownstreamError(fmt.Errorf("resourceNameArray does not have enough elements"), false) + return nil, backend.DownstreamError(fmt.Errorf("resourceNameArray does not have enough elements")) } } diff --git a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go index 699ee7055a5..a8d01db97d5 100644 --- a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go +++ b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -71,7 +70,7 @@ func (e *AzureResourceGraphDatasource) ExecuteTimeSeriesQuery(ctx context.Contex } res, err := e.executeQuery(ctx, graphQuery, dsInfo, client, url) if err != nil { - errorsource.AddErrorToResponse(query.RefID, result, err) + result.Responses[query.RefID] = backend.ErrorResponseWithErrorSource(err) continue } result.Responses[query.RefID] = *res @@ -158,7 +157,7 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query * res, err := client.Do(req) if err != nil { - return nil, errorsource.DownstreamError(err, false) + return nil, backend.DownstreamError(err) } defer func() { @@ -218,7 +217,11 @@ func (e *AzureResourceGraphDatasource) unmarshalResponse(res *http.Response) (Az }() if res.StatusCode/100 != 2 { - return AzureResourceGraphResponse{}, errorsource.SourceError(backend.ErrorSourceFromHTTPStatus(res.StatusCode), fmt.Errorf("%s. Azure Resource Graph error: %s", res.Status, string(body)), false) + err := fmt.Errorf("%s. Azure Resource Graph error: %s", res.Status, string(body)) + if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream { + return AzureResourceGraphResponse{}, backend.DownstreamError(err) + } + return AzureResourceGraphResponse{}, backend.PluginError(err) } var data AzureResourceGraphResponse diff --git a/pkg/tsdb/azuremonitor/time/time-grain.go b/pkg/tsdb/azuremonitor/time/time-grain.go index d89e59bbf96..bc8a02fabef 100644 --- a/pkg/tsdb/azuremonitor/time/time-grain.go +++ b/pkg/tsdb/azuremonitor/time/time-grain.go @@ -6,8 +6,8 @@ import ( "strings" "time" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" - "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" ) // TimeGrain handles conversions between @@ -27,7 +27,7 @@ func CreateISO8601DurationFromIntervalMS(it int64) (string, error) { timeValueString := formatted[0 : len(formatted)-1] timeValue, err := strconv.Atoi(timeValueString) if err != nil { - return "", errorsource.DownstreamError(fmt.Errorf("could not parse interval %q to an ISO 8061 duration: %w", it, err), false) + return "", backend.DownstreamError(fmt.Errorf("could not parse interval %q to an ISO 8061 duration: %w", it, err)) } unit := formatted[len(formatted)-1:] diff --git a/pkg/tsdb/azuremonitor/utils/utils.go b/pkg/tsdb/azuremonitor/utils/utils.go index fd36558886d..b87ee1a2e98 100644 --- a/pkg/tsdb/azuremonitor/utils/utils.go +++ b/pkg/tsdb/azuremonitor/utils/utils.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" ) @@ -28,7 +28,7 @@ func GetFirstSubscriptionOrDefault(ctx context.Context, dsInfo types.DatasourceI res, err := dsInfo.Services["Azure Monitor"].HTTPClient.Do(request) if err != nil { - return "", errorsource.DownstreamError(fmt.Errorf("failed to retrieve subscriptions: %v", err), false) + return "", backend.DownstreamError(fmt.Errorf("failed to retrieve subscriptions: %v", err)) } defer func() { if err := res.Body.Close(); err != nil { @@ -42,7 +42,7 @@ func GetFirstSubscriptionOrDefault(ctx context.Context, dsInfo types.DatasourceI } if len(subscriptions) == 0 { - return "", errorsource.DownstreamError(fmt.Errorf("no subscriptions found: %v", err), false) + return "", backend.DownstreamError(fmt.Errorf("no subscriptions found: %v", err)) } return subscriptions[0], nil @@ -73,9 +73,12 @@ func ParseSubscriptions(res *http.Response, logger log.Logger) ([]string, error) } func ApplySourceFromError(errorMessage error, err error) error { - var sourceError errorsource.Error - if errors.As(err, &sourceError) { - return errorsource.SourceError(sourceError.Source(), errorMessage, false) + var errorWithSource backend.ErrorWithSource + if errors.As(err, &errorWithSource) { + if errorWithSource.ErrorSource() == backend.ErrorSourcePlugin { + return backend.PluginError(errorMessage) + } + return backend.DownstreamError(errorMessage) } return errorMessage } diff --git a/pkg/tsdb/elasticsearch/response_parser_test.go b/pkg/tsdb/elasticsearch/response_parser_test.go index 0032796cadf..fe913953ed4 100644 --- a/pkg/tsdb/elasticsearch/response_parser_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_test.go @@ -355,7 +355,7 @@ func TestProcessLogsResponse(t *testing.T) { // Correctly detects float64 types require.Equal(t, data.FieldTypeNullableFloat64, frame.Fields[7].Type()) // Correctly detects json types - require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[8].Type()) + require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[8].Type()) //nolint:staticcheck // Correctly flattens fields require.Equal(t, "nested.field.double_nested", frame.Fields[13].Name) require.Equal(t, data.FieldTypeNullableString, frame.Fields[13].Type()) @@ -546,7 +546,7 @@ func TestProcessRawDataResponse(t *testing.T) { // Correctly detects float64 types require.Equal(t, data.FieldTypeNullableFloat64, frame.Fields[5].Type()) // Correctly detects json types - require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[6].Type()) + require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[6].Type()) //nolint:staticcheck // Correctly flattens fields require.Equal(t, "nested.field.double_nested", frame.Fields[11].Name) require.Equal(t, data.FieldTypeNullableString, frame.Fields[11].Type()) @@ -736,7 +736,7 @@ func TestProcessRawDocumentResponse(t *testing.T) { require.Len(t, fields, 1) f := fields[0] - require.Equal(t, data.FieldTypeNullableJSON, f.Type()) + require.Equal(t, data.FieldTypeNullableJSON, f.Type()) //nolint:staticcheck require.Equal(t, 2, f.Len()) v := f.At(0).(*json.RawMessage) @@ -829,7 +829,7 @@ func TestProcessRawDocumentResponse(t *testing.T) { //Fields have the correct length require.Equal(t, 2, frame.Fields[0].Len()) // The only field is the raw document - require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[0].Type()) + require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[0].Type()) //nolint:staticcheck require.Equal(t, "A", frame.Fields[0].Name) }) } diff --git a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go index 5a19ca859f4..61784b36e51 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go +++ b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go @@ -368,7 +368,7 @@ func (e *DataSourceHandler) processFrame(frame *data.Frame, qm *dataQueryModel, } var err error - frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) + frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) //nolint:staticcheck if err != nil { logger.Error("Failed to resample dataframe", "err", err) frame.AppendNotices(data.Notice{Text: "Failed to resample dataframe", Severity: data.NoticeSeverityWarning}) @@ -748,7 +748,7 @@ func getFieldTypesFromDescriptions(fieldDescriptions []pgconn.FieldDescription, case "timestamptz", "timestamp", "date": fieldTypes[i] = data.FieldTypeNullableTime case "json", "jsonb": - fieldTypes[i] = data.FieldTypeNullableJSON + fieldTypes[i] = data.FieldTypeNullableJSON //nolint:staticcheck default: fieldTypes[i] = data.FieldTypeNullableString } diff --git a/pkg/tsdb/graphite/query.go b/pkg/tsdb/graphite/query.go index 2ab60b21592..c06c6d5af08 100644 --- a/pkg/tsdb/graphite/query.go +++ b/pkg/tsdb/graphite/query.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "golang.org/x/net/html" @@ -250,7 +249,11 @@ func (s *Service) parseResponse(res *http.Response) ([]TargetResponseDTO, error) if res.StatusCode/100 != 2 { graphiteError := parseGraphiteError(res.StatusCode, string(body)) s.logger.Info("Request failed", "status", res.Status, "error", graphiteError, "body", string(body)) - return nil, errorsource.SourceError(backend.ErrorSourceFromHTTPStatus(res.StatusCode), fmt.Errorf("request failed with error: %s", graphiteError), false) + err := fmt.Errorf("request failed with error: %s", graphiteError) + if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream { + return nil, backend.DownstreamError(err) + } + return nil, backend.PluginError(err) } var data []TargetResponseDTO diff --git a/pkg/tsdb/influxdb/influxql/converter/converter.go b/pkg/tsdb/influxdb/influxql/converter/converter.go index b4c366bff51..6b338fb3207 100644 --- a/pkg/tsdb/influxdb/influxql/converter/converter.go +++ b/pkg/tsdb/influxdb/influxql/converter/converter.go @@ -151,7 +151,7 @@ func readSeries(iter *sdkjsoniter.Iterator, query *models.Query) *backend.DataRe } if util.GetVisType(query.ResultFormat) != util.TableVisType { for i, v := range valueFields { - if v.Type() == data.FieldTypeNullableJSON { + if v.Type() == data.FieldTypeNullableJSON { //nolint:staticcheck maybeFixValueFieldType(valueFields, data.FieldTypeNullableFloat64, i) } } @@ -189,7 +189,7 @@ func readSeries(iter *sdkjsoniter.Iterator, query *models.Query) *backend.DataRe // also frontend probably will not interpret the nullableJson value for i, f := range rsp.Frames { for j, v := range f.Fields { - if v.Type() == data.FieldTypeNullableJSON { + if v.Type() == data.FieldTypeNullableJSON { //nolint:staticcheck newField := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, 0) newField.Name = v.Name newField.Config = v.Config @@ -301,7 +301,7 @@ func readValues(iter *sdkjsoniter.Iterator, hasTimeColumn bool) (valueFields dat // we don't know the type of the values for this field, yet // so we create a FieldTypeNullableJSON to hold nil values // if that is something else it will be replaced later - unknownField := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) + unknownField := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) //nolint:staticcheck unknownField.Name = "Value" valueFields = append(valueFields, unknownField) } @@ -335,7 +335,7 @@ func maybeCreateValueField(valueFields data.Fields, expectedType data.FieldType, // or the type of the field in valueFields is nullableJSON // we change the type of the field as expectedType func maybeFixValueFieldType(valueFields data.Fields, expectedType data.FieldType, colIdx int) { - if valueFields[colIdx].Type() == expectedType || valueFields[colIdx].Type() != data.FieldTypeNullableJSON { + if valueFields[colIdx].Type() == expectedType || valueFields[colIdx].Type() != data.FieldTypeNullableJSON { //nolint:staticcheck return } stringField := data.NewFieldFromFieldType(expectedType, 0) @@ -364,7 +364,7 @@ func typeOf(value interface{}) data.FieldType { return data.FieldTypeNullableBool default: slog.Error("unknown influx value type", "value", v) - return data.FieldTypeNullableJSON + return data.FieldTypeNullableJSON //nolint:staticcheck } } @@ -471,7 +471,7 @@ func handleTableFormatValueFields(rsp *backend.DataResponse, valueFields data.Fi } else { ll := valueFields[i].Len() for vi := 0; vi < ll; vi++ { - if valueFields[i].Type() == data.FieldTypeNullableJSON { + if valueFields[i].Type() == data.FieldTypeNullableJSON { //nolint:staticcheck // add nil explicitly. // we don't know if it is a float pointer nil or string pointer nil or etc rsp.Frames[0].Fields[si].Append(nil) diff --git a/pkg/tsdb/influxdb/influxql/converter/converter_test.go b/pkg/tsdb/influxdb/influxql/converter/converter_test.go index 2c5e1deecd5..ac1561afcc2 100644 --- a/pkg/tsdb/influxdb/influxql/converter/converter_test.go +++ b/pkg/tsdb/influxdb/influxql/converter/converter_test.go @@ -38,28 +38,28 @@ func TestMaybeFixValueFieldType(t *testing.T) { }, { name: "should return nullableJson if both are nullableJson", - valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, - inputType: data.FieldTypeNullableJSON, + valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, //nolint:staticcheck + inputType: data.FieldTypeNullableJSON, //nolint:staticcheck colIdx: 0, - expectedType: data.FieldTypeNullableJSON, + expectedType: data.FieldTypeNullableJSON, //nolint:staticcheck }, { name: "should return nullableString if valueField is nullableJson and input is nullableString", - valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, + valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, //nolint:staticcheck inputType: data.FieldTypeNullableString, colIdx: 0, expectedType: data.FieldTypeNullableString, }, { name: "should return nullableBool if valueField is nullableJson and input is nullableBool", - valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, + valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, //nolint:staticcheck inputType: data.FieldTypeNullableBool, colIdx: 0, expectedType: data.FieldTypeNullableBool, }, { name: "should return nullableFloat64 if valueField is nullableJson and input is nullableFloat64", - valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, + valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, //nolint:staticcheck inputType: data.FieldTypeNullableFloat64, colIdx: 0, expectedType: data.FieldTypeNullableFloat64, diff --git a/pkg/tsdb/mssql/sqleng/sql_engine.go b/pkg/tsdb/mssql/sqleng/sql_engine.go index 80e3d29927c..3d27b0a9a01 100644 --- a/pkg/tsdb/mssql/sqleng/sql_engine.go +++ b/pkg/tsdb/mssql/sqleng/sql_engine.go @@ -400,7 +400,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG } var err error - frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) + frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) //nolint:staticcheck if err != nil { logger.Error("Failed to resample dataframe", "err", err) frame.AppendNotices(data.Notice{Text: "Failed to resample dataframe", Severity: data.NoticeSeverityWarning}) diff --git a/pkg/tsdb/mysql/sqleng/sql_engine.go b/pkg/tsdb/mysql/sqleng/sql_engine.go index 350c038b3c0..2d628b5c7f9 100644 --- a/pkg/tsdb/mysql/sqleng/sql_engine.go +++ b/pkg/tsdb/mysql/sqleng/sql_engine.go @@ -357,7 +357,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG } var err error - frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) + frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) //nolint:staticcheck if err != nil { logger.Error("Failed to resample dataframe", "err", err) frame.AppendNotices(data.Notice{Text: "Failed to resample dataframe", Severity: data.NoticeSeverityWarning}) From a6497cf91ec0973a8b9ff1244eb00a296eab4787 Mon Sep 17 00:00:00 2001 From: Ihor Yeromin Date: Tue, 25 Nov 2025 17:27:45 +0100 Subject: [PATCH 110/378] Expressions: Fix duplicate SQL expression tracking events (#114239) * Expressions: Fix duplicate SQL expression tracking events * fix(tracking): event firing on type select dropdown * chore(queries): wrap handler with useCallback --- .../PanelDataPane/PanelDataQueriesTab.tsx | 19 +++++++++++++++++-- .../expressions/ExpressionQueryEditor.tsx | 17 ----------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index 490084b24ff..cea1dfc6cb0 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -1,7 +1,9 @@ +import { useCallback } from 'react'; + import { CoreApp, DataSourceApi, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { t, Trans } from '@grafana/i18n'; -import { config, getDataSourceSrv } from '@grafana/runtime'; +import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { SceneObjectBase, SceneComponentProps, @@ -337,11 +339,24 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps { + reportInteraction('dashboards_expression_interaction', { + action: 'add_expression', + expression_type: type, + context: 'panel_query_section', + }); + model.onAddExpressionOfType(type); + }, + [model] + ); + if (!datasource || !dsSettings || !data) { return null; } const showAddButton = !isSharedDashboardQuery(dsSettings.name); + const onSelectQueryFromLibrary = async (query: DataQuery) => { // ensure all queries explicitly define a datasource const enrichedQueries = queries.map((q) => @@ -421,7 +436,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps )} {config.expressionsEnabled && model.isExpressionsSupported(dsSettings) && ( - + diff --git a/public/app/features/expressions/ExpressionQueryEditor.tsx b/public/app/features/expressions/ExpressionQueryEditor.tsx index e08cdd5d94c..4e3d89cd005 100644 --- a/public/app/features/expressions/ExpressionQueryEditor.tsx +++ b/public/app/features/expressions/ExpressionQueryEditor.tsx @@ -3,7 +3,6 @@ import { useCallback, useEffect, useRef } from 'react'; import { DataSourceApi, FeatureState, GrafanaTheme2, QueryEditorProps } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; -import { reportInteraction } from '@grafana/runtime'; import { Button, FeatureBadge, IconButton, InlineField, PopoverContent, useStyles2 } from '@grafana/ui'; import { ClassicConditions } from './components/ClassicConditions'; @@ -97,22 +96,6 @@ export function ExpressionQueryEditor(props: ExpressionQueryEditorProps) { const styles = useStyles2(getStyles); - const initialExpressionRef = useRef(query.expression); - const hasTrackedAddExpression = useRef(false); - - useEffect(() => { - // Only track if 1) query has a type, and 2) we haven't tracked yet for this component instance, and - // 3) initial expression was empty (indicating a new expression, not editing existing) - if (query.type && !hasTrackedAddExpression.current && !initialExpressionRef.current) { - reportInteraction('dashboards_expression_interaction', { - action: 'add_expression', - expression_type: query.type, - context: 'panel_query_section', - }); - hasTrackedAddExpression.current = true; - } - }, [query.type, query.refId]); - useEffect(() => { setCachedExpression(query.type, query.expression); }, [query.expression, query.type, setCachedExpression]); From ccba9fd70bf9207ca320f4880a850a58eff478e8 Mon Sep 17 00:00:00 2001 From: Renato Costa <103441181+renatolabs@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:33:06 -0500 Subject: [PATCH 111/378] chore: print wire file diff in `Check Wire Changes` workflow (#114417) --- .github/workflows/pr-go-workspace-check.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/pr-go-workspace-check.yml b/.github/workflows/pr-go-workspace-check.yml index c90573d6f4e..fd08206f0b5 100644 --- a/.github/workflows/pr-go-workspace-check.yml +++ b/.github/workflows/pr-go-workspace-check.yml @@ -107,6 +107,7 @@ jobs: CURRENT_OSS_WIRE_CHECKSUM=$(sha256sum pkg/server/wire_gen.go | cut -d' ' -f1) if [ "$CURRENT_OSS_WIRE_CHECKSUM" != "${{ steps.pre_gen_oss.outputs.wire_checksum }}" ]; then OSS_WIRE_CHANGED=true + OSS_DIFF=$(git diff pkg/server/wire_gen.go) echo "Uncomitted changes detected in pkg/server/wire_gen.go" fi @@ -115,6 +116,7 @@ jobs: CURRENT_ENTERPRISE_WIRE_CHECKSUM=$(sha256sum pkg/server/enterprise_wire_gen.go | cut -d' ' -f1) if [ "$CURRENT_ENTERPRISE_WIRE_CHECKSUM" != "${{ steps.pre_gen_enterprise.outputs.wire_checksum }}" ]; then ENTERPRISE_WIRE_CHANGED=true + ENTERPRISE_DIFF=$(git diff pkg/server/enterprise_wire_gen.go) echo "Uncomitted changes detected in pkg/server/enterprise_wire_gen.go" fi fi @@ -124,6 +126,14 @@ jobs: else if [[ "${{ github.event.pull_request.head.repo.fork }}" == "false" ]]; then echo "> !!! Please synchronize the grafana OSS and grafana enterprise code bases as defined in the enterprise readme, then run 'make gen-go' in the OSS folder and commit the changes to both repositories." + if [[ "$OSS_WIRE_CHANGED" == "true" ]]; then + echo "OSS changes:" + echo "$OSS_DIFF" + fi + if [[ "$ENTERPRISE_WIRE_CHANGED" == "true" ]]; then + echo "Enterprise changes:" + echo "$ENTERPRISE_DIFF" + fi else echo "> !!! Please run 'make gen-go' and commit the changes." fi From 6e0093f048a8840c307985db74f37254de8d734d Mon Sep 17 00:00:00 2001 From: Renato Costa <103441181+renatolabs@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:38:26 -0500 Subject: [PATCH 112/378] fix: update search request for existing provisioned dashboards in modes 3+ (#114412) Fix search for existing provisioned dashboards in modes 3+ The search query was not requesting the dashboard's "legacy ID". As a result, the provisioning process would not find existing provisioned dashboards, making copies of these dashboards every time there was a change in the provisioned dashboard's definition. --- .../dashboards/service/dashboard_service.go | 9 ++++++++- .../service/dashboard_service_test.go | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index b63b4a40f96..6ebeb537257 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -2009,7 +2009,14 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex request.Limit = query.Limit request.Page = query.Page request.Offset = (query.Page - 1) * query.Limit // only relevant when running in modes 3+ - request.Fields = dashboardsearch.IncludeFields + request.Fields = append( + dashboardsearch.IncludeFields, + // Include the dashboard legacy ID in the results, as it is needed when + // determining whether a provisioned dashboard exists or not, see + // `(*DashboardServiceImpl).searchProvisionedDashboardsThroughK8s`. + resource.SEARCH_FIELD_LEGACY_ID, + resource.SEARCH_FIELD_LABELS+"."+resource.SEARCH_FIELD_LEGACY_ID, + ) namespace := dr.k8sclient.GetNamespace(query.OrgId) var err error diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 5725cdf193b..35ac47afd9a 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -2016,6 +2016,26 @@ func TestSearchDashboardsThroughK8sRaw(t *testing.T) { _, err := service.searchDashboardsThroughK8s(ctx, query) require.NoError(t, err) }) + + t.Run("search will request legacy dashboard ID", func(t *testing.T) { + ctx := context.Background() + k8sCliMock := new(client.MockK8sHandler) + service := &DashboardServiceImpl{k8sclient: k8sCliMock} + query := &dashboards.FindPersistedDashboardsQuery{ + ManagedBy: utils.ManagerKindClassicFP, //nolint:staticcheck + OrgId: 1, + } + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool { + return slices.Contains(req.Fields, "grafana.app/deprecatedInternalID") && + slices.Contains(req.Fields, "labels.grafana.app/deprecatedInternalID") + })).Return(&resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{}, + TotalHits: 0, + }, nil) + _, err := service.searchDashboardsThroughK8s(ctx, query) + require.NoError(t, err) + }) } func TestSearchProvisionedDashboardsThroughK8sRaw(t *testing.T) { From da374527f2118154bb347132edac62384fe7f552 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Tue, 25 Nov 2025 16:34:10 -0300 Subject: [PATCH 113/378] ShortURL: K8s Implement custom authorizer (#114192) --- apps/shorturl/go.mod | 2 +- apps/shorturl/pkg/app/authorizer.go | 20 ++++++++++++++++++++ pkg/registry/apps/shorturl/register.go | 5 +++++ pkg/tests/api/shorturl/short_url_test.go | 2 +- pkg/tests/apis/shorturl/shorturl_test.go | 16 ++++++++-------- 5 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 apps/shorturl/pkg/app/authorizer.go diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index b3db27ba205..740aea32a72 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -7,6 +7,7 @@ require ( github.com/grafana/grafana-app-sdk/logging v0.48.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba k8s.io/apimachinery v0.34.2 + k8s.io/apiserver v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -90,7 +91,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.2 // indirect k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/apiserver v0.34.2 // indirect k8s.io/client-go v0.34.2 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect diff --git a/apps/shorturl/pkg/app/authorizer.go b/apps/shorturl/pkg/app/authorizer.go new file mode 100644 index 00000000000..4cde905414d --- /dev/null +++ b/apps/shorturl/pkg/app/authorizer.go @@ -0,0 +1,20 @@ +package app + +import ( + "context" + + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc(func( + ctx context.Context, attr authorizer.Attributes, + ) (authorized authorizer.Decision, reason string, err error) { + if !attr.IsResourceRequest() { + return authorizer.DecisionNoOpinion, "", nil + } + + // Any authenticated user can access the API + return authorizer.DecisionAllow, "", nil + }) +} diff --git a/pkg/registry/apps/shorturl/register.go b/pkg/registry/apps/shorturl/register.go index c5d314ff2bc..e15950dbc56 100644 --- a/pkg/registry/apps/shorturl/register.go +++ b/pkg/registry/apps/shorturl/register.go @@ -5,6 +5,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/rest" restclient "k8s.io/client-go/rest" @@ -55,6 +56,10 @@ func RegisterAppInstaller( return installer, nil } +func (a *ShortURLAppInstaller) GetAuthorizer() authorizer.Authorizer { + return shorturlapp.GetAuthorizer() +} + func (s *ShortURLAppInstaller) GetLegacyStorage(requested schema.GroupVersionResource) grafanarest.Storage { gvr := shorturl.ShortURLKind().GroupVersionResource() if requested.String() != gvr.String() { diff --git a/pkg/tests/api/shorturl/short_url_test.go b/pkg/tests/api/shorturl/short_url_test.go index cc014662457..e3c4d10c675 100644 --- a/pkg/tests/api/shorturl/short_url_test.go +++ b/pkg/tests/api/shorturl/short_url_test.go @@ -42,7 +42,7 @@ func TestShortURL(t *testing.T) { // Test that the endpoint is accessible with authentication. username, password := "viewer", "viewer" createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleEditor), + DefaultOrgRole: string(org.RoleNone), Password: user.Password(password), Login: username, }) diff --git a/pkg/tests/apis/shorturl/shorturl_test.go b/pkg/tests/apis/shorturl/shorturl_test.go index be7023c0c6a..becb0b2b564 100644 --- a/pkg/tests/apis/shorturl/shorturl_test.go +++ b/pkg/tests/apis/shorturl/shorturl_test.go @@ -123,7 +123,7 @@ func TestIntegrationShortURL(t *testing.T) { // Only legacy API should be used, no K8s API interaction func doLegacyOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -189,7 +189,7 @@ func doDualWriteTests(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest t.Run("Legacy API -> K8s API visibility", func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -223,7 +223,7 @@ func doDualWriteTests(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest t.Run("K8s API -> Legacy API visibility", func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -259,7 +259,7 @@ func doDualWriteTests(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest t.Run("Redirect functionality", func(t *testing.T) { t.Skip("Skipping redirect functionality tests for now - flaky test") client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -312,7 +312,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { t.Run("K8s API CRUD (unified storage only)", func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -349,7 +349,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { t.Run("K8s API validation - invalid paths", func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -402,7 +402,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { t.Run("K8s API validation - valid edge cases", func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -441,7 +441,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { t.Run("Redirect functionality (unified only)", func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) From 235ebbf5c5b4a4b747c3d68d81c9d0c536864733 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 25 Nov 2025 13:27:38 -0700 Subject: [PATCH 114/378] Dashboards: Prevent panic in validation (#114436) --- apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go | 3 +++ apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go index 7c5573f600a..a90eab95126 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go @@ -66,6 +66,9 @@ func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorLis } func formatErrorPath(path []string) string { + if len(path) <= 4 { + return strings.Join(path, ".") + } // omitting the "lineage.schemas[0].schema.spec" prefix here. return strings.Join(path[4:], ".") } diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go b/apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go index b48e38ffd7b..248dc27111c 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go @@ -67,6 +67,9 @@ func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorLis } func formatErrorPath(path []string) string { + if len(path) <= 4 { + return strings.Join(path, ".") + } // omitting the "lineage.schemas[0].schema.spec" prefix here. return strings.Join(path[4:], ".") } From fee3f12e4eb30ae3b4b91191a91fa7d976a2f501 Mon Sep 17 00:00:00 2001 From: Bryan Huhta <32787160+bryanhuhta@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:19:29 -0700 Subject: [PATCH 115/378] grafana-flamegraph: Add storybook stories for major components (#113375) --- .gitignore | 1 + .../.storybook/copyAssets.ts | 66 +++ .../grafana-flamegraph/.storybook/main.ts | 53 ++ .../grafana-flamegraph/.storybook/preview.ts | 54 ++ packages/grafana-flamegraph/package.json | 9 +- .../src/FlameGraph/FlameGraph.story.tsx | 31 + .../src/FlameGraph/testData/dataBasic.ts | 32 ++ .../src/FlameGraphContainer.story.tsx | 23 + .../FlameGraphTopTableContainer.story.tsx | 41 ++ .../TopTable/FlameGraphTopTableContainer.tsx | 1 + .../src/utils/storybook/withTheme.tsx | 36 ++ .../src/utils/storybook/withTimeZone.tsx | 12 + yarn.lock | 534 ++++++++++++++++-- 13 files changed, 850 insertions(+), 43 deletions(-) create mode 100644 packages/grafana-flamegraph/.storybook/copyAssets.ts create mode 100644 packages/grafana-flamegraph/.storybook/main.ts create mode 100644 packages/grafana-flamegraph/.storybook/preview.ts create mode 100644 packages/grafana-flamegraph/src/FlameGraph/FlameGraph.story.tsx create mode 100644 packages/grafana-flamegraph/src/FlameGraph/testData/dataBasic.ts create mode 100644 packages/grafana-flamegraph/src/FlameGraphContainer.story.tsx create mode 100644 packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.story.tsx create mode 100644 packages/grafana-flamegraph/src/utils/storybook/withTheme.tsx create mode 100644 packages/grafana-flamegraph/src/utils/storybook/withTimeZone.tsx diff --git a/.gitignore b/.gitignore index d0e233d6a42..f1a90d05693 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,7 @@ pkg/services/quota/quotaimpl/storage/storage.json /packages/grafana-ui/.yarn/.cache /packages/grafana-ui/.storybook/static /packages/grafana-ui/unstable +/packages/grafana-flamegraph/.storybook/static /packages/**/dist /packages/**/compiled /packages/**/.rpt2_cache diff --git a/packages/grafana-flamegraph/.storybook/copyAssets.ts b/packages/grafana-flamegraph/.storybook/copyAssets.ts new file mode 100644 index 00000000000..ab357f9b72c --- /dev/null +++ b/packages/grafana-flamegraph/.storybook/copyAssets.ts @@ -0,0 +1,66 @@ +// This script is used to copy assets from the public folder to a temporary static folder within +// the .storybook directory. +// We selectively limit assets that are uploaded to the Storybook bucket to prevent rate limiting +// when publishing new storybook. +// Note: Storybook has a static copying feature but it copies entire directories which can contain thousands of icons. + +import fs from 'fs-extra'; +import { resolve } from 'node:path'; + +// avoid importing from @grafana/data to prevent error: window is not defined +import { availableIconsIndex, IconName } from '../../grafana-data/src/types/icon.ts'; +import { getIconSubDir } from '../../grafana-ui/src/components/Icon/utils.ts'; + +const __dirname = import.meta.dirname; + +// doesn't require uploading 1000s of unused assets. +const iconPaths = Object.keys(availableIconsIndex) + .filter((iconName) => !iconName.startsWith('fa ')) + .map((iconName) => { + const subDir = getIconSubDir(iconName as IconName, 'default'); + return { + from: `../../../public/img/icons/${subDir}/${iconName}.svg`, + to: `./static/public/build/img/icons/${subDir}/${iconName}.svg`, + }; + }); + +export function copyAssetsSync() { + const assets = [ + { + from: '../../../public/fonts', + to: './static/public/fonts', + }, + { + from: '../../../public/img/grafana_text_logo-dark.svg', + to: './static/public/img/grafana_text_logo-dark.svg', + }, + { + from: '../../../public/img/grafana_text_logo-light.svg', + to: './static/public/img/grafana_text_logo-light.svg', + }, + { + from: '../../../public/img/fav32.png', + to: './static/public/img/fav32.png', + }, + { + from: '../../../public/lib', + to: './static/public/lib', + }, + ...iconPaths, + // copy over the MSW mock service worker so we can mock requests in Storybook + { + from: '../../../public/mockServiceWorker.js', + to: './static/mockServiceWorker.js', + }, + ]; + + for (const asset of assets) { + const fromPath = resolve(__dirname, asset.from); + const toPath = resolve(__dirname, asset.to); + if (!fs.existsSync(toPath)) { + fs.copySync(fromPath, toPath, { + filter: (src) => !fs.lstatSync(src).isSymbolicLink(), + }); + } + } +} diff --git a/packages/grafana-flamegraph/.storybook/main.ts b/packages/grafana-flamegraph/.storybook/main.ts new file mode 100644 index 00000000000..5f950843923 --- /dev/null +++ b/packages/grafana-flamegraph/.storybook/main.ts @@ -0,0 +1,53 @@ +import type { StorybookConfig } from '@storybook/react-webpack5'; +import { copyAssetsSync } from './copyAssets.ts'; + +import { createRequire } from 'node:module'; +const require = createRequire(import.meta.url); + +copyAssetsSync(); + +const config: StorybookConfig = { + stories: ['../src/**/*.story.tsx'], + addons: [ + { + name: '@storybook/preset-scss', + options: { + styleLoaderOptions: { + // this is required for theme switching .use() and .unuse() + injectType: 'lazyStyleTag', + }, + cssLoaderOptions: { + url: false, + importLoaders: 2, + }, + sassLoaderOptions: { + sassOptions: { + // silencing these warnings since we're planning to remove sass when angular is gone + silenceDeprecations: ['import', 'global-builtin'], + }, + }, + }, + }, + '@storybook/addon-webpack5-compiler-swc', + ], + framework: { + name: '@storybook/react-webpack5', + options: {}, + }, + staticDirs: ['static'], + webpackFinal: async (config) => { + const webpack = await import('webpack'); + + // Define process.env for browser context + config.plugins?.push( + new webpack.default.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + 'process.env.STORYBOOK_THEME': JSON.stringify(process.env.STORYBOOK_THEME || 'system'), + }) + ); + + return config; + }, +}; + +export default config; diff --git a/packages/grafana-flamegraph/.storybook/preview.ts b/packages/grafana-flamegraph/.storybook/preview.ts new file mode 100644 index 00000000000..00d08fccc24 --- /dev/null +++ b/packages/grafana-flamegraph/.storybook/preview.ts @@ -0,0 +1,54 @@ +import { Preview } from '@storybook/react'; + +import { getBuiltInThemes, getTimeZone, getTimeZones, GrafanaTheme2 } from '@grafana/data'; + +import { withTheme } from '../src/utils/storybook/withTheme'; +import { withTimeZone } from '../src/utils/storybook/withTimeZone'; + +const allowedExtraThemes: string[] = []; + +if (process.env.NODE_ENV === 'development') { + allowedExtraThemes.push('debug'); + allowedExtraThemes.push('desertbloom'); + allowedExtraThemes.push('gildedgrove'); + allowedExtraThemes.push('gloom'); + allowedExtraThemes.push('sapphiredusk'); + allowedExtraThemes.push('tron'); +} + +const preview: Preview = { + decorators: [withTheme(), withTimeZone()], + globalTypes: { + theme: { + name: 'Theme', + description: 'Global theme for components', + defaultValue: 'system', + toolbar: { + icon: 'paintbrush', + items: getBuiltInThemes(allowedExtraThemes).map((theme) => ({ + value: theme.id, + title: theme.name, + })), + showName: true, + }, + }, + timeZone: { + description: 'Set the timezone for the storybook preview', + defaultValue: getTimeZone(), + toolbar: { + icon: 'globe', + items: getTimeZones(true) + .filter((timezone) => !!timezone) + .map((timezone) => ({ + title: timezone, + value: timezone, + })), + }, + }, + }, + initialGlobals: { + theme: process.env.STORYBOOK_THEME || 'system', + }, +}; + +export default preview; diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 3e1c2347cd1..f4ec11c7cfa 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -36,7 +36,8 @@ "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", - "postpack": "mv package.json.bak package.json" + "postpack": "mv package.json.bak package.json", + "storybook": "storybook dev -p 6006" }, "browserslist": [ "defaults", @@ -60,11 +61,15 @@ "@babel/preset-env": "7.28.0", "@babel/preset-react": "7.27.1", "@rollup/plugin-node-resolve": "16.0.1", + "@storybook/addon-webpack5-compiler-swc": "^4.0.2", + "@storybook/react": "^10.0.8", + "@storybook/react-webpack5": "^10.0.8", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "@types/d3": "^7", + "@types/fs-extra": "^11", "@types/jest": "^29.5.4", "@types/lodash": "4.17.20", "@types/node": "24.10.1", @@ -73,11 +78,13 @@ "@types/tinycolor2": "1.4.6", "babel-jest": "29.7.0", "esbuild": "0.25.8", + "fs-extra": "^11.3.2", "jest": "^29.6.4", "jest-canvas-mock": "2.5.2", "rollup": "^4.22.4", "rollup-plugin-esbuild": "6.2.1", "rollup-plugin-node-externals": "^8.0.0", + "storybook": "^10.0.8", "ts-jest": "29.4.0", "ts-node": "10.9.2", "typescript": "5.9.2" diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.story.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.story.tsx new file mode 100644 index 00000000000..fdd0cd08074 --- /dev/null +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.story.tsx @@ -0,0 +1,31 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { createDataFrame } from '@grafana/data'; + +import { ColorScheme, SelectedView } from '../types'; + +import FlameGraph from './FlameGraph'; +import { CollapsedMap, FlameGraphDataContainer } from './dataTransform'; +import { data } from './testData/dataBasic'; + +const meta: Meta = { + title: 'FlameGraph', + component: FlameGraph, + args: { + rangeMin: 0, + rangeMax: 1, + textAlign: 'left', + colorScheme: ColorScheme.PackageBased, + selectedView: SelectedView.Both, + search: '', + }, +}; + +export default meta; +export const Basic: StoryObj = { + render: (args) => { + const dataContainer = new FlameGraphDataContainer(createDataFrame(data), { collapsing: false }); + + return ; + }, +}; diff --git a/packages/grafana-flamegraph/src/FlameGraph/testData/dataBasic.ts b/packages/grafana-flamegraph/src/FlameGraph/testData/dataBasic.ts new file mode 100644 index 00000000000..0d582dfa0b7 --- /dev/null +++ b/packages/grafana-flamegraph/src/FlameGraph/testData/dataBasic.ts @@ -0,0 +1,32 @@ +import { DataFrameDTO } from '@grafana/data'; + +export const data: DataFrameDTO = { + name: 'response', + refId: 'A', + // @ts-ignore + meta: { preferredVisualisationType: 'flamegraph' }, + fields: [ + { + name: 'level', + values: [0, 1, 2, 3, 2, 1], + }, + { + name: 'value', + values: [10000, 5000, 4000, 3000, 500, 3000], + config: { + unit: 'ms', + }, + }, + { + name: 'self', + values: [10000, 500, 1000, 3000, 500, 3000], + config: { + unit: 'ms', + }, + }, + { + name: 'label', + values: ['total', 'fn_1', 'fn_2', 'fn_3', 'fn_4', 'fn_5'], + }, + ], +}; diff --git a/packages/grafana-flamegraph/src/FlameGraphContainer.story.tsx b/packages/grafana-flamegraph/src/FlameGraphContainer.story.tsx new file mode 100644 index 00000000000..67fadcb0cac --- /dev/null +++ b/packages/grafana-flamegraph/src/FlameGraphContainer.story.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { createDataFrame } from '@grafana/data'; +import { useTheme2 } from '@grafana/ui'; + +import { data } from './FlameGraph/testData/dataNestedSet'; +import FlameGraphContainer, { Props } from './FlameGraphContainer'; + +const WrappedFlameGraph = (props: Omit) => { + const theme = useTheme2(); + const df = createDataFrame(data); + return theme} />; +}; + +const meta: Meta = { + title: 'FlameGraphContainer', + render: (args) => { + return ; + }, +}; + +export default meta; +export const Basic: StoryObj = {}; diff --git a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.story.tsx b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.story.tsx new file mode 100644 index 00000000000..37eb7bd4c4e --- /dev/null +++ b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.story.tsx @@ -0,0 +1,41 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { createDataFrame } from '@grafana/data'; + +import { FlameGraphDataContainer } from '../FlameGraph/dataTransform'; +import { data } from '../FlameGraph/testData/dataNestedSet'; +import { ColorScheme } from '../types'; + +import FlameGraphTopTableContainer from './FlameGraphTopTableContainer'; + +const meta: Meta = { + title: 'TopTable', + component: FlameGraphTopTableContainer, + args: { + colorScheme: ColorScheme.ValueBased, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +export const Basic: StoryObj = { + render: (args) => { + const dataContainer = new FlameGraphDataContainer(createDataFrame(data), { collapsing: true }); + + return ( + {}} + onSearch={() => {}} + onSandwich={() => {}} + /> + ); + }, +}; diff --git a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx index b9072f42c29..dc70b7c5690 100644 --- a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx +++ b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx @@ -144,6 +144,7 @@ function buildFilteredTable(data: FlameGraphDataContainer, matchedLabels?: Set) => { + const theme = getThemeById(themeId); + + const css = ` + #storybook-root { + padding: ${theme.spacing(2)}; + } + + body { + background: ${theme.colors.background.primary}; + } + `; + + return ( + + + + + {children} + + ); +}; + +export const withTheme = + (): Decorator => + // eslint-disable-next-line react/display-name + (story, context) => {story()}; diff --git a/packages/grafana-flamegraph/src/utils/storybook/withTimeZone.tsx b/packages/grafana-flamegraph/src/utils/storybook/withTimeZone.tsx new file mode 100644 index 00000000000..27445b3c24e --- /dev/null +++ b/packages/grafana-flamegraph/src/utils/storybook/withTimeZone.tsx @@ -0,0 +1,12 @@ +import { Decorator } from '@storybook/react'; +import { useEffect } from 'react'; + +import { setTimeZoneResolver } from '@grafana/data'; + +export const withTimeZone = (): Decorator => (Story, context) => { + useEffect(() => { + setTimeZoneResolver(() => context.globals.timeZone ?? 'browser'); + }, [context.globals.timeZone]); + + return Story(); +}; diff --git a/yarn.lock b/yarn.lock index 131a46fe1fc..2022568eb8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1473,7 +1473,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7": version: 7.28.4 resolution: "@babel/runtime@npm:7.28.4" checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163 @@ -2324,15 +2324,15 @@ __metadata: languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:2.3.4": - version: 2.3.4 - resolution: "@formatjs/ecma402-abstract@npm:2.3.4" +"@formatjs/ecma402-abstract@npm:2.3.6": + version: 2.3.6 + resolution: "@formatjs/ecma402-abstract@npm:2.3.6" dependencies: "@formatjs/fast-memoize": "npm:2.2.7" - "@formatjs/intl-localematcher": "npm:0.6.1" + "@formatjs/intl-localematcher": "npm:0.6.2" decimal.js: "npm:^10.4.3" tslib: "npm:^2.8.0" - checksum: 10/573971ffc291096a4b9fcc80b4708124e89bf2e3ac50e0f78b41eb797e9aa1b842f4dc3665e4467a853c738386821769d9e40408a1d25bc73323a1f057a16cf2 + checksum: 10/30b1b5cd6b62ba46245f934429936592df5500bc1b089dc92dd49c826757b873dd92c305dcfe370701e4df6b057bf007782113abb9b65db550d73be4961718bc languageName: node linkType: hard @@ -2376,13 +2376,13 @@ __metadata: linkType: hard "@formatjs/intl-durationformat@npm:^0.7.0": - version: 0.7.4 - resolution: "@formatjs/intl-durationformat@npm:0.7.4" + version: 0.7.6 + resolution: "@formatjs/intl-durationformat@npm:0.7.6" dependencies: - "@formatjs/ecma402-abstract": "npm:2.3.4" - "@formatjs/intl-localematcher": "npm:0.6.1" + "@formatjs/ecma402-abstract": "npm:2.3.6" + "@formatjs/intl-localematcher": "npm:0.6.2" tslib: "npm:^2.8.0" - checksum: 10/d62273ecd635475ca91e9b501301f3f396403fa91b584c550734b19b2d194ba1316b27303fed985c1d42ae933d54eb220da6540edfdf376b0d9371ecfd0d4e15 + checksum: 10/442236ba85bcd9cb7296c43a708271fa09f110b1ca9d5899066d00812fc2965eaeaec6b5240be421b80daba62860352131088449ba0fcd2061f671cec6240f0b languageName: node linkType: hard @@ -2395,12 +2395,12 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-localematcher@npm:0.6.1": - version: 0.6.1 - resolution: "@formatjs/intl-localematcher@npm:0.6.1" +"@formatjs/intl-localematcher@npm:0.6.2": + version: 0.6.2 + resolution: "@formatjs/intl-localematcher@npm:0.6.2" dependencies: tslib: "npm:^2.8.0" - checksum: 10/c7b3bc8395d18670677f207b2fd107561fff5d6394a9b4273c29e0bea920300ec3a2eefead600ebb7761c04a770cada28f78ac059f84d00520bfb57a9db36998 + checksum: 10/eb12a7f5367bbecdfafc20d7f005559ce840f420e970f425c5213d35e94e86dfe75bde03464971a26494bf8427d4961269db22ecad2834f2a19d888b5d9cc064 languageName: node linkType: hard @@ -3260,11 +3260,15 @@ __metadata: "@grafana/ui": "npm:12.4.0-pre" "@leeoniya/ufuzzy": "npm:1.0.19" "@rollup/plugin-node-resolve": "npm:16.0.1" + "@storybook/addon-webpack5-compiler-swc": "npm:^4.0.2" + "@storybook/react": "npm:^10.0.8" + "@storybook/react-webpack5": "npm:^10.0.8" "@testing-library/dom": "npm:10.4.1" "@testing-library/jest-dom": "npm:^6.1.2" "@testing-library/react": "npm:16.3.0" "@testing-library/user-event": "npm:14.6.1" "@types/d3": "npm:^7" + "@types/fs-extra": "npm:^11" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.20" "@types/node": "npm:24.10.1" @@ -3274,6 +3278,7 @@ __metadata: babel-jest: "npm:29.7.0" d3: "npm:^7.8.5" esbuild: "npm:0.25.8" + fs-extra: "npm:^11.3.2" jest: "npm:^29.6.4" jest-canvas-mock: "npm:2.5.2" lodash: "npm:4.17.21" @@ -3283,6 +3288,7 @@ __metadata: rollup: "npm:^4.22.4" rollup-plugin-esbuild: "npm:6.2.1" rollup-plugin-node-externals: "npm:^8.0.0" + storybook: "npm:^10.0.8" tinycolor2: "npm:1.6.0" ts-jest: "npm:29.4.0" ts-node: "npm:10.9.2" @@ -4245,7 +4251,7 @@ __metadata: languageName: node linkType: hard -"@inquirer/type@npm:^3.0.10, @inquirer/type@npm:^3.0.9": +"@inquirer/type@npm:^3.0.10": version: 3.0.10 resolution: "@inquirer/type@npm:3.0.10" peerDependencies: @@ -4257,6 +4263,18 @@ __metadata: languageName: node linkType: hard +"@inquirer/type@npm:^3.0.9": + version: 3.0.9 + resolution: "@inquirer/type@npm:3.0.9" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/960ba4737405f70bac17e7cdc4696c60064b06c8dd13a4b3d0783763ba1714bdadbd598b88d537ab9415b7d5d61e011ac042cfbd1438b2a35298e2868724b853 + languageName: node + linkType: hard + "@internationalized/date@npm:^3.10.0": version: 3.10.0 resolution: "@internationalized/date@npm:3.10.0" @@ -5027,6 +5045,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.5.5": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:0.3.9": version: 0.3.9 resolution: "@jridgewell/trace-mapping@npm:0.3.9" @@ -6096,9 +6121,9 @@ __metadata: linkType: hard "@openfeature/core@npm:^1.9.0": - version: 1.9.0 - resolution: "@openfeature/core@npm:1.9.0" - checksum: 10/c6d20edc09053afd99752fe46d8328158680950bca4b86679f67f79249d7226eea127b31fffdc38e26ecb729f2bab5a4a5a7c1db708ae76b7fbbac68cd56f094 + version: 1.9.1 + resolution: "@openfeature/core@npm:1.9.1" + checksum: 10/6099e16b1b4cd6e3c45c05ab4acd44c9cb4ab501b676ab6f3e77f0be1b56abc7506c5629187381333a02975d315d831e0905582435b356d9676255e72a465899 languageName: node linkType: hard @@ -6123,11 +6148,11 @@ __metadata: linkType: hard "@openfeature/web-sdk@npm:^1.6.1": - version: 1.6.1 - resolution: "@openfeature/web-sdk@npm:1.6.1" + version: 1.7.1 + resolution: "@openfeature/web-sdk@npm:1.7.1" peerDependencies: "@openfeature/core": ^1.9.0 - checksum: 10/8bd7d1ea386e21cdd7492cab2fd1d2b138b4e6a376a4c0a40244633e5955f6452039bc2633fc5230bd7b494506a4137ba7210d40850634f9618f77a0ee435f9d + checksum: 10/358cabfddda8bf67a9bd96ce28f0f84a3218416907f8df4f2a58750ee840d164d86a1c859e8c9aa92d132fa28fcf72a0f17ceece798ff8af297d3e1c1e30cdab languageName: node linkType: hard @@ -7943,6 +7968,18 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-webpack5-compiler-swc@npm:^4.0.2": + version: 4.0.2 + resolution: "@storybook/addon-webpack5-compiler-swc@npm:4.0.2" + dependencies: + "@swc/core": "npm:^1.13.5" + swc-loader: "npm:^0.2.6" + peerDependencies: + storybook: ^9.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 + checksum: 10/1383346b97d9f679ad830751dd1c94c8ab1c6aeb141feda60dcc7ef2ac69e97c3fb3a2a2a3f020e07598dea315760cb0c079f928f29faaf12f143d627487b0e1 + languageName: node + linkType: hard + "@storybook/blocks@npm:8.6.2, @storybook/blocks@npm:^8.6.2": version: 8.6.2 resolution: "@storybook/blocks@npm:8.6.2" @@ -7962,6 +7999,34 @@ __metadata: languageName: node linkType: hard +"@storybook/builder-webpack5@npm:10.0.8": + version: 10.0.8 + resolution: "@storybook/builder-webpack5@npm:10.0.8" + dependencies: + "@storybook/core-webpack": "npm:10.0.8" + case-sensitive-paths-webpack-plugin: "npm:^2.4.0" + cjs-module-lexer: "npm:^1.2.3" + css-loader: "npm:^7.1.2" + es-module-lexer: "npm:^1.5.0" + fork-ts-checker-webpack-plugin: "npm:^9.1.0" + html-webpack-plugin: "npm:^5.5.0" + magic-string: "npm:^0.30.5" + style-loader: "npm:^4.0.0" + terser-webpack-plugin: "npm:^5.3.14" + ts-dedent: "npm:^2.0.0" + webpack: "npm:5" + webpack-dev-middleware: "npm:^6.1.2" + webpack-hot-middleware: "npm:^2.25.1" + webpack-virtual-modules: "npm:^0.6.0" + peerDependencies: + storybook: ^10.0.8 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/136ad58f58a3595d443fa4bd3a5dd9cca30a52a59c3a34e82d1b32b1be82e292b02d3c4b74204b1e5d92c3bab3de86a164ae69f19671a52408da5cc14f550720 + languageName: node + linkType: hard + "@storybook/builder-webpack5@npm:8.6.2": version: 8.6.2 resolution: "@storybook/builder-webpack5@npm:8.6.2" @@ -8017,6 +8082,17 @@ __metadata: languageName: node linkType: hard +"@storybook/core-webpack@npm:10.0.8": + version: 10.0.8 + resolution: "@storybook/core-webpack@npm:10.0.8" + dependencies: + ts-dedent: "npm:^2.0.0" + peerDependencies: + storybook: ^10.0.8 + checksum: 10/b71baea4b0c929d1173c39bf287be94fd2862c91cf0945bed62331a4ed2b6f96dc9273b1d8b229e885cfd260d0cac7d7a8a86582a20d95c5bf27669be198752e + languageName: node + linkType: hard + "@storybook/core-webpack@npm:8.6.2": version: 8.6.2 resolution: "@storybook/core-webpack@npm:8.6.2" @@ -8080,6 +8156,16 @@ __metadata: languageName: node linkType: hard +"@storybook/icons@npm:^1.6.0": + version: 1.6.0 + resolution: "@storybook/icons@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + checksum: 10/f9036ca3b0d2904778eb4e202305f2780b549434380f9760f0bc704fe3ee19d7332f9560a66435ebb2156346cee9a863e40fa5e4b27790bf993b0c1180a3146d + languageName: node + linkType: hard + "@storybook/instrumenter@npm:8.6.2": version: 8.6.2 resolution: "@storybook/instrumenter@npm:8.6.2" @@ -8108,6 +8194,30 @@ __metadata: languageName: node linkType: hard +"@storybook/preset-react-webpack@npm:10.0.8": + version: 10.0.8 + resolution: "@storybook/preset-react-webpack@npm:10.0.8" + dependencies: + "@storybook/core-webpack": "npm:10.0.8" + "@storybook/react-docgen-typescript-plugin": "npm:1.0.6--canary.9.0c3f3b7.0" + "@types/semver": "npm:^7.3.4" + magic-string: "npm:^0.30.5" + react-docgen: "npm:^7.1.1" + resolve: "npm:^1.22.8" + semver: "npm:^7.3.7" + tsconfig-paths: "npm:^4.2.0" + webpack: "npm:5" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.0.8 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/b361b27b1550db1f03888fb06596cee35e1b6724aa41eafc23e47b28623d64c5ec1d33d0120f0280e49064a573144ccf8de114014f1d2f23730045be0ac86773 + languageName: node + linkType: hard + "@storybook/preset-react-webpack@npm:8.6.2": version: 8.6.2 resolution: "@storybook/preset-react-webpack@npm:8.6.2" @@ -8172,6 +8282,17 @@ __metadata: languageName: node linkType: hard +"@storybook/react-dom-shim@npm:10.0.8": + version: 10.0.8 + resolution: "@storybook/react-dom-shim@npm:10.0.8" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.0.8 + checksum: 10/99106afb92644ce801c0c24923e40d78d7ed935b9f8f10ceb1a1a827d777d294c37ecbdf793148747dcb4e735a34d876c691b6bac82c7058deef9ad451147dd1 + languageName: node + linkType: hard + "@storybook/react-dom-shim@npm:8.6.2": version: 8.6.2 resolution: "@storybook/react-dom-shim@npm:8.6.2" @@ -8183,6 +8304,25 @@ __metadata: languageName: node linkType: hard +"@storybook/react-webpack5@npm:^10.0.8": + version: 10.0.8 + resolution: "@storybook/react-webpack5@npm:10.0.8" + dependencies: + "@storybook/builder-webpack5": "npm:10.0.8" + "@storybook/preset-react-webpack": "npm:10.0.8" + "@storybook/react": "npm:10.0.8" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.0.8 + typescript: ">= 4.9.x" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/384b1261a9a94c4399109da0121c0070b8138a1a1ce7d1a39bab43fe5c4898144ffacb22308d6d7101677f5c4b70ffe0001b41054cc675ffe8d0fa7c84e9ed24 + languageName: node + linkType: hard + "@storybook/react-webpack5@npm:^8.6.2": version: 8.6.2 resolution: "@storybook/react-webpack5@npm:8.6.2" @@ -8202,6 +8342,24 @@ __metadata: languageName: node linkType: hard +"@storybook/react@npm:10.0.8, @storybook/react@npm:^10.0.8": + version: 10.0.8 + resolution: "@storybook/react@npm:10.0.8" + dependencies: + "@storybook/global": "npm:^5.0.0" + "@storybook/react-dom-shim": "npm:10.0.8" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.0.8 + typescript: ">= 4.9.x" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/cbb46246958a0d2db0cc641661a14e5ae20b08ee4d2626e6d39ae4332053b3c6d84bdb77e36bc839ae7dd770504049f187466383ee06aac33bc2ef5a1c69b78f + languageName: node + linkType: hard + "@storybook/react@npm:8.6.2, @storybook/react@npm:^8.6.2": version: 8.6.2 resolution: "@storybook/react@npm:8.6.2" @@ -8881,6 +9039,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-arm64@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-darwin-arm64@npm:1.15.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-darwin-x64@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-darwin-x64@npm:1.13.3" @@ -8895,6 +9060,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-x64@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-darwin-x64@npm:1.15.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@swc/core-linux-arm-gnueabihf@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.3" @@ -8909,6 +9081,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm-gnueabihf@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.15.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@swc/core-linux-arm64-gnu@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-arm64-gnu@npm:1.13.3" @@ -8923,6 +9102,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-gnu@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-linux-arm64-gnu@npm:1.15.3" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-arm64-musl@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-arm64-musl@npm:1.13.3" @@ -8937,6 +9123,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-musl@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-linux-arm64-musl@npm:1.15.3" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@swc/core-linux-x64-gnu@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-x64-gnu@npm:1.13.3" @@ -8951,6 +9144,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-gnu@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-linux-x64-gnu@npm:1.15.3" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-x64-musl@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-x64-musl@npm:1.13.3" @@ -8965,6 +9165,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-musl@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-linux-x64-musl@npm:1.15.3" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@swc/core-win32-arm64-msvc@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-win32-arm64-msvc@npm:1.13.3" @@ -8979,6 +9186,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-arm64-msvc@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-win32-arm64-msvc@npm:1.15.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-win32-ia32-msvc@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-win32-ia32-msvc@npm:1.13.3" @@ -8993,6 +9207,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-ia32-msvc@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-win32-ia32-msvc@npm:1.15.3" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@swc/core-win32-x64-msvc@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-win32-x64-msvc@npm:1.13.3" @@ -9007,6 +9228,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-x64-msvc@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-win32-x64-msvc@npm:1.15.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@swc/core@npm:1.13.3": version: 1.13.3 resolution: "@swc/core@npm:1.13.3" @@ -9053,7 +9281,7 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.15.2, @swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22": +"@swc/core@npm:1.15.2, @swc/core@npm:^1.13.5": version: 1.15.2 resolution: "@swc/core@npm:1.15.2" dependencies: @@ -9099,6 +9327,52 @@ __metadata: languageName: node linkType: hard +"@swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22": + version: 1.15.3 + resolution: "@swc/core@npm:1.15.3" + dependencies: + "@swc/core-darwin-arm64": "npm:1.15.3" + "@swc/core-darwin-x64": "npm:1.15.3" + "@swc/core-linux-arm-gnueabihf": "npm:1.15.3" + "@swc/core-linux-arm64-gnu": "npm:1.15.3" + "@swc/core-linux-arm64-musl": "npm:1.15.3" + "@swc/core-linux-x64-gnu": "npm:1.15.3" + "@swc/core-linux-x64-musl": "npm:1.15.3" + "@swc/core-win32-arm64-msvc": "npm:1.15.3" + "@swc/core-win32-ia32-msvc": "npm:1.15.3" + "@swc/core-win32-x64-msvc": "npm:1.15.3" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.25" + peerDependencies: + "@swc/helpers": ">=0.5.17" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10/280330d82328818138ed64fdcf9ea9abde6b6f16eca65a9d4db27dde06a8dfffd2649f3447d2243387277513c7430fa4142cafcfd64e943d682ce6a713cb8c2d + languageName: node + linkType: hard + "@swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -9480,6 +9754,16 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^5.2.2": + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10/e79947307dc235953622e65f83d2683835212357ca261389116ab90bed369ac862ba28b146b4fed08b503ae1e1a12cb93ce783f24bb8d562950469f4320e1c7c + languageName: node + linkType: hard + "@types/chance@npm:^1.1.7": version: 1.1.7 resolution: "@types/chance@npm:1.1.7" @@ -9844,6 +10128,13 @@ __metadata: languageName: node linkType: hard +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10/249a27b0bb22f6aa28461db56afa21ec044fa0e303221a62dff81831b20c8530502175f1a49060f7099e7be06181078548ac47c668de79ff9880241968d43d0c + languageName: node + linkType: hard + "@types/doctrine@npm:^0.0.9": version: 0.0.9 resolution: "@types/doctrine@npm:0.0.9" @@ -9916,7 +10207,7 @@ __metadata: languageName: node linkType: hard -"@types/fs-extra@npm:^11.0.4": +"@types/fs-extra@npm:^11, @types/fs-extra@npm:^11.0.4": version: 11.0.4 resolution: "@types/fs-extra@npm:11.0.4" dependencies: @@ -11134,6 +11425,38 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/expect@npm:3.2.4" + dependencies: + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:3.2.4" + "@vitest/utils": "npm:3.2.4" + chai: "npm:^5.2.0" + tinyrainbow: "npm:^2.0.0" + checksum: 10/dc69ce886c13714dfbbff78f2d2cb7eb536017e82301a73c42d573a9e9d2bf91005ac7abd9b977adf0a3bd431209f45a8ac2418029b68b0a377e092607c843ce + languageName: node + linkType: hard + +"@vitest/mocker@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/mocker@npm:3.2.4" + dependencies: + "@vitest/spy": "npm:3.2.4" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.17" + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10/5e92431b6ed9fc1679060e4caef3e4623f4750542a5d7cd944774f8217c4d231e273202e8aea00bab33260a5a9222ecb7005d80da0348c3c829bd37d123071a8 + languageName: node + linkType: hard + "@vitest/pretty-format@npm:2.0.5": version: 2.0.5 resolution: "@vitest/pretty-format@npm:2.0.5" @@ -11152,6 +11475,15 @@ __metadata: languageName: node linkType: hard +"@vitest/pretty-format@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/pretty-format@npm:3.2.4" + dependencies: + tinyrainbow: "npm:^2.0.0" + checksum: 10/8dd30cbf956e01fbab042fe651fb5175d9f0cd00b7b569a46cd98df89c4fec47dab12916201ad6e09a4f25f2a2ec8927a4bfdc61118593097f759c90b18a51d4 + languageName: node + linkType: hard + "@vitest/spy@npm:2.0.5": version: 2.0.5 resolution: "@vitest/spy@npm:2.0.5" @@ -11161,6 +11493,15 @@ __metadata: languageName: node linkType: hard +"@vitest/spy@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/spy@npm:3.2.4" + dependencies: + tinyspy: "npm:^4.0.3" + checksum: 10/7d38c299f42a8c7e5e41652b203af98ca54e63df69c3b072d0e401d5a57fbbba3e39d8538ac1b3022c26718a6388d0bcc222bc2f07faab75942543b9247c007d + languageName: node + linkType: hard + "@vitest/utils@npm:2.0.5": version: 2.0.5 resolution: "@vitest/utils@npm:2.0.5" @@ -11173,6 +11514,17 @@ __metadata: languageName: node linkType: hard +"@vitest/utils@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/utils@npm:3.2.4" + dependencies: + "@vitest/pretty-format": "npm:3.2.4" + loupe: "npm:^3.1.4" + tinyrainbow: "npm:^2.0.0" + checksum: 10/7f12ef63bd8ee13957744d1f336b0405f164ade4358bf9dfa531f75bbb58ffac02bf61aba65724311ddbc50b12ba54853a169e59c6b837c16086173b9a480710 + languageName: node + linkType: hard + "@vitest/utils@npm:^2.1.1": version: 2.1.9 resolution: "@vitest/utils@npm:2.1.9" @@ -13201,6 +13553,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.2.0": + version: 5.3.3 + resolution: "chai@npm:5.3.3" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10/0d0ef63106083b05c7ba510697cd9991a02b8df5984a7d010ab4af10205c7a1f27d1c06bfa4679540894295ac4dcc22aa2a281e2e4cfe5133c1db379626689a2 + languageName: node + linkType: hard + "chalk@npm:4.1.0": version: 4.1.0 resolution: "chalk@npm:4.1.0" @@ -14455,7 +14820,7 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:7.1.2": +"css-loader@npm:7.1.2, css-loader@npm:^7.1.2": version: 7.1.2 resolution: "css-loader@npm:7.1.2" dependencies: @@ -17951,7 +18316,7 @@ __metadata: languageName: node linkType: hard -"fork-ts-checker-webpack-plugin@npm:9.1.0": +"fork-ts-checker-webpack-plugin@npm:9.1.0, fork-ts-checker-webpack-plugin@npm:^9.1.0": version: 9.1.0 resolution: "fork-ts-checker-webpack-plugin@npm:9.1.0" dependencies: @@ -18097,6 +18462,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.3.2": + version: 11.3.2 + resolution: "fs-extra@npm:11.3.2" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/d559545c73fda69c75aa786f345c2f738b623b42aea850200b1582e006a35278f63787179e3194ba19413c26a280441758952b0c7e88dd96762d497e365a6c3e + languageName: node + linkType: hard + "fs-extra@npm:^8.0.1, fs-extra@npm:^8.1.0": version: 8.1.0 resolution: "fs-extra@npm:8.1.0" @@ -20037,22 +20413,22 @@ __metadata: linkType: hard "i18next@npm:^23.5.1 || ^24.2.0": - version: 24.2.2 - resolution: "i18next@npm:24.2.2" + version: 24.2.3 + resolution: "i18next@npm:24.2.3" dependencies: - "@babel/runtime": "npm:^7.23.2" + "@babel/runtime": "npm:^7.26.10" peerDependencies: typescript: ^5 peerDependenciesMeta: typescript: optional: true - checksum: 10/f66ed9e56d9412e59502f5df39163631daf9f1264774732fb21edbd66a528ca7a6b67dc2e2aec95683c6c7956e42c651587a54bd8ee082bd12008880ce6cd326 + checksum: 10/6c73d964f2a98b1aa2c2717fe6da66fc265bcbbc5fcd52b2bfff51ff013d30d4f7d7449c4eb7f464d27af43e2e73f2e7f1d46a144d731bd3bdb1385d4c199e4c languageName: node linkType: hard "i18next@npm:^25.0.0, i18next@npm:^25.5.2": - version: 25.5.2 - resolution: "i18next@npm:25.5.2" + version: 25.6.2 + resolution: "i18next@npm:25.6.2" dependencies: "@babel/runtime": "npm:^7.27.6" peerDependencies: @@ -20060,7 +20436,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/8d52e82386722a228f4465aa5cf39d82bd9861ea9cc8b31d7cc4d22d5e70b2740ee006068b09f486d5273cabd227a2ac14f37d68fab26344524200083f679bcd + checksum: 10/bb7f3e200caac72bacaead766287dbad5ed5945430bd9f2be1c14fbccd49cf78ab751f78cf53cd0be3ec063bc4c40837d7d771f7b14d50fcb9022e1ed5d6e72f languageName: node linkType: hard @@ -23161,9 +23537,9 @@ __metadata: linkType: hard "lossless-json@npm:^4.1.1": - version: 4.2.0 - resolution: "lossless-json@npm:4.2.0" - checksum: 10/b29cbf9a90b8f3108219ff410223a90a3a6ad14cf902eed4038571d9421c69ade7077fb59737e1ca20f3f404ddd68fad69be11365b0d79b4b61421b054759551 + version: 4.3.0 + resolution: "lossless-json@npm:4.3.0" + checksum: 10/a984a882c79b6e62a917d0202518472c17587bdee002f1427d7ec61115f9fbdeb5f0baa9f0e9fff8d9dbacd17da6b6c910012b2dcab69dd33d151cb7c13d5a37 languageName: node linkType: hard @@ -23174,6 +23550,13 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.4": + version: 3.2.1 + resolution: "loupe@npm:3.2.1" + checksum: 10/a4d78ec758aaa04e0e35d5cd1c15e970beb9cdbfd3d0f34f98b9bcda489f896a7190b3b6cc40b7a6dcb8e97e82e96eafaae10096aaa469804acdba6f7c2bde5f + languageName: node + linkType: hard + "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -23262,6 +23645,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.17": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10/57d5691f41ed40d962d8bd300148114f53db67fadbff336207db10a99f2bdf4a1be9cac3a68ee85dba575912ee1d4402e4396408196ec2d3afd043b076156221 + languageName: node + linkType: hard + "magic-string@npm:^0.30.3, magic-string@npm:^0.30.5": version: 0.30.17 resolution: "magic-string@npm:0.30.17" @@ -23638,9 +24030,9 @@ __metadata: linkType: hard "micro-memoize@npm:^4.1.2": - version: 4.1.3 - resolution: "micro-memoize@npm:4.1.3" - checksum: 10/4e9c7767911cc76ae9c9779584ec87844437af9446b295a01774640a732c2c7f91944794027f44625031f7330ab7f9147740d0a9fb612680d1d2d858dad43402 + version: 4.2.0 + resolution: "micro-memoize@npm:4.2.0" + checksum: 10/260e27a5c15809f7dc435a89a424d93e49ccca62b743fa96ee72f254264df58c95edacbd847a2899a6d7f8ad0daef188548a8b12add5ccff83e88869a50a762c languageName: node linkType: hard @@ -27948,6 +28340,24 @@ __metadata: languageName: node linkType: hard +"react-docgen@npm:^7.1.1": + version: 7.1.1 + resolution: "react-docgen@npm:7.1.1" + dependencies: + "@babel/core": "npm:^7.18.9" + "@babel/traverse": "npm:^7.18.9" + "@babel/types": "npm:^7.18.9" + "@types/babel__core": "npm:^7.18.0" + "@types/babel__traverse": "npm:^7.18.0" + "@types/doctrine": "npm:^0.0.9" + "@types/resolve": "npm:^1.20.2" + doctrine: "npm:^3.0.0" + resolve: "npm:^1.22.1" + strip-indent: "npm:^4.0.0" + checksum: 10/501e5fa0d00e32ee27559f44462a34e9531018ccb46c51efbe60b98a4c077f43dbe8999da5bb91d2ab45a83a34099436a3b725fdabd3f218dbb4493c0b1c9f95 + languageName: node + linkType: hard + "react-dom@npm:18.3.1": version: 18.3.1 resolution: "react-dom@npm:18.3.1" @@ -30862,6 +31272,32 @@ __metadata: languageName: node linkType: hard +"storybook@npm:^10.0.8": + version: 10.0.8 + resolution: "storybook@npm:10.0.8" + dependencies: + "@storybook/global": "npm:^5.0.0" + "@storybook/icons": "npm:^1.6.0" + "@testing-library/jest-dom": "npm:^6.6.3" + "@testing-library/user-event": "npm:^14.6.1" + "@vitest/expect": "npm:3.2.4" + "@vitest/mocker": "npm:3.2.4" + "@vitest/spy": "npm:3.2.4" + esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0" + recast: "npm:^0.23.5" + semver: "npm:^7.6.2" + ws: "npm:^8.18.0" + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + bin: + storybook: ./dist/bin/dispatcher.js + checksum: 10/a30b54248d69d7406f78e1871e075092ed9ae366d1c7cec56dc10b31b72a5196ef9e751453c9a7546797228c4fe0f01893c6fa541d24cf51037124d4f0e1566e + languageName: node + linkType: hard + "storybook@npm:^8.6.2": version: 8.6.2 resolution: "storybook@npm:8.6.2" @@ -31210,7 +31646,7 @@ __metadata: languageName: node linkType: hard -"style-loader@npm:4.0.0": +"style-loader@npm:4.0.0, style-loader@npm:^4.0.0": version: 4.0.0 resolution: "style-loader@npm:4.0.0" peerDependencies: @@ -31656,7 +32092,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:5.3.14, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.11": +"terser-webpack-plugin@npm:5.3.14, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.11, terser-webpack-plugin@npm:^5.3.14": version: 5.3.14 resolution: "terser-webpack-plugin@npm:5.3.14" dependencies: @@ -31860,6 +32296,13 @@ __metadata: languageName: node linkType: hard +"tinyrainbow@npm:^2.0.0": + version: 2.0.0 + resolution: "tinyrainbow@npm:2.0.0" + checksum: 10/94d4e16246972614a5601eeb169ba94f1d49752426312d3cf8cc4f2cc663a2e354ffc653aa4de4eebccbf9eeebdd0caef52d1150271fdfde65d7ae7f3dcb9eb5 + languageName: node + linkType: hard + "tinyspy@npm:^3.0.0": version: 3.0.2 resolution: "tinyspy@npm:3.0.2" @@ -31867,6 +32310,13 @@ __metadata: languageName: node linkType: hard +"tinyspy@npm:^4.0.3": + version: 4.0.4 + resolution: "tinyspy@npm:4.0.4" + checksum: 10/858a99e3ded2fba8fe7c243099d9e58e926d6525af03d19cdf86c1a9a30398161fb830b4f77890d266bcc1c69df08fa6f4baf29d089385e4cdaa98d7b6296e7c + languageName: node + linkType: hard + "title-case@npm:^3.0.3": version: 3.0.3 resolution: "title-case@npm:3.0.3" @@ -34037,7 +34487,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.18.3, ws@npm:^8.2.3, ws@npm:^8.9.0": +"ws@npm:^8.18.0, ws@npm:^8.18.3, ws@npm:^8.2.3, ws@npm:^8.9.0": version: 8.18.3 resolution: "ws@npm:8.18.3" peerDependencies: From 2544a3441ee642e73ec93adddaf817e7bf730ca8 Mon Sep 17 00:00:00 2001 From: Johnny Kartheiser <140559259+JohnnyK-Grafana@users.noreply.github.com> Date: Tue, 25 Nov 2025 16:35:38 -0600 Subject: [PATCH 116/378] alerting docs: initialization_timeout addition (#114373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit added docs for initialization_timeout (https://github.com/grafana/grafana/pull/96053) Co-authored-by: Irene Rodríguez --- docs/sources/setup-grafana/configure-grafana/_index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index c81d24d0758..13c2b77fd2f 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1933,6 +1933,10 @@ The initial delay before retrying a failed alert evaluation. Default is `1s`. This value is the starting point for exponential backoff. +#### `initialization_timeout` + +Allows the context deadline for the `AlertNG` service to be configurable. The default timeout is 30s. + #### `max_retry_delay` The maximum delay between retries during exponential backoff. Default is `4s`. From a99946d92150b65a7a9acc4b5b88468d48c8913f Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 25 Nov 2025 19:58:20 -0700 Subject: [PATCH 117/378] Dashboards: Cleanup includes fields (#114455) --- pkg/services/dashboards/service/dashboard_service.go | 9 +-------- pkg/services/dashboards/service/search/search.go | 3 +++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 6ebeb537257..b63b4a40f96 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -2009,14 +2009,7 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex request.Limit = query.Limit request.Page = query.Page request.Offset = (query.Page - 1) * query.Limit // only relevant when running in modes 3+ - request.Fields = append( - dashboardsearch.IncludeFields, - // Include the dashboard legacy ID in the results, as it is needed when - // determining whether a provisioned dashboard exists or not, see - // `(*DashboardServiceImpl).searchProvisionedDashboardsThroughK8s`. - resource.SEARCH_FIELD_LEGACY_ID, - resource.SEARCH_FIELD_LABELS+"."+resource.SEARCH_FIELD_LEGACY_ID, - ) + request.Fields = dashboardsearch.IncludeFields namespace := dr.k8sclient.GetNamespace(query.OrgId) var err error diff --git a/pkg/services/dashboards/service/search/search.go b/pkg/services/dashboards/service/search/search.go index 79e61eab99a..e1ce9e9132d 100644 --- a/pkg/services/dashboards/service/search/search.go +++ b/pkg/services/dashboards/service/search/search.go @@ -40,6 +40,9 @@ var ( resource.SEARCH_FIELD_SOURCE_PATH, resource.SEARCH_FIELD_SOURCE_CHECKSUM, resource.SEARCH_FIELD_SOURCE_TIME, + // below is needed to determine whether a provisioned dashboard exists or not + resource.SEARCH_FIELD_LEGACY_ID, + resource.SEARCH_FIELD_LABELS + "." + resource.SEARCH_FIELD_LEGACY_ID, } ) From d226c359042aa648d760316605860066fcba41d5 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 26 Nov 2025 09:37:57 +0100 Subject: [PATCH 118/378] Alerting: Add first CLAUDE.md in the frontend alerting folder (#114308) Co-authored-by: Gilles De Mey --- .../app/features/alerting/unified/CLAUDE.md | 432 ++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 public/app/features/alerting/unified/CLAUDE.md diff --git a/public/app/features/alerting/unified/CLAUDE.md b/public/app/features/alerting/unified/CLAUDE.md new file mode 100644 index 00000000000..7778bb08da4 --- /dev/null +++ b/public/app/features/alerting/unified/CLAUDE.md @@ -0,0 +1,432 @@ +# Alerting Squad - Claude Code Configuration + +This file provides context for Claude Code when working on the Grafana Alerting codebase. It contains alerting-specific patterns and references to Grafana's coding standards. + +## Project Context + +**Location**: `public/app/features/alerting/unified/` +**Squad**: Alerting +**Focus**: Frontend development for Grafana's unified alerting system +**Tech Stack**: React, TypeScript, Redux Toolkit, RTK Query, Emotion, Jest, React Testing Library, MSW + +## Grafana Coding Standards + +**IMPORTANT**: Always follow Grafana's official style guides. Do not duplicate standards here - reference the source files: + +### Required Reading + +1. **Frontend Style Guide**: [../../../../contribute/style-guides/frontend.md](../../../../contribute/style-guides/frontend.md) + - Naming conventions, component patterns, TypeScript, exports + - Function declarations for components, callback props with "on" prefix + +2. **Testing Guidelines**: [../../../../contribute/style-guides/testing.md](../../../../contribute/style-guides/testing.md) + - React Testing Library, query priorities, user event setup + +3. **Styling Guide**: [../../../../contribute/style-guides/styling.md](../../../../contribute/style-guides/styling.md) + - Emotion usage, `useStyles2` hook patterns + +4. **Redux Framework**: [../../../../contribute/style-guides/redux.md](../../../../contribute/style-guides/redux.md) + - Redux Toolkit patterns, reducer testing + +5. **Alerting Testing Guide**: [./TESTING.md](./TESTING.md) + - MSW API mocking, permission mocking, data source setup + +### Alerting-Specific Conventions + +**Use @grafana/alerting Package**: + +- **Always check @grafana/alerting for shared components/hooks** before creating new ones: + + ```typescript + // Good - Use exported components/hooks from @grafana/alerting + import { AlertLabel, alertingMatchers } from '@grafana/alerting'; + + // Check what's available before reimplementing + ``` + +**Layout Components**: + +- **Prefer @grafana/ui layout components** over styled divs: + + ```typescript + // Good - Use Box for simple layout/spacing + import { Box } from '@grafana/ui'; + Content + + // Good - Use Stack for flex layouts + import { Stack } from '@grafana/ui'; + +
Item 1
+
Item 2
+
+ + // Avoid - Custom styled divs when layout components exist +
Content
+ ``` + +## Alerting Codebase Structure + +### Key Directories + +- `api/` - RTK Query API slices for data fetching +- `components/` - Feature-specific React components organized by domain +- `hooks/` - Reusable custom hooks for logic and data fetching +- `rule-editor/` - Alert rule creation and editing forms +- `rule-list/` - Alert rules list views (v1 and v2) +- `state/` - Redux state management and context providers +- `utils/` - Utility functions and helpers +- `types/` - TypeScript type definitions +- `mocks/` - MSW mock server setup for testing +- `testSetup/` - Test utilities and configuration + +### Component Domains + +- `alert-groups/` - Alert grouping and filtering +- `contact-points/` - Contact point configuration +- `notification-policies/` - Notification routing policies +- `mute-timings/` - Mute timing windows +- `silences/` - Silence management +- `receivers/` - Receiver configuration +- `templates/` - Notification templates +- `permissions/` - Permission management +- `settings/` - Alertmanager settings + +## State Management Patterns + +### RTK Query (Primary - Preferred) + +**IMPORTANT**: Our direction is to use RTK Query for data fetching, NOT Redux. Do not create new RTKQ endpoints, those should be created manually. + +- API slices in `api/` directory +- Custom base query in `api/alertingApi.ts` +- Automatic caching with 2-minute polling: `RULE_LIST_POLL_INTERVAL_MS` +- Key APIs: `alertRuleApi`, `alertmanagerApi`, `prometheusApi`, `receiversApi` + +**For new features**: Always use RTK Query hooks for data fetching: + +```typescript +import { useGetAlertRulesQuery } from '../api/alertRuleApi'; + +const { data, isLoading, error } = useGetAlertRulesQuery(params); +``` + +### Redux Toolkit (Legacy) + +**Avoid for new features** - Use RTK Query instead + +- Legacy reducers exist in `state/reducers/` +- Use state selectors: `useUnifiedAlertingSelector` +- Only modify if maintaining existing Redux code + +### Context Providers + +- `AlertmanagerContext` - Alertmanager selection state, for managing Alertmanager entities for a specific Alertmanager data source. +- `SettingsContext` - Settings state – used in `public/app/features/alerting/unified/components/settings` +- `WorkbenchContext` - Workbench state used in the alert triage feature `public/app/features/alerting/unified/triage` + +### Forms + +- Use `react-hook-form` (v7) for all forms +- See `rule-editor/alert-rule-form/` for patterns + +## Alerting-Specific Testing Patterns + +See [./TESTING.md](./TESTING.md) for comprehensive testing guide. Key points: + +### API Mocking with MSW + +**REQUIRED**: Use MSW for all API mocking (not `jest.fn()`) – though it's fine to use this function for unit testing. + +```typescript +import { mockApi } from '../mockApi'; + +// Mock common endpoints +mockApi.eval(); // for AlertingQueryRunner +// If helper doesn't exist, add it to mockApi.ts +``` + +**Why MSW?** Forces proper loading state handling, discovers UI issues early + +### Permission Mocking + +**Default: RBAC enabled** (most common user scenario) + +```typescript +import { enableRBAC, grantUserPermissions } from '../mocks'; + +enableRBAC(); // Usually not needed, enabled by default +grantUserPermissions([AccessControlAction.AlertingRuleRead]); +``` + +### Mock Data Factories + +Located in `mocks.ts`: + +```typescript +mockDataSource(); +mockPromAlert(); +mockRulerGrafanaRule(); +mockAlertmanagerAlert(); +mockSilence(); +``` + +### Data Source Setup + +Located in `testSetup/datasources.ts` for data source mocking patterns + +### Test Data Factories + +**Use factories for creating test data** - Don't manually create objects. + +For Kubernetes APIs and new schemas – use the `@grafana/alerting` package. + +Mock factories are defined in `packages/grafana-alerting/src/grafana/api/notifications/v0alpha1/mocks/fakes` +MSW handlers in `packages/grafana-alerting/src/grafana/api/notifications/v0alpha1/mocks/handlers` + +And there are "scenarios" that combine the two above. An example of such is `packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.test.scenario.ts` and is used for integration tests. + +Additionally alerting uses **`alertingFactory`** from `mocks/server/db` for building test data: + +```typescript +import { alertingFactory } from './mocks/server/db'; +import { mockFolder } from './mocks'; + +// Build a single alerting rule +const alertingRuleBuilder = alertingFactory.ruler.grafana.alertingRule; +const rule = alertingRuleBuilder.build(); + +// Build multiple rules +const rules = alertingRuleBuilder.buildList(6); + +// Override specific fields +const customRule = alertingRuleBuilder.build({ + grafana_alert: { title: 'CPU Alert' }, + labels: { severity: 'critical' }, +}); +``` + +**Common patterns**: + +```typescript +// Alerting rules +alertingFactory.ruler.grafana.alertingRule.build(); +alertingFactory.ruler.grafana.alertingRule.buildList(n); + +// Folders +mockFolder(); // Simple mock function + +// Override fields when building +alertingRuleBuilder.build({ + grafana_alert: { title: 'Custom Title' }, + labels: { key: 'value' }, +}); +``` + +**Benefits**: + +- Consistent test data across tests +- Easy to generate multiple instances with `buildList(n)` +- Override only the fields you care about +- Automatic sequencing (e.g., "Alerting rule 1", "Alerting rule 2") + +**Other mock functions** (from `mocks.ts`): + +```typescript +mockDataSource(); +mockPromAlert(); +mockRulerGrafanaRule(); +mockAlertmanagerAlert(); +mockSilence(); +mockFolder(); +``` + +## Alerting-Specific Patterns + +### Feature Toggles & settings + +A full list of features can be found in `pkg/services/featuremgmt/toggles_gen.csv` – focus on feature toggles owned by `@grafana/alerting-squad`. + +```typescript +import { config } from '@grafana/runtime'; + +if (config.featureToggles.alertingTriage) { + // Render triage view +} +``` + +A common configuration setting would be `unifiedAlertingEnabled` which allows a user to configure Grafana without any alerting UI or backend enabled at all. + +### Data Source Abstractions + +```typescript +import { isGrafanaRulerRule } from '../utils/rules'; + +if (isGrafanaRulerRule(rule)) { + // Grafana-managed +} else { + // External alertmanager +} +``` + +### Access Control (RBAC) + +```typescript +import { useAbilities } from '../hooks/useAbilities'; + +function Component() { + const [_, { can }] = useAbilities(); + const canCreate = can(AccessControlAction.AlertingRuleCreate); + + return canCreate ? : null; +} +``` + +### Key Routes + +Defined in `routes.tsx`: + +- `/alerting` - Home +- `/alerting/list` - Rules list (v1/v2) +- `/alerting/new/:type?` - Create rule +- `/alerting/:id/edit` - Edit rule +- `/alerting/notifications` - Contact points +- `/alerting/routes` - Notification policies + +### Common Hooks + +```typescript +useCombinedRuleNamespaces(); // Combines Prometheus + Ruler rules +useAlertmanagerConfig(); // Fetch alertmanager config +useFolder(); // Folder operations +useUnifiedAlertingSelector(); // Redux state – avoid using +useAbilities(); // Permission checking +``` + +### Link URLs and Navigation + +**IMPORTANT**: Different navigation components require different URL formats. + +#### When to use `createRelativeUrl` + +Use `createRelativeUrl` **only with LinkButton** (and other components that render HTML `` elements): + +```typescript +import { createRelativeUrl } from '@grafana/data'; +import { LinkButton } from '@grafana/ui'; + +// LinkButton renders tag - needs manual subpath prefix + + View Rules + +``` + +**Why?** LinkButton renders a native HTML anchor element, so it doesn't use React Router. You must manually add the subpath prefix using `createRelativeUrl`. + +#### When NOT to use `createRelativeUrl` + +Do **NOT** use `createRelativeUrl` with: + +1. **locationService** - Automatically adds prefix: + +```typescript +import { locationService } from '@grafana/runtime'; + +// locationService uses react-router history - prefix added automatically +locationService.push('/alerting/list'); // ✅ Correct - no createRelativeUrl +locationService.push(createRelativeUrl('/alerting/list')); // ❌ Wrong - double prefix! +``` + +2. **TextLink component** - Automatically adds prefix: + +```typescript +import { TextLink } from '@grafana/ui'; + +// TextLink uses react-router Link - prefix added automatically +View Rules // ✅ Correct +View Rules // ❌ Wrong +``` + +#### Summary + +| Component/Service | Use `createRelativeUrl`? | Reason | +| -------------------- | ------------------------ | --------------------------------------- | +| `LinkButton` | ✅ YES | Renders `` tag (native HTML) | +| `Button` with `href` | ✅ YES | Renders `` tag when href provided | +| `locationService` | ❌ NO | Uses react-router history (auto-prefix) | +| `TextLink` | ❌ NO | Uses react-router Link (auto-prefix) | +| React Router `Link` | ❌ NO | React Router component (auto-prefix) | + +**Rule of thumb**: If it renders a native HTML `` tag, use `createRelativeUrl`. If it uses React Router, don't. + +## Key Libraries + +### Grafana Internal + +- `@grafana/ui` - UI components (Button, Select, Input, etc.) +- `@grafana/data` - Data models and utilities +- `@grafana/runtime` - Runtime services (config, backendSrv, locationService) +- `@grafana/scenes` - Scene framework (Insights/Triage views) +- `@grafana/e2e-selectors` - Test selectors +- `@grafana/alerting` - Grafana managed alerting specific package (utility functions, API endpoints, mocks, React components, etc) + +### External + +- `react-hook-form` (v7) - Form state +- `@reduxjs/toolkit` - Redux + RTK Query +- `@emotion/css` - Styling +- `lodash` - Utilities +- `msw` - API mocking for tests + +## Quick Reference Checklists + +### Creating a New Component + +1. ✅ Create in appropriate `components/` subdirectory +2. ✅ Use function declaration (not arrow function) +3. ✅ Add TypeScript props interface (no "I" prefix) +4. ✅ Use `useStyles2` for styling (Emotion) +5. ✅ Create colocated test file +6. ✅ Use MSW for API mocking in tests +7. ✅ Query with `*ByRole` queries + +### Adding a New API Endpoint + +1. ✅ Add to relevant API slice in `api/` (RTK Query) +2. ✅ Add helper to `mockApi.ts` for testing +3. ✅ Handle loading/error states in UI +4. ✅ Test with MSW + +### Creating a New Form + +1. ✅ Use `react-hook-form` (v7) +2. ✅ See `rule-editor/alert-rule-form/` for patterns +3. ✅ Add validation (schema if needed) +4. ✅ Handle API submission errors +5. ✅ Test user interactions with `userEvent` + +### Writing Tests + +Check https://testing-library.com/docs/queries/about/ for what selectors to prefer when using React Testing Library + +- [ ] RBAC enabled by default +- [ ] MSW for API mocking (not `jest.fn()`) +- [ ] Loading states tested +- [ ] Error states tested +- [ ] User interactions use `userEvent.setup()` +- [ ] Queries prefer `*ByRole` +- [ ] Async operations use `await` and `findBy*` +- [ ] Permissions tested with `grantUserPermissions` + +## Getting Help + +- Check patterns in existing `components/` code +- Review test examples in `*.test.tsx` files +- Consult `mockApi.ts` for API mocking +- See `mocks.ts` for data factories +- Read [./TESTING.md](./TESTING.md) for testing details +- Review Grafana style guides (linked at top) + +--- + +**Last Updated**: 2025-11-21 +**Maintained By**: Alerting Squad From bfda534825f13ff541c19728206717f8cc6db0cf Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 26 Nov 2025 10:40:35 +0100 Subject: [PATCH 119/378] Zanzana: Implement role bindings write APIs (#114385) --- pkg/services/authz/proto/v1/extention.pb.go | 942 +++++++++++------- pkg/services/authz/proto/v1/extention.proto | 24 + .../authz/zanzana/server/server_mutate.go | 7 + .../server/server_mutate_rolebindings.go | 99 ++ .../server/server_mutate_rolebindings_test.go | 106 ++ .../authz/zanzana/server/server_test.go | 4 + 6 files changed, 818 insertions(+), 364 deletions(-) create mode 100644 pkg/services/authz/zanzana/server/server_mutate_rolebindings.go create mode 100644 pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go diff --git a/pkg/services/authz/proto/v1/extention.pb.go b/pkg/services/authz/proto/v1/extention.pb.go index 03a44978db9..358b6a0df95 100644 --- a/pkg/services/authz/proto/v1/extention.pb.go +++ b/pkg/services/authz/proto/v1/extention.pb.go @@ -123,6 +123,8 @@ type MutateOperation struct { // *MutateOperation_UpdateUserOrgRole // *MutateOperation_DeleteUserOrgRole // *MutateOperation_AddUserOrgRole + // *MutateOperation_CreateRoleBinding + // *MutateOperation_DeleteRoleBinding Operation isMutateOperation_Operation `protobuf_oneof:"operation"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -228,6 +230,24 @@ func (x *MutateOperation) GetAddUserOrgRole() *AddUserOrgRoleOperation { return nil } +func (x *MutateOperation) GetCreateRoleBinding() *CreateRoleBindingOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_CreateRoleBinding); ok { + return x.CreateRoleBinding + } + } + return nil +} + +func (x *MutateOperation) GetDeleteRoleBinding() *DeleteRoleBindingOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_DeleteRoleBinding); ok { + return x.DeleteRoleBinding + } + } + return nil +} + type isMutateOperation_Operation interface { isMutateOperation_Operation() } @@ -260,6 +280,14 @@ type MutateOperation_AddUserOrgRole struct { AddUserOrgRole *AddUserOrgRoleOperation `protobuf:"bytes,7,opt,name=add_user_org_role,json=addUserOrgRole,proto3,oneof"` } +type MutateOperation_CreateRoleBinding struct { + CreateRoleBinding *CreateRoleBindingOperation `protobuf:"bytes,8,opt,name=create_role_binding,json=createRoleBinding,proto3,oneof"` +} + +type MutateOperation_DeleteRoleBinding struct { + DeleteRoleBinding *DeleteRoleBindingOperation `protobuf:"bytes,9,opt,name=delete_role_binding,json=deleteRoleBinding,proto3,oneof"` +} + func (*MutateOperation_SetFolderParent) isMutateOperation_Operation() {} func (*MutateOperation_DeleteFolder) isMutateOperation_Operation() {} @@ -274,6 +302,10 @@ func (*MutateOperation_DeleteUserOrgRole) isMutateOperation_Operation() {} func (*MutateOperation_AddUserOrgRole) isMutateOperation_Operation() {} +func (*MutateOperation_CreateRoleBinding) isMutateOperation_Operation() {} + +func (*MutateOperation_DeleteRoleBinding) isMutateOperation_Operation() {} + type SetFolderParentOperation struct { state protoimpl.MessageState `protogen:"open.v1"` // UID of the folder @@ -667,6 +699,150 @@ func (x *DeleteUserOrgRoleOperation) GetRole() string { return "" } +type CreateRoleBindingOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // kind of the identity getting the permission (User/Team/ServiceAccount/BasicRole) + SubjectKind string `protobuf:"bytes,1,opt,name=subject_kind,json=subjectKind,proto3" json:"subject_kind,omitempty"` + // uid of the identity + SubjectName string `protobuf:"bytes,2,opt,name=subject_name,json=subjectName,proto3" json:"subject_name,omitempty"` + // kind of the role (Role/CoreRole/GlobalRole) + RoleKind string `protobuf:"bytes,3,opt,name=role_kind,json=roleKind,proto3" json:"role_kind,omitempty"` + // uid of the role + RoleName string `protobuf:"bytes,4,opt,name=role_name,json=roleName,proto3" json:"role_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateRoleBindingOperation) Reset() { + *x = CreateRoleBindingOperation{} + mi := &file_extention_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateRoleBindingOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRoleBindingOperation) ProtoMessage() {} + +func (x *CreateRoleBindingOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRoleBindingOperation.ProtoReflect.Descriptor instead. +func (*CreateRoleBindingOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{10} +} + +func (x *CreateRoleBindingOperation) GetSubjectKind() string { + if x != nil { + return x.SubjectKind + } + return "" +} + +func (x *CreateRoleBindingOperation) GetSubjectName() string { + if x != nil { + return x.SubjectName + } + return "" +} + +func (x *CreateRoleBindingOperation) GetRoleKind() string { + if x != nil { + return x.RoleKind + } + return "" +} + +func (x *CreateRoleBindingOperation) GetRoleName() string { + if x != nil { + return x.RoleName + } + return "" +} + +type DeleteRoleBindingOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // kind of the identity getting the permission (User/Team/ServiceAccount/BasicRole) + SubjectKind string `protobuf:"bytes,1,opt,name=subject_kind,json=subjectKind,proto3" json:"subject_kind,omitempty"` + // uid of the identity + SubjectName string `protobuf:"bytes,2,opt,name=subject_name,json=subjectName,proto3" json:"subject_name,omitempty"` + // kind of the role (Role/CoreRole/GlobalRole) + RoleKind string `protobuf:"bytes,3,opt,name=role_kind,json=roleKind,proto3" json:"role_kind,omitempty"` + // uid of the role + RoleName string `protobuf:"bytes,4,opt,name=role_name,json=roleName,proto3" json:"role_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRoleBindingOperation) Reset() { + *x = DeleteRoleBindingOperation{} + mi := &file_extention_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRoleBindingOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRoleBindingOperation) ProtoMessage() {} + +func (x *DeleteRoleBindingOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRoleBindingOperation.ProtoReflect.Descriptor instead. +func (*DeleteRoleBindingOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{11} +} + +func (x *DeleteRoleBindingOperation) GetSubjectKind() string { + if x != nil { + return x.SubjectKind + } + return "" +} + +func (x *DeleteRoleBindingOperation) GetSubjectName() string { + if x != nil { + return x.SubjectName + } + return "" +} + +func (x *DeleteRoleBindingOperation) GetRoleKind() string { + if x != nil { + return x.RoleKind + } + return "" +} + +func (x *DeleteRoleBindingOperation) GetRoleName() string { + if x != nil { + return x.RoleName + } + return "" +} + type Resource struct { state protoimpl.MessageState `protogen:"open.v1"` // group of the resource (e.g: "dashboard.grafana.app") @@ -681,7 +857,7 @@ type Resource struct { func (x *Resource) Reset() { *x = Resource{} - mi := &file_extention_proto_msgTypes[10] + mi := &file_extention_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -693,7 +869,7 @@ func (x *Resource) String() string { func (*Resource) ProtoMessage() {} func (x *Resource) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[10] + mi := &file_extention_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -706,7 +882,7 @@ func (x *Resource) ProtoReflect() protoreflect.Message { // Deprecated: Use Resource.ProtoReflect.Descriptor instead. func (*Resource) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{10} + return file_extention_proto_rawDescGZIP(), []int{12} } func (x *Resource) GetGroup() string { @@ -744,7 +920,7 @@ type Permission struct { func (x *Permission) Reset() { *x = Permission{} - mi := &file_extention_proto_msgTypes[11] + mi := &file_extention_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -756,7 +932,7 @@ func (x *Permission) String() string { func (*Permission) ProtoMessage() {} func (x *Permission) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[11] + mi := &file_extention_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -769,7 +945,7 @@ func (x *Permission) ProtoReflect() protoreflect.Message { // Deprecated: Use Permission.ProtoReflect.Descriptor instead. func (*Permission) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{11} + return file_extention_proto_rawDescGZIP(), []int{13} } func (x *Permission) GetKind() string { @@ -805,7 +981,7 @@ type TupleKey struct { func (x *TupleKey) Reset() { *x = TupleKey{} - mi := &file_extention_proto_msgTypes[12] + mi := &file_extention_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -817,7 +993,7 @@ func (x *TupleKey) String() string { func (*TupleKey) ProtoMessage() {} func (x *TupleKey) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[12] + mi := &file_extention_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -830,7 +1006,7 @@ func (x *TupleKey) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleKey.ProtoReflect.Descriptor instead. func (*TupleKey) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{12} + return file_extention_proto_rawDescGZIP(), []int{14} } func (x *TupleKey) GetUser() string { @@ -871,7 +1047,7 @@ type Tuple struct { func (x *Tuple) Reset() { *x = Tuple{} - mi := &file_extention_proto_msgTypes[13] + mi := &file_extention_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -883,7 +1059,7 @@ func (x *Tuple) String() string { func (*Tuple) ProtoMessage() {} func (x *Tuple) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[13] + mi := &file_extention_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -896,7 +1072,7 @@ func (x *Tuple) ProtoReflect() protoreflect.Message { // Deprecated: Use Tuple.ProtoReflect.Descriptor instead. func (*Tuple) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{13} + return file_extention_proto_rawDescGZIP(), []int{15} } func (x *Tuple) GetKey() *TupleKey { @@ -924,7 +1100,7 @@ type TupleKeyWithoutCondition struct { func (x *TupleKeyWithoutCondition) Reset() { *x = TupleKeyWithoutCondition{} - mi := &file_extention_proto_msgTypes[14] + mi := &file_extention_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -936,7 +1112,7 @@ func (x *TupleKeyWithoutCondition) String() string { func (*TupleKeyWithoutCondition) ProtoMessage() {} func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[14] + mi := &file_extention_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -949,7 +1125,7 @@ func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleKeyWithoutCondition.ProtoReflect.Descriptor instead. func (*TupleKeyWithoutCondition) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{14} + return file_extention_proto_rawDescGZIP(), []int{16} } func (x *TupleKeyWithoutCondition) GetUser() string { @@ -983,7 +1159,7 @@ type RelationshipCondition struct { func (x *RelationshipCondition) Reset() { *x = RelationshipCondition{} - mi := &file_extention_proto_msgTypes[15] + mi := &file_extention_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -995,7 +1171,7 @@ func (x *RelationshipCondition) String() string { func (*RelationshipCondition) ProtoMessage() {} func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[15] + mi := &file_extention_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1008,7 +1184,7 @@ func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use RelationshipCondition.ProtoReflect.Descriptor instead. func (*RelationshipCondition) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{15} + return file_extention_proto_rawDescGZIP(), []int{17} } func (x *RelationshipCondition) GetName() string { @@ -1037,7 +1213,7 @@ type ReadRequest struct { func (x *ReadRequest) Reset() { *x = ReadRequest{} - mi := &file_extention_proto_msgTypes[16] + mi := &file_extention_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1049,7 +1225,7 @@ func (x *ReadRequest) String() string { func (*ReadRequest) ProtoMessage() {} func (x *ReadRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[16] + mi := &file_extention_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1062,7 +1238,7 @@ func (x *ReadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead. func (*ReadRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{16} + return file_extention_proto_rawDescGZIP(), []int{18} } func (x *ReadRequest) GetNamespace() string { @@ -1104,7 +1280,7 @@ type ReadRequestTupleKey struct { func (x *ReadRequestTupleKey) Reset() { *x = ReadRequestTupleKey{} - mi := &file_extention_proto_msgTypes[17] + mi := &file_extention_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1116,7 +1292,7 @@ func (x *ReadRequestTupleKey) String() string { func (*ReadRequestTupleKey) ProtoMessage() {} func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[17] + mi := &file_extention_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1129,7 +1305,7 @@ func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRequestTupleKey.ProtoReflect.Descriptor instead. func (*ReadRequestTupleKey) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{17} + return file_extention_proto_rawDescGZIP(), []int{19} } func (x *ReadRequestTupleKey) GetUser() string { @@ -1163,7 +1339,7 @@ type ReadResponse struct { func (x *ReadResponse) Reset() { *x = ReadResponse{} - mi := &file_extention_proto_msgTypes[18] + mi := &file_extention_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1175,7 +1351,7 @@ func (x *ReadResponse) String() string { func (*ReadResponse) ProtoMessage() {} func (x *ReadResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[18] + mi := &file_extention_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1188,7 +1364,7 @@ func (x *ReadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead. func (*ReadResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{18} + return file_extention_proto_rawDescGZIP(), []int{20} } func (x *ReadResponse) GetTuples() []*Tuple { @@ -1214,7 +1390,7 @@ type WriteRequestWrites struct { func (x *WriteRequestWrites) Reset() { *x = WriteRequestWrites{} - mi := &file_extention_proto_msgTypes[19] + mi := &file_extention_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1226,7 +1402,7 @@ func (x *WriteRequestWrites) String() string { func (*WriteRequestWrites) ProtoMessage() {} func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[19] + mi := &file_extention_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1239,7 +1415,7 @@ func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequestWrites.ProtoReflect.Descriptor instead. func (*WriteRequestWrites) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{19} + return file_extention_proto_rawDescGZIP(), []int{21} } func (x *WriteRequestWrites) GetTupleKeys() []*TupleKey { @@ -1258,7 +1434,7 @@ type WriteRequestDeletes struct { func (x *WriteRequestDeletes) Reset() { *x = WriteRequestDeletes{} - mi := &file_extention_proto_msgTypes[20] + mi := &file_extention_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1270,7 +1446,7 @@ func (x *WriteRequestDeletes) String() string { func (*WriteRequestDeletes) ProtoMessage() {} func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[20] + mi := &file_extention_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1283,7 +1459,7 @@ func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequestDeletes.ProtoReflect.Descriptor instead. func (*WriteRequestDeletes) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{20} + return file_extention_proto_rawDescGZIP(), []int{22} } func (x *WriteRequestDeletes) GetTupleKeys() []*TupleKeyWithoutCondition { @@ -1304,7 +1480,7 @@ type WriteRequest struct { func (x *WriteRequest) Reset() { *x = WriteRequest{} - mi := &file_extention_proto_msgTypes[21] + mi := &file_extention_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1316,7 +1492,7 @@ func (x *WriteRequest) String() string { func (*WriteRequest) ProtoMessage() {} func (x *WriteRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[21] + mi := &file_extention_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1329,7 +1505,7 @@ func (x *WriteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequest.ProtoReflect.Descriptor instead. func (*WriteRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{21} + return file_extention_proto_rawDescGZIP(), []int{23} } func (x *WriteRequest) GetNamespace() string { @@ -1361,7 +1537,7 @@ type WriteResponse struct { func (x *WriteResponse) Reset() { *x = WriteResponse{} - mi := &file_extention_proto_msgTypes[22] + mi := &file_extention_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1373,7 +1549,7 @@ func (x *WriteResponse) String() string { func (*WriteResponse) ProtoMessage() {} func (x *WriteResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[22] + mi := &file_extention_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1386,7 +1562,7 @@ func (x *WriteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteResponse.ProtoReflect.Descriptor instead. func (*WriteResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{22} + return file_extention_proto_rawDescGZIP(), []int{24} } type BatchCheckRequest struct { @@ -1400,7 +1576,7 @@ type BatchCheckRequest struct { func (x *BatchCheckRequest) Reset() { *x = BatchCheckRequest{} - mi := &file_extention_proto_msgTypes[23] + mi := &file_extention_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1412,7 +1588,7 @@ func (x *BatchCheckRequest) String() string { func (*BatchCheckRequest) ProtoMessage() {} func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[23] + mi := &file_extention_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1425,7 +1601,7 @@ func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckRequest.ProtoReflect.Descriptor instead. func (*BatchCheckRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{23} + return file_extention_proto_rawDescGZIP(), []int{25} } func (x *BatchCheckRequest) GetSubject() string { @@ -1463,7 +1639,7 @@ type BatchCheckItem struct { func (x *BatchCheckItem) Reset() { *x = BatchCheckItem{} - mi := &file_extention_proto_msgTypes[24] + mi := &file_extention_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1475,7 +1651,7 @@ func (x *BatchCheckItem) String() string { func (*BatchCheckItem) ProtoMessage() {} func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[24] + mi := &file_extention_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1488,7 +1664,7 @@ func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckItem.ProtoReflect.Descriptor instead. func (*BatchCheckItem) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{24} + return file_extention_proto_rawDescGZIP(), []int{26} } func (x *BatchCheckItem) GetVerb() string { @@ -1542,7 +1718,7 @@ type BatchCheckResponse struct { func (x *BatchCheckResponse) Reset() { *x = BatchCheckResponse{} - mi := &file_extention_proto_msgTypes[25] + mi := &file_extention_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1554,7 +1730,7 @@ func (x *BatchCheckResponse) String() string { func (*BatchCheckResponse) ProtoMessage() {} func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[25] + mi := &file_extention_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1567,7 +1743,7 @@ func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckResponse.ProtoReflect.Descriptor instead. func (*BatchCheckResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{25} + return file_extention_proto_rawDescGZIP(), []int{27} } func (x *BatchCheckResponse) GetGroups() map[string]*BatchCheckGroupResource { @@ -1586,7 +1762,7 @@ type BatchCheckGroupResource struct { func (x *BatchCheckGroupResource) Reset() { *x = BatchCheckGroupResource{} - mi := &file_extention_proto_msgTypes[26] + mi := &file_extention_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1598,7 +1774,7 @@ func (x *BatchCheckGroupResource) String() string { func (*BatchCheckGroupResource) ProtoMessage() {} func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[26] + mi := &file_extention_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1611,7 +1787,7 @@ func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckGroupResource.ProtoReflect.Descriptor instead. func (*BatchCheckGroupResource) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{26} + return file_extention_proto_rawDescGZIP(), []int{28} } func (x *BatchCheckGroupResource) GetItems() map[string]bool { @@ -1631,7 +1807,7 @@ type QueryRequest struct { func (x *QueryRequest) Reset() { *x = QueryRequest{} - mi := &file_extention_proto_msgTypes[27] + mi := &file_extention_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1643,7 +1819,7 @@ func (x *QueryRequest) String() string { func (*QueryRequest) ProtoMessage() {} func (x *QueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[27] + mi := &file_extention_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1656,7 +1832,7 @@ func (x *QueryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead. func (*QueryRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{27} + return file_extention_proto_rawDescGZIP(), []int{29} } func (x *QueryRequest) GetNamespace() string { @@ -1685,7 +1861,7 @@ type QueryResponse struct { func (x *QueryResponse) Reset() { *x = QueryResponse{} - mi := &file_extention_proto_msgTypes[28] + mi := &file_extention_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1697,7 +1873,7 @@ func (x *QueryResponse) String() string { func (*QueryResponse) ProtoMessage() {} func (x *QueryResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[28] + mi := &file_extention_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1710,7 +1886,7 @@ func (x *QueryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryResponse.ProtoReflect.Descriptor instead. func (*QueryResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{28} + return file_extention_proto_rawDescGZIP(), []int{30} } func (x *QueryResponse) GetResult() isQueryResponse_Result { @@ -1751,7 +1927,7 @@ type QueryOperation struct { func (x *QueryOperation) Reset() { *x = QueryOperation{} - mi := &file_extention_proto_msgTypes[29] + mi := &file_extention_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1763,7 +1939,7 @@ func (x *QueryOperation) String() string { func (*QueryOperation) ProtoMessage() {} func (x *QueryOperation) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[29] + mi := &file_extention_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1776,7 +1952,7 @@ func (x *QueryOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryOperation.ProtoReflect.Descriptor instead. func (*QueryOperation) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{29} + return file_extention_proto_rawDescGZIP(), []int{31} } func (x *QueryOperation) GetOperation() isQueryOperation_Operation { @@ -1815,7 +1991,7 @@ type GetFolderParentsQuery struct { func (x *GetFolderParentsQuery) Reset() { *x = GetFolderParentsQuery{} - mi := &file_extention_proto_msgTypes[30] + mi := &file_extention_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1827,7 +2003,7 @@ func (x *GetFolderParentsQuery) String() string { func (*GetFolderParentsQuery) ProtoMessage() {} func (x *GetFolderParentsQuery) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[30] + mi := &file_extention_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1840,7 +2016,7 @@ func (x *GetFolderParentsQuery) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFolderParentsQuery.ProtoReflect.Descriptor instead. func (*GetFolderParentsQuery) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{30} + return file_extention_proto_rawDescGZIP(), []int{32} } func (x *GetFolderParentsQuery) GetFolder() string { @@ -1860,7 +2036,7 @@ type GetFolderParentsResult struct { func (x *GetFolderParentsResult) Reset() { *x = GetFolderParentsResult{} - mi := &file_extention_proto_msgTypes[31] + mi := &file_extention_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1872,7 +2048,7 @@ func (x *GetFolderParentsResult) String() string { func (*GetFolderParentsResult) ProtoMessage() {} func (x *GetFolderParentsResult) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[31] + mi := &file_extention_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1885,7 +2061,7 @@ func (x *GetFolderParentsResult) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFolderParentsResult.ProtoReflect.Descriptor instead. func (*GetFolderParentsResult) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{31} + return file_extention_proto_rawDescGZIP(), []int{33} } func (x *GetFolderParentsResult) GetParentUids() []string { @@ -1914,7 +2090,7 @@ var file_extention_proto_rawDesc = string([]byte{ 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x10, 0x0a, 0x0e, 0x4d, 0x75, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa8, 0x05, 0x0a, 0x0f, 0x4d, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xec, 0x06, 0x0a, 0x0f, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5a, 0x0a, 0x11, 0x73, 0x65, 0x74, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, @@ -1956,245 +2132,277 @@ var file_extention_proto_rawDesc = string([]byte{ 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0e, 0x61, 0x64, 0x64, 0x55, 0x73, 0x65, - 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x73, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, - 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, - 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, - 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, - 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x70, 0x0a, 0x15, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, - 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, - 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, - 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x95, 0x01, 0x0a, - 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, + 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x60, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, + 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x60, 0x0a, 0x13, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, + 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x0b, 0x0a, 0x09, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x73, 0x0a, 0x18, 0x53, 0x65, 0x74, + 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, + 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, + 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, + 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, + 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x70, + 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, + 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, + 0x22, 0x95, 0x01, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, + 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, - 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, - 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, - 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x41, 0x0a, 0x17, - 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, + 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x19, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x12, 0x3e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x22, 0x41, 0x0a, 0x17, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, + 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, + 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, + 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, + 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, + 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, - 0x44, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, - 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, - 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, - 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, - 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x50, 0x0a, 0x08, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, - 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x48, 0x0a, - 0x0a, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6b, - 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x54, 0x75, 0x70, 0x6c, - 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x47, 0x0a, 0x09, - 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, - 0x70, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x71, 0x0a, 0x05, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x12, 0x2e, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, - 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, - 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x62, 0x0a, 0x18, 0x54, 0x75, 0x70, 0x6c, - 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x5e, 0x0a, 0x15, - 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, 0x6e, - 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, - 0x75, 0x63, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xda, 0x01, 0x0a, - 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x74, 0x75, - 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, + 0x9c, 0x01, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, + 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, + 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, 0x6e, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, 0x6e, + 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x9c, + 0x01, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, + 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, + 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, 0x6e, 0x64, + 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x50, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, + 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, + 0x48, 0x0a, 0x0a, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, + 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, + 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x54, 0x75, + 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x47, + 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x68, 0x69, 0x70, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x71, 0x0a, 0x05, 0x54, 0x75, 0x70, 0x6c, 0x65, + 0x12, 0x2e, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, - 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, - 0x12, 0x38, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, - 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x52, 0x65, 0x61, - 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, - 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x70, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x75, 0x70, 0x6c, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, - 0x70, 0x6c, 0x65, 0x52, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x63, - 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x12, 0x57, 0x72, - 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, - 0x12, 0x3b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, - 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, - 0x65, 0x79, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x62, 0x0a, - 0x13, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, - 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, + 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x62, 0x0a, 0x18, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, - 0x73, 0x22, 0xaf, 0x01, 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x12, 0x3e, 0x0a, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, 0x52, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, - 0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x11, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, - 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xa4, 0x01, 0x0a, - 0x0e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x12, - 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, - 0x65, 0x72, 0x62, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x75, 0x62, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x73, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, - 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, - 0x64, 0x65, 0x72, 0x22, 0xc8, 0x01, 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x06, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x61, 0x75, 0x74, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x5e, + 0x0a, 0x15, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, + 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xda, + 0x01, 0x0a, 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, + 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, + 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, + 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2d, 0x0a, 0x12, + 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, + 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x52, + 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, + 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x70, 0x0a, 0x0c, 0x52, 0x65, + 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x75, + 0x70, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x1a, 0x66, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, - 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa1, - 0x01, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, 0x74, - 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x61, 0x75, 0x74, 0x68, - 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x49, 0x74, 0x65, 0x6d, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0x6e, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x12, 0x40, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, - 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, - 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x6f, 0x6c, 0x64, 0x65, - 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x22, 0x78, 0x0a, 0x0e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x59, 0x0a, 0x12, 0x67, 0x65, 0x74, 0x5f, 0x66, 0x6f, 0x6c, 0x64, - 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, - 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x51, 0x75, 0x65, 0x72, 0x79, 0x48, 0x00, 0x52, 0x10, 0x67, - 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x42, - 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2f, 0x0a, 0x15, - 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x39, 0x0a, - 0x16, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x65, 0x6e, - 0x74, 0x5f, 0x75, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x61, - 0x72, 0x65, 0x6e, 0x74, 0x55, 0x69, 0x64, 0x73, 0x32, 0xac, 0x03, 0x0a, 0x15, 0x41, 0x75, 0x74, - 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x0a, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, - 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, - 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, - 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, - 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x57, 0x72, - 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, + 0x54, 0x75, 0x70, 0x6c, 0x65, 0x52, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, + 0x12, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, + 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x12, + 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, + 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, + 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, + 0x65, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, + 0x62, 0x0a, 0x13, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, + 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, + 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, + 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, + 0x65, 0x79, 0x73, 0x22, 0xaf, 0x01, 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, 0x52, 0x06, 0x77, 0x72, 0x69, 0x74, + 0x65, 0x73, 0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x11, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, + 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, + 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xa4, + 0x01, 0x0a, 0x0e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, + 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x76, 0x65, 0x72, 0x62, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, + 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x73, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, + 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0xc8, 0x01, 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x06, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x1a, 0x66, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x22, 0xa1, 0x01, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x05, + 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x49, 0x74, + 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6e, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x12, 0x40, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, + 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x22, 0x78, 0x0a, 0x0e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x59, 0x0a, 0x12, 0x67, 0x65, 0x74, 0x5f, 0x66, 0x6f, + 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, + 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x51, 0x75, 0x65, 0x72, 0x79, 0x48, 0x00, 0x52, + 0x10, 0x67, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, + 0x73, 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2f, + 0x0a, 0x15, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, + 0x74, 0x73, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, + 0x39, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x5f, 0x75, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, + 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x55, 0x69, 0x64, 0x73, 0x32, 0xac, 0x03, 0x0a, 0x15, 0x41, + 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x0a, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, + 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x06, 0x4d, 0x75, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x51, 0x75, 0x65, - 0x72, 0x79, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x06, 0x4d, 0x75, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x51, + 0x75, 0x65, 0x72, 0x79, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, - 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, - 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, + 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, + 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -2209,7 +2417,7 @@ func file_extention_proto_rawDescGZIP() []byte { return file_extention_proto_rawDescData } -var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 34) +var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 36) var file_extention_proto_goTypes = []any{ (*MutateRequest)(nil), // 0: authz.extention.v1.MutateRequest (*MutateResponse)(nil), // 1: authz.extention.v1.MutateResponse @@ -2221,33 +2429,35 @@ var file_extention_proto_goTypes = []any{ (*AddUserOrgRoleOperation)(nil), // 7: authz.extention.v1.AddUserOrgRoleOperation (*UpdateUserOrgRoleOperation)(nil), // 8: authz.extention.v1.UpdateUserOrgRoleOperation (*DeleteUserOrgRoleOperation)(nil), // 9: authz.extention.v1.DeleteUserOrgRoleOperation - (*Resource)(nil), // 10: authz.extention.v1.Resource - (*Permission)(nil), // 11: authz.extention.v1.Permission - (*TupleKey)(nil), // 12: authz.extention.v1.TupleKey - (*Tuple)(nil), // 13: authz.extention.v1.Tuple - (*TupleKeyWithoutCondition)(nil), // 14: authz.extention.v1.TupleKeyWithoutCondition - (*RelationshipCondition)(nil), // 15: authz.extention.v1.RelationshipCondition - (*ReadRequest)(nil), // 16: authz.extention.v1.ReadRequest - (*ReadRequestTupleKey)(nil), // 17: authz.extention.v1.ReadRequestTupleKey - (*ReadResponse)(nil), // 18: authz.extention.v1.ReadResponse - (*WriteRequestWrites)(nil), // 19: authz.extention.v1.WriteRequestWrites - (*WriteRequestDeletes)(nil), // 20: authz.extention.v1.WriteRequestDeletes - (*WriteRequest)(nil), // 21: authz.extention.v1.WriteRequest - (*WriteResponse)(nil), // 22: authz.extention.v1.WriteResponse - (*BatchCheckRequest)(nil), // 23: authz.extention.v1.BatchCheckRequest - (*BatchCheckItem)(nil), // 24: authz.extention.v1.BatchCheckItem - (*BatchCheckResponse)(nil), // 25: authz.extention.v1.BatchCheckResponse - (*BatchCheckGroupResource)(nil), // 26: authz.extention.v1.BatchCheckGroupResource - (*QueryRequest)(nil), // 27: authz.extention.v1.QueryRequest - (*QueryResponse)(nil), // 28: authz.extention.v1.QueryResponse - (*QueryOperation)(nil), // 29: authz.extention.v1.QueryOperation - (*GetFolderParentsQuery)(nil), // 30: authz.extention.v1.GetFolderParentsQuery - (*GetFolderParentsResult)(nil), // 31: authz.extention.v1.GetFolderParentsResult - nil, // 32: authz.extention.v1.BatchCheckResponse.GroupsEntry - nil, // 33: authz.extention.v1.BatchCheckGroupResource.ItemsEntry - (*timestamppb.Timestamp)(nil), // 34: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 35: google.protobuf.Struct - (*wrapperspb.Int32Value)(nil), // 36: google.protobuf.Int32Value + (*CreateRoleBindingOperation)(nil), // 10: authz.extention.v1.CreateRoleBindingOperation + (*DeleteRoleBindingOperation)(nil), // 11: authz.extention.v1.DeleteRoleBindingOperation + (*Resource)(nil), // 12: authz.extention.v1.Resource + (*Permission)(nil), // 13: authz.extention.v1.Permission + (*TupleKey)(nil), // 14: authz.extention.v1.TupleKey + (*Tuple)(nil), // 15: authz.extention.v1.Tuple + (*TupleKeyWithoutCondition)(nil), // 16: authz.extention.v1.TupleKeyWithoutCondition + (*RelationshipCondition)(nil), // 17: authz.extention.v1.RelationshipCondition + (*ReadRequest)(nil), // 18: authz.extention.v1.ReadRequest + (*ReadRequestTupleKey)(nil), // 19: authz.extention.v1.ReadRequestTupleKey + (*ReadResponse)(nil), // 20: authz.extention.v1.ReadResponse + (*WriteRequestWrites)(nil), // 21: authz.extention.v1.WriteRequestWrites + (*WriteRequestDeletes)(nil), // 22: authz.extention.v1.WriteRequestDeletes + (*WriteRequest)(nil), // 23: authz.extention.v1.WriteRequest + (*WriteResponse)(nil), // 24: authz.extention.v1.WriteResponse + (*BatchCheckRequest)(nil), // 25: authz.extention.v1.BatchCheckRequest + (*BatchCheckItem)(nil), // 26: authz.extention.v1.BatchCheckItem + (*BatchCheckResponse)(nil), // 27: authz.extention.v1.BatchCheckResponse + (*BatchCheckGroupResource)(nil), // 28: authz.extention.v1.BatchCheckGroupResource + (*QueryRequest)(nil), // 29: authz.extention.v1.QueryRequest + (*QueryResponse)(nil), // 30: authz.extention.v1.QueryResponse + (*QueryOperation)(nil), // 31: authz.extention.v1.QueryOperation + (*GetFolderParentsQuery)(nil), // 32: authz.extention.v1.GetFolderParentsQuery + (*GetFolderParentsResult)(nil), // 33: authz.extention.v1.GetFolderParentsResult + nil, // 34: authz.extention.v1.BatchCheckResponse.GroupsEntry + nil, // 35: authz.extention.v1.BatchCheckGroupResource.ItemsEntry + (*timestamppb.Timestamp)(nil), // 36: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 37: google.protobuf.Struct + (*wrapperspb.Int32Value)(nil), // 38: google.protobuf.Int32Value } var file_extention_proto_depIdxs = []int32{ 2, // 0: authz.extention.v1.MutateRequest.operations:type_name -> authz.extention.v1.MutateOperation @@ -2258,43 +2468,45 @@ var file_extention_proto_depIdxs = []int32{ 8, // 5: authz.extention.v1.MutateOperation.update_user_org_role:type_name -> authz.extention.v1.UpdateUserOrgRoleOperation 9, // 6: authz.extention.v1.MutateOperation.delete_user_org_role:type_name -> authz.extention.v1.DeleteUserOrgRoleOperation 7, // 7: authz.extention.v1.MutateOperation.add_user_org_role:type_name -> authz.extention.v1.AddUserOrgRoleOperation - 10, // 8: authz.extention.v1.CreatePermissionOperation.resource:type_name -> authz.extention.v1.Resource - 11, // 9: authz.extention.v1.CreatePermissionOperation.permission:type_name -> authz.extention.v1.Permission - 10, // 10: authz.extention.v1.DeletePermissionOperation.resource:type_name -> authz.extention.v1.Resource - 11, // 11: authz.extention.v1.DeletePermissionOperation.permission:type_name -> authz.extention.v1.Permission - 15, // 12: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition - 12, // 13: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey - 34, // 14: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp - 35, // 15: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct - 17, // 16: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey - 36, // 17: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value - 13, // 18: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple - 12, // 19: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey - 14, // 20: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition - 19, // 21: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites - 20, // 22: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes - 24, // 23: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem - 32, // 24: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry - 33, // 25: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry - 29, // 26: authz.extention.v1.QueryRequest.operation:type_name -> authz.extention.v1.QueryOperation - 31, // 27: authz.extention.v1.QueryResponse.folder_parents:type_name -> authz.extention.v1.GetFolderParentsResult - 30, // 28: authz.extention.v1.QueryOperation.get_folder_parents:type_name -> authz.extention.v1.GetFolderParentsQuery - 26, // 29: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource - 23, // 30: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest - 16, // 31: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest - 21, // 32: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest - 0, // 33: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest - 27, // 34: authz.extention.v1.AuthzExtentionService.Query:input_type -> authz.extention.v1.QueryRequest - 25, // 35: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse - 18, // 36: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse - 22, // 37: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse - 1, // 38: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse - 28, // 39: authz.extention.v1.AuthzExtentionService.Query:output_type -> authz.extention.v1.QueryResponse - 35, // [35:40] is the sub-list for method output_type - 30, // [30:35] is the sub-list for method input_type - 30, // [30:30] is the sub-list for extension type_name - 30, // [30:30] is the sub-list for extension extendee - 0, // [0:30] is the sub-list for field type_name + 10, // 8: authz.extention.v1.MutateOperation.create_role_binding:type_name -> authz.extention.v1.CreateRoleBindingOperation + 11, // 9: authz.extention.v1.MutateOperation.delete_role_binding:type_name -> authz.extention.v1.DeleteRoleBindingOperation + 12, // 10: authz.extention.v1.CreatePermissionOperation.resource:type_name -> authz.extention.v1.Resource + 13, // 11: authz.extention.v1.CreatePermissionOperation.permission:type_name -> authz.extention.v1.Permission + 12, // 12: authz.extention.v1.DeletePermissionOperation.resource:type_name -> authz.extention.v1.Resource + 13, // 13: authz.extention.v1.DeletePermissionOperation.permission:type_name -> authz.extention.v1.Permission + 17, // 14: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition + 14, // 15: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey + 36, // 16: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp + 37, // 17: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct + 19, // 18: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey + 38, // 19: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value + 15, // 20: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple + 14, // 21: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey + 16, // 22: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition + 21, // 23: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites + 22, // 24: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes + 26, // 25: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem + 34, // 26: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry + 35, // 27: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry + 31, // 28: authz.extention.v1.QueryRequest.operation:type_name -> authz.extention.v1.QueryOperation + 33, // 29: authz.extention.v1.QueryResponse.folder_parents:type_name -> authz.extention.v1.GetFolderParentsResult + 32, // 30: authz.extention.v1.QueryOperation.get_folder_parents:type_name -> authz.extention.v1.GetFolderParentsQuery + 28, // 31: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource + 25, // 32: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest + 18, // 33: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest + 23, // 34: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest + 0, // 35: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest + 29, // 36: authz.extention.v1.AuthzExtentionService.Query:input_type -> authz.extention.v1.QueryRequest + 27, // 37: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse + 20, // 38: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse + 24, // 39: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse + 1, // 40: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse + 30, // 41: authz.extention.v1.AuthzExtentionService.Query:output_type -> authz.extention.v1.QueryResponse + 37, // [37:42] is the sub-list for method output_type + 32, // [32:37] is the sub-list for method input_type + 32, // [32:32] is the sub-list for extension type_name + 32, // [32:32] is the sub-list for extension extendee + 0, // [0:32] is the sub-list for field type_name } func init() { file_extention_proto_init() } @@ -2310,11 +2522,13 @@ func file_extention_proto_init() { (*MutateOperation_UpdateUserOrgRole)(nil), (*MutateOperation_DeleteUserOrgRole)(nil), (*MutateOperation_AddUserOrgRole)(nil), + (*MutateOperation_CreateRoleBinding)(nil), + (*MutateOperation_DeleteRoleBinding)(nil), } - file_extention_proto_msgTypes[28].OneofWrappers = []any{ + file_extention_proto_msgTypes[30].OneofWrappers = []any{ (*QueryResponse_FolderParents)(nil), } - file_extention_proto_msgTypes[29].OneofWrappers = []any{ + file_extention_proto_msgTypes[31].OneofWrappers = []any{ (*QueryOperation_GetFolderParents)(nil), } type x struct{} @@ -2323,7 +2537,7 @@ func file_extention_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_extention_proto_rawDesc), len(file_extention_proto_rawDesc)), NumEnums: 0, - NumMessages: 34, + NumMessages: 36, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/services/authz/proto/v1/extention.proto b/pkg/services/authz/proto/v1/extention.proto index 2a11b8c5b97..80886ae13b3 100644 --- a/pkg/services/authz/proto/v1/extention.proto +++ b/pkg/services/authz/proto/v1/extention.proto @@ -34,6 +34,8 @@ message MutateOperation { UpdateUserOrgRoleOperation update_user_org_role = 5; DeleteUserOrgRoleOperation delete_user_org_role = 6; AddUserOrgRoleOperation add_user_org_role = 7; + CreateRoleBindingOperation create_role_binding = 8; + DeleteRoleBindingOperation delete_role_binding = 9; } } @@ -87,6 +89,28 @@ message DeleteUserOrgRoleOperation { string role = 2; } +message CreateRoleBindingOperation { + // kind of the identity getting the permission (User/Team/ServiceAccount/BasicRole) + string subject_kind = 1; + // uid of the identity + string subject_name = 2; + // kind of the role (Role/CoreRole/GlobalRole) + string role_kind = 3; + // uid of the role + string role_name = 4; +} + +message DeleteRoleBindingOperation { + // kind of the identity getting the permission (User/Team/ServiceAccount/BasicRole) + string subject_kind = 1; + // uid of the identity + string subject_name = 2; + // kind of the role (Role/CoreRole/GlobalRole) + string role_kind = 3; + // uid of the role + string role_name = 4; +} + message Resource { // group of the resource (e.g: "dashboard.grafana.app") string group = 1; diff --git a/pkg/services/authz/zanzana/server/server_mutate.go b/pkg/services/authz/zanzana/server/server_mutate.go index 3a691ed6e12..45d40df8173 100644 --- a/pkg/services/authz/zanzana/server/server_mutate.go +++ b/pkg/services/authz/zanzana/server/server_mutate.go @@ -15,6 +15,7 @@ const ( OperationGroupFolder OperationGroup = "folder" OperationGroupPermission OperationGroup = "permission" OperationGroupUserOrgRole OperationGroup = "user_org_role" + OperationGroupRoleBinding OperationGroup = "role_binding" ) func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) { @@ -63,6 +64,10 @@ func (s *Server) mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au if err := s.mutateOrgRoles(ctx, storeInf, operations); err != nil { return nil, fmt.Errorf("failed to mutate org roles: %w", err) } + case OperationGroupRoleBinding: + if err := s.mutateRoleBindings(ctx, storeInf, operations); err != nil { + return nil, fmt.Errorf("failed to mutate role bindings: %w", err) + } default: s.logger.Warn("unsupported operation group", "operationGroup", operationGroup) } @@ -79,6 +84,8 @@ func getOperationGroup(operation *authzextv1.MutateOperation) (OperationGroup, e return OperationGroupPermission, nil case *authzextv1.MutateOperation_UpdateUserOrgRole, *authzextv1.MutateOperation_DeleteUserOrgRole, *authzextv1.MutateOperation_AddUserOrgRole: return OperationGroupUserOrgRole, nil + case *authzextv1.MutateOperation_CreateRoleBinding, *authzextv1.MutateOperation_DeleteRoleBinding: + return OperationGroupRoleBinding, nil } return OperationGroup(""), errors.New("unsupported mutate operation type") } diff --git a/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go b/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go new file mode 100644 index 00000000000..3b18566bee2 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go @@ -0,0 +1,99 @@ +package server + +import ( + "context" + "fmt" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + zanzana "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func (s *Server) mutateRoleBindings(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error { + ctx, span := s.tracer.Start(ctx, "server.mutateRoleBindings") + defer span.End() + + writeTuples := make([]*openfgav1.TupleKey, 0) + deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + for _, operation := range operations { + switch op := operation.Operation.(type) { + case *authzextv1.MutateOperation_CreateRoleBinding: + r := op.CreateRoleBinding + tuple, err := s.getRoleBindingTuple(ctx, r.SubjectKind, r.SubjectName, r.RoleName) + if err != nil { + return err + } + writeTuples = append(writeTuples, tuple) + case *authzextv1.MutateOperation_DeleteRoleBinding: + r := op.DeleteRoleBinding + tuple, err := s.getRoleBindingTuple(ctx, r.SubjectKind, r.SubjectName, r.RoleName) + if err != nil { + return err + } + writeTuple := &openfgav1.TupleKeyWithoutCondition{ + User: tuple.User, + Relation: tuple.Relation, + Object: tuple.Object, + } + deleteTuples = append(deleteTuples, writeTuple) + default: + s.logger.Debug("unsupported mutate operation", "operation", op) + } + } + + writeReq := &openfgav1.WriteRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + } + if len(writeTuples) > 0 { + writeReq.Writes = &openfgav1.WriteRequestWrites{ + TupleKeys: writeTuples, + OnDuplicate: "ignore", + } + } + if len(deleteTuples) > 0 { + writeReq.Deletes = &openfgav1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + OnMissing: "ignore", + } + } + + _, err := s.openfga.Write(ctx, writeReq) + if err != nil { + s.logger.Error("failed to write resource role binding tuples", "error", err) + return err + } + + return nil +} + +func (s *Server) getRoleBindingTuple(ctx context.Context, subjectKind string, subjectName string, roleName string) (*openfgav1.TupleKey, error) { + zanzanaType := "" + subjectRelation := "" + + switch subjectKind { + case string(iamv0.RoleBindingSpecSubjectKindUser): + zanzanaType = zanzana.TypeUser + case string(iamv0.RoleBindingSpecSubjectKindTeam): + zanzanaType = zanzana.TypeTeam + subjectRelation = zanzana.RelationTeamMember + case string(iamv0.RoleBindingSpecSubjectKindServiceAccount): + zanzanaType = zanzana.TypeServiceAccount + case string(iamv0.RoleBindingSpecSubjectKindBasicRole): + zanzanaType = zanzana.TypeRole + subjectRelation = zanzana.RelationAssignee + default: + return nil, fmt.Errorf("invalid subject kind: %s", subjectKind) + } + + tuple := &openfgav1.TupleKey{ + User: zanzana.NewTupleEntry(zanzanaType, subjectName, subjectRelation), + Relation: zanzana.RelationAssignee, + Object: zanzana.NewTupleEntry(zanzana.TypeRole, roleName, ""), + } + + return tuple, nil +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go b/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go new file mode 100644 index 00000000000..87db285c360 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go @@ -0,0 +1,106 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupMutateRoleBindings(t *testing.T, srv *Server) *Server { + t.Helper() + + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewTuple("user:1", common.RelationAssignee, "role:foo_viewer"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testMutateRoleBindings(t *testing.T, srv *Server) { + setupMutateRoleBindings(t, srv) + + t.Run("should update user role and delete old role", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_CreateRoleBinding{ + CreateRoleBinding: &v1.CreateRoleBindingOperation{ + SubjectKind: "User", + SubjectName: "1", + RoleKind: "Role", + RoleName: "foo_editor", + }, + }, + }, + { + Operation: &v1.MutateOperation_DeleteRoleBinding{ + DeleteRoleBinding: &v1.DeleteRoleBindingOperation{ + SubjectKind: "User", + SubjectName: "1", + RoleKind: "Role", + RoleName: "foo_viewer", + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationAssignee, + Object: "role:foo_editor", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "user:1", res.Tuples[0].Key.User) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationAssignee, + Object: "role:foo_viewer", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + }) + + t.Run("should assign role to basic role", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_CreateRoleBinding{ + CreateRoleBinding: &v1.CreateRoleBindingOperation{ + SubjectKind: "BasicRole", + SubjectName: "basic_viewer", + RoleKind: "Role", + RoleName: "foo_bar", + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationAssignee, + Object: "role:foo_bar", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "role:basic_viewer#assignee", res.Tuples[0].Key.User) + }) +} diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index df4f90cf85f..7275fd411af 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -136,6 +136,10 @@ func TestIntegrationServer(t *testing.T) { t.Run("test query folders", func(t *testing.T) { testQueryFolders(t, srv) }) + + t.Run("test mutate role bindings", func(t *testing.T) { + testMutateRoleBindings(t, srv) + }) } func setupOpenFGAServer(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { From 136a0eb4d6037ddd3732f530365d5e8ff6cfa9dd Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 09:51:56 +0000 Subject: [PATCH 120/378] I18n: Download translations from Crowdin (#114456) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 16 +++++++++------- public/locales/de-DE/grafana.json | 16 +++++++++------- public/locales/es-ES/grafana.json | 16 +++++++++------- public/locales/fr-FR/grafana.json | 16 +++++++++------- public/locales/hu-HU/grafana.json | 16 +++++++++------- public/locales/id-ID/grafana.json | 16 +++++++++------- public/locales/it-IT/grafana.json | 16 +++++++++------- public/locales/ja-JP/grafana.json | 16 +++++++++------- public/locales/ko-KR/grafana.json | 16 +++++++++------- public/locales/nl-NL/grafana.json | 16 +++++++++------- public/locales/pl-PL/grafana.json | 16 +++++++++------- public/locales/pt-BR/grafana.json | 16 +++++++++------- public/locales/pt-PT/grafana.json | 16 +++++++++------- public/locales/ru-RU/grafana.json | 16 +++++++++------- public/locales/sv-SE/grafana.json | 16 +++++++++------- public/locales/tr-TR/grafana.json | 16 +++++++++------- public/locales/zh-Hans/grafana.json | 16 +++++++++------- public/locales/zh-Hant/grafana.json | 16 +++++++++------- 18 files changed, 162 insertions(+), 126 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index ac6538d3719..0a61dfbdd25 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -2648,7 +2648,9 @@ "placeholder-search-input": "Hledejte podle názvu nebo zadejte dotaz na filtr…" }, "grafana-folder": "Složka Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Kondice", "label": { "hide": "Skrýt", @@ -2659,6 +2661,7 @@ "placeholder-all-data-sources": "Všechny zdroje dat", "placeholder-contact-point": "Vyberte kontaktní bod", "placeholder-data-sources": "Vyberte zdroje dat", + "placeholder-group-search": "", "placeholder-labels": "Vyberte štítky", "plugin-rules": "Pravidla pluginu", "rule-source": { @@ -6228,8 +6231,8 @@ "placeholder-search-for": "Hledat…", "radio-options": { "label": { - "suggestions": "Návrhy", - "visualizations": "Vizualizace" + "all-visualizations": "", + "suggestions": "Návrhy" } }, "title-close": "Zavřít" @@ -8175,8 +8178,7 @@ "edit-pane": { "go-back": "Přejít zpět" } - }, - "select-group": "Vybrat skupinu" + } }, "grafana-data": { "datetime": { @@ -11144,8 +11146,8 @@ "tooltip-delete": "Odstranit" }, "visualization-suggestions": { - "based-on-current-data": "Na základě současných dat", - "no-results-matched-your-query": "Vašemu dotazu neodpovídají žádné výsledky" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Nenašly jsme žádnou shodu s vaším dotazem" diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 47445cd97ed..8da43ece8a3 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Nach Name suchen oder Filterabfrage eingeben …" }, "grafana-folder": "Grafana-Ordner", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Zustand", "label": { "hide": "Ausblenden", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Alle Datenquellen", "placeholder-contact-point": "Kontaktpunkt auswählen", "placeholder-data-sources": "Datenquellen auswählen", + "placeholder-group-search": "", "placeholder-labels": "Labels auswählen", "plugin-rules": "Plugin-Regeln", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Suche nach ...", "radio-options": { "label": { - "suggestions": "Vorschläge", - "visualizations": "Visualisierungen" + "all-visualizations": "", + "suggestions": "Vorschläge" } }, "title-close": "Schließen" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Zurückgehen" } - }, - "select-group": "Gruppe auswählen" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Löschen" }, "visualization-suggestions": { - "based-on-current-data": "Basierend auf aktuellen Daten", - "no-results-matched-your-query": "Keine passenden Ergebnisse zu Ihrer Abfrage" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Es konnte nichts gefunden werden, was Ihrer Abfrage entspricht" diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 653d7942c02..f25a23bb253 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Buscar por nombre o introducir consulta de filtro..." }, "grafana-folder": "Carpeta de Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Salud", "label": { "hide": "Ocultar", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Todas las fuentes de datos", "placeholder-contact-point": "Seleccionar punto de contacto", "placeholder-data-sources": "Seleccionar fuentes de datos", + "placeholder-group-search": "", "placeholder-labels": "Seleccionar etiquetas", "plugin-rules": "Reglas del plugin", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Buscar...", "radio-options": { "label": { - "suggestions": "Sugerencias", - "visualizations": "Visualizaciones" + "all-visualizations": "", + "suggestions": "Sugerencias" } }, "title-close": "Cerrar" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Volver" } - }, - "select-group": "Seleccionar grupo" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Eliminar" }, "visualization-suggestions": { - "based-on-current-data": "Basado en datos actuales", - "no-results-matched-your-query": "No hay resultados que coincidan con tu consulta" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "No se ha encontrado nada que coincida con tu consulta" diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index fecfc76d3ea..5d716092b57 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Rechercher par nom ou saisir une requête de filtre..." }, "grafana-folder": "Dossier Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Santé", "label": { "hide": "Masquer", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Toutes les sources de données", "placeholder-contact-point": "Sélectionner un point de contact", "placeholder-data-sources": "Sélectionner des sources de données", + "placeholder-group-search": "", "placeholder-labels": "Sélectionner des étiquettes", "plugin-rules": "Règles de plugin", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Rechercher…", "radio-options": { "label": { - "suggestions": "Suggestions", - "visualizations": "Visualisations" + "all-visualizations": "", + "suggestions": "Suggestions" } }, "title-close": "Fermer" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Retour" } - }, - "select-group": "Sélectionner un groupe" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Supprimer" }, "visualization-suggestions": { - "based-on-current-data": "Sur la base des données actuelles", - "no-results-matched-your-query": "Aucun résultat ne correspond à votre requête" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Impossible de trouver quoi que ce soit correspondant à votre requête" diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index e691d36eff2..d0dbf31ba9e 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Keresés név alapján, vagy szűrőlekérdezés megadása..." }, "grafana-folder": "Grafana mappa", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Állapot", "label": { "hide": "Elrejtés", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Összes adatforrás", "placeholder-contact-point": "Kapcsolattartási pont kiválasztása", "placeholder-data-sources": "Adatforrások kiválasztása", + "placeholder-group-search": "", "placeholder-labels": "Címkék kiválasztása", "plugin-rules": "Bővítményszabályok", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Keresés a következőre…", "radio-options": { "label": { - "suggestions": "Javaslatok", - "visualizations": "Vizualizációk" + "all-visualizations": "", + "suggestions": "Javaslatok" } }, "title-close": "Bezárás" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Visszalépés" } - }, - "select-group": "Csoport kijelölése" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Törlés" }, "visualization-suggestions": { - "based-on-current-data": "Aktuális adatok alapján", - "no-results-matched-your-query": "Nincs találat a lekérdezésre" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Nincs találat a lekérdezésre" diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index ff5ecf95800..3880215b56c 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -2612,7 +2612,9 @@ "placeholder-search-input": "Cari berdasarkan nama atau masukkan kueri filter..." }, "grafana-folder": "Folder Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Kesehatan", "label": { "hide": "Sembunyikan", @@ -2623,6 +2625,7 @@ "placeholder-all-data-sources": "Semua sumber data", "placeholder-contact-point": "Pilih titik kontak", "placeholder-data-sources": "Pilih sumber data", + "placeholder-group-search": "", "placeholder-labels": "Pilih label", "plugin-rules": "Aturan plugin", "rule-source": { @@ -6159,8 +6162,8 @@ "placeholder-search-for": "Cari...", "radio-options": { "label": { - "suggestions": "Saran", - "visualizations": "Visualisasi" + "all-visualizations": "", + "suggestions": "Saran" } }, "title-close": "Tutup" @@ -8097,8 +8100,7 @@ "edit-pane": { "go-back": "Kembali" } - }, - "select-group": "Pilih grup" + } }, "grafana-data": { "datetime": { @@ -11003,8 +11005,8 @@ "tooltip-delete": "Hapus" }, "visualization-suggestions": { - "based-on-current-data": "Berdasarkan data saat ini", - "no-results-matched-your-query": "Tidak ada hasil yang cocok dengan kueri Anda" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Tidak dapat menemukan sesuatu yang cocok dengan kueri Anda" diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index d70278c2c40..a86adf9b336 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Cerca per nome o inserisci una query di filtro..." }, "grafana-folder": "Cartella Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Stato", "label": { "hide": "Nascondi", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Tutte le fonti dei dati", "placeholder-contact-point": "Seleziona punto di contatto", "placeholder-data-sources": "Seleziona origini dati", + "placeholder-group-search": "", "placeholder-labels": "Seleziona etichette", "plugin-rules": "Regole dei componenti aggiuntivi", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Cerca...", "radio-options": { "label": { - "suggestions": "Suggerimenti", - "visualizations": "Visualizzazioni" + "all-visualizations": "", + "suggestions": "Suggerimenti" } }, "title-close": "Chiudi" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Torna indietro" } - }, - "select-group": "Seleziona gruppo" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Elimina" }, "visualization-suggestions": { - "based-on-current-data": "In base ai dati attuali", - "no-results-matched-your-query": "Nessun risultato corrisponde alla tua query" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Non è stato possibile trovare nulla che corrispondesse alla tua query" diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 9667ae7ea41..42aa76d2642 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -2612,7 +2612,9 @@ "placeholder-search-input": "名前で検索するか、フィルタークエリを入力してください…" }, "grafana-folder": "Grafanaフォルダ", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "健康", "label": { "hide": "非表示", @@ -2623,6 +2625,7 @@ "placeholder-all-data-sources": "すべてのデータソース", "placeholder-contact-point": "コンタクトポイントを選択してください", "placeholder-data-sources": "データソースを選択してください", + "placeholder-group-search": "", "placeholder-labels": "ラベルを選択してください", "plugin-rules": "プラグインルール", "rule-source": { @@ -6159,8 +6162,8 @@ "placeholder-search-for": "次を検索...", "radio-options": { "label": { - "suggestions": "提案", - "visualizations": "可視化" + "all-visualizations": "", + "suggestions": "提案" } }, "title-close": "閉じる" @@ -8097,8 +8100,7 @@ "edit-pane": { "go-back": "戻る" } - }, - "select-group": "グループを選択" + } }, "grafana-data": { "datetime": { @@ -11003,8 +11005,8 @@ "tooltip-delete": "削除" }, "visualization-suggestions": { - "based-on-current-data": "現在のデータに基づく", - "no-results-matched-your-query": "クエリに一致する結果がありません" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "クエリに一致するものが見つかりませんでした" diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 115f653a526..437982e410b 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -2612,7 +2612,9 @@ "placeholder-search-input": "이름으로 검색하거나 필터 쿼리를 입력하세요..." }, "grafana-folder": "Grafana 폴더", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "상태", "label": { "hide": "숨기기", @@ -2623,6 +2625,7 @@ "placeholder-all-data-sources": "모든 데이터 소스", "placeholder-contact-point": "연락처 선택", "placeholder-data-sources": "데이터 소스 선택", + "placeholder-group-search": "", "placeholder-labels": "레이블 선택", "plugin-rules": "플러그인 규칙", "rule-source": { @@ -6159,8 +6162,8 @@ "placeholder-search-for": "검색…", "radio-options": { "label": { - "suggestions": "제안", - "visualizations": "시각화" + "all-visualizations": "", + "suggestions": "제안" } }, "title-close": "닫기" @@ -8097,8 +8100,7 @@ "edit-pane": { "go-back": "뒤로 가기" } - }, - "select-group": "그룹 선택" + } }, "grafana-data": { "datetime": { @@ -11003,8 +11005,8 @@ "tooltip-delete": "삭제" }, "visualization-suggestions": { - "based-on-current-data": "현재 데이터 기반", - "no-results-matched-your-query": "쿼리와 일치하는 결과가 없습니다" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "쿼리와 일치하는 항목을 찾을 수 없습니다" diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index f1236285a6b..994f52eb9e4 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Zoeken op naam of filterquery invoeren..." }, "grafana-folder": "Grafana-map", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Gezondheid", "label": { "hide": "Verbergen", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Alle gegevensbronnen", "placeholder-contact-point": "Contactpunt selecteren", "placeholder-data-sources": "Gegevensbronnen selecteren", + "placeholder-group-search": "", "placeholder-labels": "Labels selecteren", "plugin-rules": "Plug-inregels", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Zoeken naar...", "radio-options": { "label": { - "suggestions": "Suggesties", - "visualizations": "Visualisaties" + "all-visualizations": "", + "suggestions": "Suggesties" } }, "title-close": "Sluiten" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Ga terug" } - }, - "select-group": "Groep selecteren" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Verwijderen" }, "visualization-suggestions": { - "based-on-current-data": "Op basis van huidige gegevens", - "no-results-matched-your-query": "Geen resultaten gevonden voor je query" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Kon niets vinden dat overeenkomt met je query" diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 1a5897fd7ed..8cafca8a198 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -2648,7 +2648,9 @@ "placeholder-search-input": "Szukaj według nazwy lub wprowadź wyrażenie do filtrowania…" }, "grafana-folder": "Folder Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Zdrowie", "label": { "hide": "Ukryj", @@ -2659,6 +2661,7 @@ "placeholder-all-data-sources": "Wszystkie źródła danych", "placeholder-contact-point": "Wybierz punkt kontaktowy", "placeholder-data-sources": "Wybierz źródła danych", + "placeholder-group-search": "", "placeholder-labels": "Wybierz etykiety", "plugin-rules": "Reguły dotyczące wtyczek", "rule-source": { @@ -6228,8 +6231,8 @@ "placeholder-search-for": "Szukaj…", "radio-options": { "label": { - "suggestions": "Sugestie", - "visualizations": "Wizualizacje" + "all-visualizations": "", + "suggestions": "Sugestie" } }, "title-close": "Zamknij" @@ -8175,8 +8178,7 @@ "edit-pane": { "go-back": "Wróć" } - }, - "select-group": "Wybierz grupę" + } }, "grafana-data": { "datetime": { @@ -11144,8 +11146,8 @@ "tooltip-delete": "Usuń" }, "visualization-suggestions": { - "based-on-current-data": "Na podstawie aktualnych danych", - "no-results-matched-your-query": "Nie znaleziono wyników pasujących do zapytania" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Nie znaleziono wyników pasujących do Twojego zapytania" diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 419d550fe09..1151ca293a1 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Pesquise por nome ou insira uma consulta de filtro…" }, "grafana-folder": "Pasta do Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Integridade", "label": { "hide": "Ocultar", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Todas as fontes de dados", "placeholder-contact-point": "Selecionar ponto de contato", "placeholder-data-sources": "Selecionar fontes de dados", + "placeholder-group-search": "", "placeholder-labels": "Selecionar rótulos", "plugin-rules": "Regras de plug-in", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Pesquisar por…", "radio-options": { "label": { - "suggestions": "Sugestões", - "visualizations": "Visualizações" + "all-visualizations": "", + "suggestions": "Sugestões" } }, "title-close": "Fechar" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Voltar" } - }, - "select-group": "Selecionar grupo" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Excluir" }, "visualization-suggestions": { - "based-on-current-data": "Com base nos dados atuais", - "no-results-matched-your-query": "Nenhum resultado correspondeu à sua consulta" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Não foi possível encontrar nada que corresponda à sua consulta" diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 28b728aee0f..856c8b78a3e 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Pesquise por nome ou introduza uma consulta de filtro..." }, "grafana-folder": "Pasta Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Saúde", "label": { "hide": "Ocultar", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Todas as origens de dados", "placeholder-contact-point": "Selecionar ponto de contacto", "placeholder-data-sources": "Selecionar origens de dados", + "placeholder-group-search": "", "placeholder-labels": "Selecionar etiquetas", "plugin-rules": "Regras de plugin", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Pesquisar por...", "radio-options": { "label": { - "suggestions": "Sugestões", - "visualizations": "Visualizações" + "all-visualizations": "", + "suggestions": "Sugestões" } }, "title-close": "Fechar" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Voltar" } - }, - "select-group": "Selecionar grupo" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Eliminar" }, "visualization-suggestions": { - "based-on-current-data": "Com base nos dados atuais", - "no-results-matched-your-query": "Não existem resultados que correspondam à sua consulta" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Não foi possível encontrar nada que corresponda à sua consulta" diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 1d05879899a..50c67543a33 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -2648,7 +2648,9 @@ "placeholder-search-input": "Выполните поиск по имени или введите запрос фильтра..." }, "grafana-folder": "Папка Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Работоспособность", "label": { "hide": "Скрыть", @@ -2659,6 +2661,7 @@ "placeholder-all-data-sources": "Все источники данных", "placeholder-contact-point": "Выбрать точку контакта", "placeholder-data-sources": "Выбрать источники данных", + "placeholder-group-search": "", "placeholder-labels": "Выбрать метки", "plugin-rules": "Правила плагинов", "rule-source": { @@ -6228,8 +6231,8 @@ "placeholder-search-for": "Поиск...", "radio-options": { "label": { - "suggestions": "Предложения", - "visualizations": "Визуализации" + "all-visualizations": "", + "suggestions": "Предложения" } }, "title-close": "Закрыть" @@ -8175,8 +8178,7 @@ "edit-pane": { "go-back": "Назад" } - }, - "select-group": "Выбрать группу" + } }, "grafana-data": { "datetime": { @@ -11144,8 +11146,8 @@ "tooltip-delete": "Удалить" }, "visualization-suggestions": { - "based-on-current-data": "На основе текущих данных", - "no-results-matched-your-query": "По вашему запросу ничего не найдено" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Не удалось найти данные, соответствующие вашему запросу" diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 95e2ae6130e..f88845db555 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Sök efter namn eller ange filterfråga …" }, "grafana-folder": "Grafana-mapp", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Hälsa", "label": { "hide": "Dölj", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Alla datakällor", "placeholder-contact-point": "Välj kontaktpunkt", "placeholder-data-sources": "Välj datakällor", + "placeholder-group-search": "", "placeholder-labels": "Välj etiketter", "plugin-rules": "Tilläggsregler", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Sök efter …", "radio-options": { "label": { - "suggestions": "Förslag", - "visualizations": "Visualiseringar" + "all-visualizations": "", + "suggestions": "Förslag" } }, "title-close": "Stäng" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Tillbaka" } - }, - "select-group": "Markera grupp" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Ta bort" }, "visualization-suggestions": { - "based-on-current-data": "Baserat på aktuella data", - "no-results-matched-your-query": "Inga träffar matchade din sökfråga" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Kunde inte hitta något som matchar din fråga" diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 16e0520c9a7..574351940a4 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "" }, "grafana-folder": "", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Sağlık", "label": { "hide": "Gizle", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Tüm veri kaynakları", "placeholder-contact-point": "", "placeholder-data-sources": "", + "placeholder-group-search": "", "placeholder-labels": "", "plugin-rules": "Eklenti kuralları", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Ara...", "radio-options": { "label": { - "suggestions": "Öneriler", - "visualizations": "Görselleştirmeler" + "all-visualizations": "", + "suggestions": "Öneriler" } }, "title-close": "Kapat" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Geri dön" } - }, - "select-group": "" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Sil" }, "visualization-suggestions": { - "based-on-current-data": "Mevcut verilere göre", - "no-results-matched-your-query": "Sorgunuza uygun sonuç bulunamadı" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Sorgunuzla eşleşen hiçbir şey bulunamadı" diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 4dccf320255..67b617cc3b8 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -2612,7 +2612,9 @@ "placeholder-search-input": "按名称搜索或输入筛选条件查询……" }, "grafana-folder": "Grafana 文件夹", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "健康", "label": { "hide": "隐藏", @@ -2623,6 +2625,7 @@ "placeholder-all-data-sources": "所有数据源", "placeholder-contact-point": "选择联络点", "placeholder-data-sources": "选择数据源", + "placeholder-group-search": "", "placeholder-labels": "选择标签", "plugin-rules": "插件规则", "rule-source": { @@ -6159,8 +6162,8 @@ "placeholder-search-for": "搜索...", "radio-options": { "label": { - "suggestions": "建议", - "visualizations": "可视化" + "all-visualizations": "", + "suggestions": "建议" } }, "title-close": "关闭" @@ -8097,8 +8100,7 @@ "edit-pane": { "go-back": "返回" } - }, - "select-group": "选择组" + } }, "grafana-data": { "datetime": { @@ -11003,8 +11005,8 @@ "tooltip-delete": "删除" }, "visualization-suggestions": { - "based-on-current-data": "基于当前数据", - "no-results-matched-your-query": "没有符合您查询的结果" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "找不到与您的查询匹配的内容" diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index dec7715e1fd..a387547aab8 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -2612,7 +2612,9 @@ "placeholder-search-input": "按名稱搜尋或輸入篩選條件查詢…" }, "grafana-folder": "Grafana 資料夾", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "使用情況", "label": { "hide": "隱藏", @@ -2623,6 +2625,7 @@ "placeholder-all-data-sources": "所有資料來源", "placeholder-contact-point": "選擇聯絡點", "placeholder-data-sources": "選擇資料來源", + "placeholder-group-search": "", "placeholder-labels": "選擇標籤", "plugin-rules": "外掛程式規則", "rule-source": { @@ -6159,8 +6162,8 @@ "placeholder-search-for": "搜尋…", "radio-options": { "label": { - "suggestions": "建議", - "visualizations": "視覺化" + "all-visualizations": "", + "suggestions": "建議" } }, "title-close": "關閉" @@ -8097,8 +8100,7 @@ "edit-pane": { "go-back": "返回" } - }, - "select-group": "選擇群組" + } }, "grafana-data": { "datetime": { @@ -11003,8 +11005,8 @@ "tooltip-delete": "刪除" }, "visualization-suggestions": { - "based-on-current-data": "根據目前的資料", - "no-results-matched-your-query": "沒有符合您查詢的結果" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "找不到符合您查詢的內容" From 513b81a531b395197c9ddd10f59ad50f2b3592ea Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:15:58 +0100 Subject: [PATCH 121/378] Scopes: Sync navigation scope and apply subScopes (#114083) * Set navigationScope if we have a subScope * Proper URL sync * Update unit test with dashboards service subscription handling * Add a bunch of tests * Update functionality to change scopes when clicking on icon * Add test for TreeFolderItem * Udpate test and remove errors * Fix issues in test * Use ScopeNavgiations by default in the ScopesDashboardService unit tests * Remove misplaced test --- .../app/features/scopes/ScopesService.test.ts | 168 ++++++++-- public/app/features/scopes/ScopesService.ts | 15 + .../ScopesDashboardsService.test.ts | 184 +++++++++-- .../dashboards/ScopesDashboardsService.ts | 13 + .../dashboards/ScopesDashboardsTree.tsx | 4 +- .../ScopesDashboardsTreeFolderItem.test.tsx | 287 ++++++++++++++++++ .../ScopesDashboardsTreeFolderItem.tsx | 10 +- .../ScopesNavigationTreeLink.test.tsx | 175 +++++++++++ .../dashboards/ScopesNavigationTreeLink.tsx | 51 +++- .../selector/ScopesSelectorService.test.ts | 30 ++ .../scopes/selector/ScopesSelectorService.ts | 20 +- 11 files changed, 905 insertions(+), 52 deletions(-) create mode 100644 public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.test.tsx diff --git a/public/app/features/scopes/ScopesService.test.ts b/public/app/features/scopes/ScopesService.test.ts index 61f85257dcf..6b360f853e3 100644 --- a/public/app/features/scopes/ScopesService.test.ts +++ b/public/app/features/scopes/ScopesService.test.ts @@ -14,15 +14,22 @@ describe('ScopesService', () => { let selectorService: jest.Mocked; let dashboardsService: jest.Mocked; let locationService: jest.Mocked; - let stateSubscription: + let selectorStateSubscription: | (( state: { appliedScopes: Array<{ scopeId: string; scopeNodeId?: string; parentNodeId?: string }> }, prevState: { appliedScopes: Array<{ scopeId: string; scopeNodeId?: string; parentNodeId?: string }> } ) => void) | undefined; + let dashboardsStateSubscription: + | (( + state: { navigationScope?: string; drawerOpened: boolean }, + prevState: { navigationScope?: string; drawerOpened: boolean } + ) => void) + | undefined; beforeEach(() => { - stateSubscription = undefined; + selectorStateSubscription = undefined; + dashboardsStateSubscription = undefined; selectorService = { state: { @@ -46,7 +53,7 @@ describe('ScopesService', () => { tree: { scopeNodeId: '', expanded: false, query: '', children: {} }, }), subscribeToState: jest.fn((callback) => { - stateSubscription = callback; + selectorStateSubscription = callback; return { unsubscribe: jest.fn() }; }), changeScopes: jest.fn(), @@ -63,6 +70,7 @@ describe('ScopesService', () => { forScopeNames: [], loading: false, searchQuery: '', + navigationScope: undefined, }, stateObservable: new BehaviorSubject({ drawerOpened: false, @@ -73,7 +81,13 @@ describe('ScopesService', () => { forScopeNames: [], loading: false, searchQuery: '', + navigationScope: undefined, }), + subscribeToState: jest.fn((callback) => { + dashboardsStateSubscription = callback; + return { unsubscribe: jest.fn() }; + }), + setNavigationScope: jest.fn(), } as unknown as jest.Mocked; locationService = { @@ -160,6 +174,40 @@ describe('ScopesService', () => { expect(selectorService.changeScopes).toHaveBeenCalledWith(['scope1', 'scope2'], undefined, 'node1', false); }); + + it('should read navigation_scope from URL on init', () => { + locationService.getLocation = jest.fn().mockReturnValue({ + pathname: '/test', + search: '?scopes=scope1&navigation_scope=navScope1', + }); + + service = new ScopesService(selectorService, dashboardsService, locationService); + + expect(dashboardsService.setNavigationScope).toHaveBeenCalledWith('navScope1'); + }); + + it('should read navigation_scope along with other scope parameters', () => { + locationService.getLocation = jest.fn().mockReturnValue({ + pathname: '/test', + search: '?scopes=scope1&scope_node=node1&navigation_scope=navScope1', + }); + + service = new ScopesService(selectorService, dashboardsService, locationService); + + expect(dashboardsService.setNavigationScope).toHaveBeenCalledWith('navScope1'); + expect(selectorService.changeScopes).toHaveBeenCalledWith(['scope1'], undefined, 'node1', false); + }); + + it('should not call setNavigationScope when navigation_scope is not in URL', () => { + locationService.getLocation = jest.fn().mockReturnValue({ + pathname: '/test', + search: '?scopes=scope1', + }); + + service = new ScopesService(selectorService, dashboardsService, locationService); + + expect(dashboardsService.setNavigationScope).not.toHaveBeenCalled(); + }); }); describe('URL synchronization', () => { @@ -172,11 +220,11 @@ describe('ScopesService', () => { }); it('should write scope_node to URL when scopes change', () => { - if (!stateSubscription) { - throw new Error('stateSubscription not set'); + if (!selectorStateSubscription) { + throw new Error('selectorStateSubscription not set'); } - stateSubscription( + selectorStateSubscription( { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node1' }], }, @@ -196,11 +244,11 @@ describe('ScopesService', () => { }); it('should reset scope_parent to null when writing URL', () => { - if (!stateSubscription) { - throw new Error('stateSubscription not set'); + if (!selectorStateSubscription) { + throw new Error('selectorStateSubscription not set'); } - stateSubscription( + selectorStateSubscription( { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node1', parentNodeId: 'parent1' }], }, @@ -218,11 +266,11 @@ describe('ScopesService', () => { }); it('should handle scopeNodeId changes without scope changes', () => { - if (!stateSubscription) { - throw new Error('stateSubscription not set'); + if (!selectorStateSubscription) { + throw new Error('selectorStateSubscription not set'); } - stateSubscription( + selectorStateSubscription( { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node2' }], }, @@ -242,11 +290,11 @@ describe('ScopesService', () => { }); it('should handle missing scopeNodeId gracefully', () => { - if (!stateSubscription) { - throw new Error('stateSubscription not set'); + if (!selectorStateSubscription) { + throw new Error('selectorStateSubscription not set'); } - stateSubscription( + selectorStateSubscription( { appliedScopes: [{ scopeId: 'scope1' }], }, @@ -266,13 +314,13 @@ describe('ScopesService', () => { }); it('should not update URL when scopes and scopeNodeId have not changed', () => { - if (!stateSubscription) { - throw new Error('stateSubscription not set'); + if (!selectorStateSubscription) { + throw new Error('selectorStateSubscription not set'); } jest.clearAllMocks(); - stateSubscription( + selectorStateSubscription( { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node1' }], }, @@ -283,6 +331,90 @@ describe('ScopesService', () => { expect(locationService.partial).not.toHaveBeenCalled(); }); + + it('should write navigation_scope to URL when navigationScope changes', () => { + if (!dashboardsStateSubscription) { + throw new Error('dashboardsStateSubscription not set'); + } + + dashboardsStateSubscription( + { + navigationScope: 'navScope1', + drawerOpened: true, + }, + { + navigationScope: undefined, + drawerOpened: false, + } + ); + + expect(locationService.partial).toHaveBeenCalledWith({ + navigation_scope: 'navScope1', + }); + }); + + it('should update navigation_scope in URL when navigationScope changes', () => { + if (!dashboardsStateSubscription) { + throw new Error('dashboardsStateSubscription not set'); + } + + dashboardsStateSubscription( + { + navigationScope: 'navScope2', + drawerOpened: true, + }, + { + navigationScope: 'navScope1', + drawerOpened: true, + } + ); + + expect(locationService.partial).toHaveBeenCalledWith({ + navigation_scope: 'navScope2', + }); + }); + + it('should not update URL when navigationScope has not changed', () => { + if (!dashboardsStateSubscription) { + throw new Error('dashboardsStateSubscription not set'); + } + + jest.clearAllMocks(); + + dashboardsStateSubscription( + { + navigationScope: 'navScope1', + drawerOpened: true, + }, + { + navigationScope: 'navScope1', + drawerOpened: false, + } + ); + + expect(locationService.partial).not.toHaveBeenCalled(); + }); + + it('should clear navigation_scope from URL when navigationScope is cleared', () => { + if (!dashboardsStateSubscription) { + throw new Error('dashboardsStateSubscription not set'); + } + + dashboardsStateSubscription( + { + navigationScope: undefined, + drawerOpened: false, + }, + { + navigationScope: 'navScope1', + drawerOpened: true, + } + ); + + expect(locationService.partial).toHaveBeenCalledWith({ + navigation_scope: undefined, + }); + }); }); describe('setEnabled', () => { diff --git a/public/app/features/scopes/ScopesService.ts b/public/app/features/scopes/ScopesService.ts index 2595ff66876..36edcc94b13 100644 --- a/public/app/features/scopes/ScopesService.ts +++ b/public/app/features/scopes/ScopesService.ts @@ -73,6 +73,11 @@ export class ScopesService implements ScopesContextValue { const scopeNodeId = queryParams.get('scope_node'); // TODO: figure out when to remove this. scope_parent is for backward compatibility only const parentNodeId = queryParams.get('scope_parent'); + const navigationScope = queryParams.get('navigation_scope'); + + if (navigationScope) { + this.dashboardsService.setNavigationScope(navigationScope); + } this.changeScopes(queryParams.getAll('scopes'), parentNodeId ?? undefined, scopeNodeId ?? undefined); @@ -133,6 +138,16 @@ export class ScopesService implements ScopesContextValue { } }) ); + // Update the URL based on change in the navigation scope + this.subscriptions.push( + this.dashboardsService.subscribeToState((state, prevState) => { + if (state.navigationScope !== prevState.navigationScope) { + this.locationService.partial({ + navigation_scope: state.navigationScope, + }); + } + }) + ); } /** diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsService.test.ts b/public/app/features/scopes/dashboards/ScopesDashboardsService.test.ts index 62b3b71e09d..52368b4577d 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsService.test.ts +++ b/public/app/features/scopes/dashboards/ScopesDashboardsService.test.ts @@ -1,7 +1,6 @@ import { Location } from 'history'; import { Subject } from 'rxjs'; -import { ScopeDashboardBinding } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { ScopesApiClient } from '../ScopesApiClient'; @@ -15,7 +14,7 @@ jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), config: { featureToggles: { - useScopesNavigationEndpoint: false, + useScopesNavigationEndpoint: true, }, apps: {}, }, @@ -33,27 +32,32 @@ describe('ScopesDashboardsService', () => { let mockApiClient: jest.Mocked; beforeEach(() => { + const fetchScopeNavigationsMock = jest.fn().mockResolvedValue([]); mockApiClient = { fetchDashboards: jest.fn(), - fetchScopeNavigations: jest.fn(), + fetchScopeNavigations: fetchScopeNavigationsMock, } as unknown as jest.Mocked; service = new ScopesDashboardsService(mockApiClient); }); + afterEach(() => { + config.featureToggles.useScopesNavigationEndpoint = true; + }); + describe('folder expansion based on location', () => { it('should expand folders when current location matches dashboard ID', async () => { // Mock current location to be a dashboard (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/d/dashboard1' } as Location); - const mockDashboards: ScopeDashboardBinding[] = [ + const mockNavigations: ScopeNavigation[] = [ { spec: { scope: 'scope1', - dashboard: 'dashboard1', + url: '/d/dashboard1', }, status: { - dashboardTitle: 'Test Dashboard', + title: 'Test Dashboard', groups: ['group1'], }, metadata: { @@ -62,7 +66,7 @@ describe('ScopesDashboardsService', () => { }, ]; - mockApiClient.fetchDashboards.mockResolvedValue(mockDashboards); + mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations); await service.fetchDashboards(['scope1']); // Verify that the folder is expanded because the current dashboard ID matches @@ -168,14 +172,14 @@ describe('ScopesDashboardsService', () => { // Mock current location to not match any navigation (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/different-path' } as Location); - const mockDashboards: ScopeDashboardBinding[] = [ + const mockNavigations: ScopeNavigation[] = [ { spec: { scope: 'scope1', - dashboard: 'dashboard1', + url: '/d/dashboard1', }, status: { - dashboardTitle: 'Test Dashboard', + title: 'Test Dashboard', groups: ['group1'], }, metadata: { @@ -184,7 +188,7 @@ describe('ScopesDashboardsService', () => { }, ]; - mockApiClient.fetchDashboards.mockResolvedValue(mockDashboards); + mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations); await service.fetchDashboards(['scope1']); // Verify that the folder is not expanded because the current location doesn't match @@ -197,14 +201,14 @@ describe('ScopesDashboardsService', () => { pathname: '/d/dashboard1/very-important', } as Location); - const mockDashboards: ScopeDashboardBinding[] = [ + const mockNavigations: ScopeNavigation[] = [ { spec: { scope: 'scope1', - dashboard: 'dashboard1', + url: '/d/dashboard1', }, status: { - dashboardTitle: 'Test Dashboard', + title: 'Test Dashboard', groups: ['group1'], }, metadata: { @@ -213,7 +217,7 @@ describe('ScopesDashboardsService', () => { }, ]; - mockApiClient.fetchDashboards.mockResolvedValue(mockDashboards); + mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations); await service.fetchDashboards(['scope1']); // Verify that the folder is expanded because the current path starts with the dashboard ID @@ -224,14 +228,14 @@ describe('ScopesDashboardsService', () => { // Mock current location to be a specific dashboard (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/d/dashboard1' } as Location); - const mockDashboards: ScopeDashboardBinding[] = [ + const mockNavigations: ScopeNavigation[] = [ { spec: { scope: 'scope1', - dashboard: 'dashboard1', + url: '/d/dashboard1', }, status: { - dashboardTitle: 'Test Dashboard', + title: 'Test Dashboard', groups: ['group1'], }, metadata: { @@ -241,10 +245,10 @@ describe('ScopesDashboardsService', () => { { spec: { scope: 'scope1', - dashboard: 'dashboard2', + url: '/d/dashboard2', }, status: { - dashboardTitle: 'Another Dashboard', + title: 'Another Dashboard', groups: ['group2'], }, metadata: { @@ -253,7 +257,7 @@ describe('ScopesDashboardsService', () => { }, ]; - mockApiClient.fetchDashboards.mockResolvedValue(mockDashboards); + mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations); await service.fetchDashboards(['scope1']); // Verify that only the folder containing the current dashboard is expanded @@ -697,4 +701,142 @@ describe('ScopesDashboardsService', () => { expect(filteredItems.some((item) => item.metadata.name === 'loki-item-1')).toBe(true); }); }); + + describe('setNavigationScope', () => { + beforeEach(() => { + (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/' } as Location); + // Reset mocks but keep the mock functions + mockApiClient.fetchDashboards.mockClear(); + // Note: fetchScopeNavigations mock is set up in top-level beforeEach + // Individual tests will override it with their own mockResolvedValue calls + }); + + it('should set navigation scope and fetch dashboards', async () => { + // Mock non-empty results so drawerOpened stays true after fetchDashboards completes + mockApiClient.fetchScopeNavigations.mockResolvedValue([ + { + spec: { scope: 'navScope1', url: '/d/dashboard1' }, + status: { title: 'Test', groups: [] }, + metadata: { name: 'dashboard1' }, + }, + ]); + + await service.setNavigationScope('navScope1'); + + expect(service.state.navigationScope).toBe('navScope1'); + expect(service.state.drawerOpened).toBe(true); + expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['navScope1']); + }); + + it('should clear navigation scope and use fallback scope names', async () => { + // Mock non-empty results so drawerOpened stays true after fetchDashboards completes + mockApiClient.fetchScopeNavigations.mockResolvedValue([ + { + spec: { scope: 'fallbackScope1', url: '/d/dashboard1' }, + status: { title: 'Test', groups: [] }, + metadata: { name: 'dashboard1' }, + }, + ]); + // Set an initial navigation scope + await service.setNavigationScope('initialScope'); + expect(service.state.navigationScope).toBe('initialScope'); + expect(service.state.drawerOpened).toBe(true); + expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['initialScope']); + + await service.setNavigationScope(undefined, ['fallbackScope1', 'fallbackScope2']); + + expect(service.state.navigationScope).toBeUndefined(); + expect(service.state.drawerOpened).toBe(true); + expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['fallbackScope1', 'fallbackScope2']); + }); + + it('should not run if previous and next navigation scopes are undefined and we provide fallback scope names', async () => { + await service.setNavigationScope(undefined, ['fallbackScope1', 'fallbackScope2']); + expect(service.state.navigationScope).toBeUndefined(); + expect(service.state.drawerOpened).toBe(false); + expect(mockApiClient.fetchScopeNavigations).not.toHaveBeenCalled(); + }); + + it('should close drawer when navigation scope is cleared without fallback', async () => { + // When setNavigationScope is called with undefined and no fallback, + // it calls fetchDashboards([]), which returns early without calling the API + await service.setNavigationScope(undefined); + + expect(service.state.navigationScope).toBeUndefined(); + expect(service.state.drawerOpened).toBe(false); + // fetchDashboards([]) returns early, so API client is not called + expect(mockApiClient.fetchScopeNavigations).not.toHaveBeenCalled(); + }); + + it('should not update state if navigation scope has not changed', async () => { + mockApiClient.fetchScopeNavigations.mockResolvedValue([ + { + spec: { scope: 'navScope1', url: '/d/dashboard1' }, + status: { title: 'Test', groups: [] }, + metadata: { name: 'dashboard1' }, + }, + ]); + await service.setNavigationScope('navScope1'); + mockApiClient.fetchScopeNavigations.mockClear(); + + await service.setNavigationScope('navScope1'); + + expect(mockApiClient.fetchScopeNavigations).not.toHaveBeenCalled(); + }); + + it('should update navigation scope when changing from one scope to another', async () => { + mockApiClient.fetchScopeNavigations.mockResolvedValue([]); + + await service.setNavigationScope('navScope1'); + mockApiClient.fetchScopeNavigations.mockClear(); + + await service.setNavigationScope('navScope2'); + + expect(service.state.navigationScope).toBe('navScope2'); + expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['navScope2']); + }); + + it('should update navigation scope when clearing an existing scope', async () => { + mockApiClient.fetchScopeNavigations.mockResolvedValue([]); + + await service.setNavigationScope('navScope1'); + mockApiClient.fetchScopeNavigations.mockClear(); + + await service.setNavigationScope(undefined, ['fallbackScope']); + + expect(service.state.navigationScope).toBeUndefined(); + expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['fallbackScope']); + }); + + it('should open drawer when navigation scope is set', async () => { + // Mock to return non-empty results so drawer stays open + mockApiClient.fetchScopeNavigations.mockResolvedValue([ + { + spec: { scope: 'navScope1', url: '/d/dashboard1' }, + status: { title: 'Test', groups: [] }, + metadata: { name: 'dashboard1' }, + }, + ]); + + await service.setNavigationScope('navScope1'); + + expect(service.state.drawerOpened).toBe(true); + }); + + it('should open drawer when fallback scopes are provided', async () => { + // Mock non-empty results so drawerOpened stays true after fetchDashboards completes + mockApiClient.fetchScopeNavigations.mockResolvedValue([ + { + spec: { scope: 'fallbackScope', url: '/d/dashboard1' }, + status: { title: 'Test', groups: [] }, + metadata: { name: 'dashboard1' }, + }, + ]); + await service.setNavigationScope('initialScope'); + + await service.setNavigationScope(undefined, ['fallbackScope']); + + expect(service.state.drawerOpened).toBe(true); + }); + }); }); diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsService.ts b/public/app/features/scopes/dashboards/ScopesDashboardsService.ts index be6f966623e..55ea5ab737a 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsService.ts +++ b/public/app/features/scopes/dashboards/ScopesDashboardsService.ts @@ -23,6 +23,7 @@ interface ScopesDashboardsServiceState { forScopeNames: string[]; loading: boolean; searchQuery: string; + navigationScope?: string; } export class ScopesDashboardsService extends ScopesServiceBase { @@ -56,6 +57,18 @@ export class ScopesDashboardsService extends ScopesServiceBase { + if (this.state.navigationScope === navigationScope) { + return; + } + + const forScopeNames = navigationScope ? [navigationScope] : (fallbackScopeNames ?? []); + this.updateState({ navigationScope, drawerOpened: forScopeNames.length > 0 }); + await this.fetchDashboards(forScopeNames); + }; + // Expand the group that matches the current path, if it is not already expanded private onLocationChange = (pathname: string) => { if (!this.state.drawerOpened) { diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsTree.tsx b/public/app/features/scopes/dashboards/ScopesDashboardsTree.tsx index b25a3bfc55b..7e7601f3b06 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsTree.tsx +++ b/public/app/features/scopes/dashboards/ScopesDashboardsTree.tsx @@ -9,12 +9,13 @@ import { ScopesNavigationTreeLink } from './ScopesNavigationTreeLink'; import { OnFolderUpdate, SuggestedNavigationsFoldersMap } from './types'; export interface ScopesDashboardsTreeProps { + subScope?: string; folders: SuggestedNavigationsFoldersMap; folderPath: string[]; onFolderUpdate: OnFolderUpdate; } -export function ScopesDashboardsTree({ folders, folderPath, onFolderUpdate }: ScopesDashboardsTreeProps) { +export function ScopesDashboardsTree({ subScope, folders, folderPath, onFolderUpdate }: ScopesDashboardsTreeProps) { const [queryParams] = useQueryParams(); const styles = useStyles2(getStyles); @@ -52,6 +53,7 @@ export function ScopesDashboardsTree({ folders, folderPath, onFolderUpdate }: Sc ))} {regularNavigations.map((navigation) => ( ({ + useQueryParams: jest.fn(() => [{}]), +})); + +// Mock ScopesContextProvider +const mockScopesSelectorService = { + changeScopes: jest.fn(), +}; + +const mockScopesDashboardsService = { + setNavigationScope: jest.fn(), +}; + +jest.mock('../ScopesContextProvider', () => ({ + ...jest.requireActual('../ScopesContextProvider'), + useScopesServices: jest.fn(() => ({ + scopesSelectorService: mockScopesSelectorService, + scopesDashboardsService: mockScopesDashboardsService, + })), +})); + +describe('ScopesDashboardsTreeFolderItem', () => { + const mockOnFolderUpdate = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const createMockFolder = (overrides?: Partial): SuggestedNavigationsFolder => ({ + title: 'Test Folder', + expanded: false, + folders: {}, + suggestedNavigations: {}, + ...overrides, + }); + + const createMockFolders: SuggestedNavigationsFoldersMap = { + '': { + title: '', + expanded: true, + folders: {}, + suggestedNavigations: {}, + }, + }; + + it('renders folder with correct props', () => { + const folder = createMockFolder(); + render( + + ); + + expect(screen.getByText('Test Folder')).toBeInTheDocument(); + expect(screen.getByTestId('scopes-dashboards-Test Folder-expand')).toBeInTheDocument(); + }); + + it('calls onFolderUpdate when expand button is clicked', async () => { + const user = userEvent.setup(); + const folder = createMockFolder({ expanded: false }); + + render( + + ); + + const expandButton = screen.getByTestId('scopes-dashboards-Test Folder-expand'); + await user.click(expandButton); + + expect(mockOnFolderUpdate).toHaveBeenCalledWith([''], true); + }); + + it('shows exchange icon when folder has subScopeName', () => { + const folder = createMockFolder({ subScopeName: 'subScope1' }); + + render( + + ); + + // IconButton with exchange-alt icon should be present + const exchangeButton = screen.getByRole('button', { name: /change root scope/i }); + expect(exchangeButton).toBeInTheDocument(); + }); + + it('does not show exchange icon when folder does not have subScopeName', () => { + const folder = createMockFolder(); + + render( + + ); + + const exchangeButtons = screen.queryAllByRole('button', { name: /change root scope/i }); + expect(exchangeButtons).toHaveLength(0); + }); + + it('calls setNavigationScope when exchange icon is clicked', async () => { + const user = userEvent.setup(); + const folder = createMockFolder({ subScopeName: 'subScope1' }); + + render( + + ); + + const exchangeButton = screen.getByRole('button', { name: /change root scope/i }); + await user.click(exchangeButton); + + expect(mockScopesDashboardsService.setNavigationScope).toHaveBeenCalledWith(undefined, ['subScope1']); + }); + + it('calls changeScopes when exchange icon is clicked', async () => { + const user = userEvent.setup(); + const folder = createMockFolder({ subScopeName: 'subScope1' }); + + render( + + ); + + const exchangeButton = screen.getByRole('button', { name: /change root scope/i }); + await user.click(exchangeButton); + + expect(mockScopesSelectorService.changeScopes).toHaveBeenCalledWith(['subScope1']); + }); + + it('passes subScope prop to ScopesDashboardsTree when folder is expanded', () => { + const folder = createMockFolder({ expanded: true, subScopeName: 'subScope1' }); + const childFolders: SuggestedNavigationsFoldersMap = { + '': { + title: '', + expanded: true, + folders: { + childFolder: { + title: 'Child Folder', + expanded: false, + folders: {}, + suggestedNavigations: {}, + }, + }, + suggestedNavigations: {}, + }, + childFolder: { + title: 'Child Folder', + expanded: false, + folders: {}, + suggestedNavigations: {}, + }, + }; + + render( + + ); + + // ScopesDashboardsTree should be rendered when folder is expanded + // We can verify this by checking that the children container is present + const childrenContainer = screen.getByText('Test Folder').closest('div')?.nextSibling; + expect(childrenContainer).toBeInTheDocument(); + }); + + it('does not render ScopesDashboardsTree when folder is not expanded', () => { + const folder = createMockFolder({ expanded: false, subScopeName: 'subScope1' }); + + render( + + ); + + // When not expanded, the children container should not be visible + // The structure should only show the folder row + expect(screen.getByText('Test Folder')).toBeInTheDocument(); + }); + + it('prevents default and stops propagation when exchange icon is clicked', async () => { + const folder = createMockFolder({ subScopeName: 'subScope1' }); + const preventDefault = jest.fn(); + const stopPropagation = jest.fn(); + + render( + + ); + + const exchangeButton = screen.getByRole('button', { name: /change root scope/i }); + const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true }); + Object.defineProperty(clickEvent, 'preventDefault', { value: preventDefault }); + Object.defineProperty(clickEvent, 'stopPropagation', { value: stopPropagation }); + + exchangeButton.dispatchEvent(clickEvent); + + // The onClick handler should prevent default and stop propagation + // Note: userEvent.click doesn't trigger preventDefault/stopPropagation directly, + // but the handler should call them. We verify the handler was called correctly. + expect(mockScopesDashboardsService.setNavigationScope).toHaveBeenCalled(); + expect(stopPropagation).toHaveBeenCalled(); + expect(preventDefault).toHaveBeenCalled(); + }); + + it('does not call setNavigationScope when subScopeName is missing', async () => { + const folder = createMockFolder({ subScopeName: undefined }); + + render( + + ); + + // No exchange button should be present, so no click should trigger setNavigationScope + expect(mockScopesDashboardsService.setNavigationScope).not.toHaveBeenCalled(); + // Check precense of exchange button + const exchangeButton = screen.queryByRole('button', { name: /change root scope/i }); + expect(exchangeButton).not.toBeInTheDocument(); + }); + + it('does not call setNavigationScope when scopesSelectorService is not available', async () => { + const user = userEvent.setup(); + const folder = createMockFolder({ subScopeName: 'subScope1' }); + + // Mock useScopesServices to return undefined + jest.spyOn(require('../ScopesContextProvider'), 'useScopesServices').mockReturnValue(undefined); + + render( + + ); + + const exchangeButton = screen.queryByRole('button', { name: /change root scope/i }); + if (exchangeButton) { + await user.click(exchangeButton); + } + + // Should not crash, but also should not call setNavigationScope if service is not available + // The component checks for scopesSelectorService existence before calling setNavigationScope + expect(mockScopesDashboardsService.setNavigationScope).not.toHaveBeenCalled(); + }); +}); diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx b/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx index da1f315c5e2..9d572a4d54c 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx +++ b/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx @@ -26,7 +26,7 @@ export function ScopesDashboardsTreeFolderItem({ // get scopesselector service const scopesSelectorService = useScopesServices()?.scopesSelectorService ?? undefined; - + const scopesDashboardsService = useScopesServices()?.scopesDashboardsService ?? undefined; return (
@@ -57,6 +57,7 @@ export function ScopesDashboardsTreeFolderItem({ e.preventDefault(); e.stopPropagation(); if (folder.subScopeName && scopesSelectorService) { + scopesDashboardsService?.setNavigationScope(undefined, [folder.subScopeName]); scopesSelectorService.changeScopes([folder.subScopeName]); } }} @@ -66,7 +67,12 @@ export function ScopesDashboardsTreeFolderItem({ {folder.expanded && (
- +
)}
diff --git a/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.test.tsx b/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.test.tsx index 3aba827d4ef..8dafc80c612 100644 --- a/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.test.tsx +++ b/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.test.tsx @@ -1,6 +1,9 @@ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { MemoryRouter, useLocation } from 'react-router-dom-v5-compat'; +import { locationService } from '@grafana/runtime'; + import { ScopesNavigationTreeLink } from './ScopesNavigationTreeLink'; // Mock react-router-dom's useLocation @@ -9,15 +12,50 @@ jest.mock('react-router-dom-v5-compat', () => ({ useLocation: jest.fn(), })); +// Mock @grafana/runtime +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + locationService: { + push: jest.fn(), + }, +})); + +// Mock ScopesContextProvider +const mockScopesSelectorService = { + state: { + appliedScopes: [{ scopeId: 'currentScope' }], + }, + changeScopes: jest.fn(), +}; + +const mockScopesDashboardsService = { + state: { + navigationScope: undefined as string | undefined, + }, + setNavigationScope: jest.fn(), +}; + +jest.mock('../ScopesContextProvider', () => ({ + ...jest.requireActual('../ScopesContextProvider'), + useScopesServices: jest.fn(() => ({ + scopesSelectorService: mockScopesSelectorService, + scopesDashboardsService: mockScopesDashboardsService, + })), +})); + const renderWithRouter = (ui: React.ReactElement) => { return render({ui}); }; describe('ScopesNavigationTreeLink', () => { const mockUseLocation = useLocation as jest.Mock; + const mockLocationServicePush = locationService.push as jest.Mock; beforeEach(() => { mockUseLocation.mockReturnValue({ pathname: '/current-path' }); + jest.clearAllMocks(); + mockScopesSelectorService.state.appliedScopes = [{ scopeId: 'currentScope' }]; + mockScopesDashboardsService.state.navigationScope = undefined; }); afterEach(() => { @@ -133,4 +171,141 @@ describe('ScopesNavigationTreeLink', () => { expect(icon).toBeInTheDocument(); expect(link).toHaveTextContent('Metrics Drilldown'); }); + + describe('click handler with subScope', () => { + beforeEach(() => { + // Mock window.location.origin + Object.defineProperty(window, 'location', { + value: { + origin: 'http://localhost', + }, + writable: true, + }); + }); + + it('should prevent default navigation when subScope is provided', async () => { + const user = userEvent.setup(); + + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await user.click(link); + + // Should call changeScopes instead of navigating normally + expect(mockScopesSelectorService.changeScopes).toHaveBeenCalled(); + expect(mockLocationServicePush).toHaveBeenCalled(); + }); + + it('should set navigation scope from current scope when not already set', async () => { + mockScopesDashboardsService.state.navigationScope = undefined; + mockScopesSelectorService.state.appliedScopes = [{ scopeId: 'currentScope' }]; + + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await userEvent.click(link); + + expect(mockScopesDashboardsService.setNavigationScope).toHaveBeenCalledWith('currentScope'); + }); + + it('should not set navigation scope when already set', async () => { + mockScopesDashboardsService.state.navigationScope = 'existingNavScope'; + mockScopesSelectorService.state.appliedScopes = [{ scopeId: 'currentScope' }]; + + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await userEvent.click(link); + + // Should not call setNavigationScope with currentScope since it's already set + expect(mockScopesDashboardsService.setNavigationScope).not.toHaveBeenCalledWith('currentScope'); + }); + + it('should call changeScopes with subScope', async () => { + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await userEvent.click(link); + + expect(mockScopesSelectorService.changeScopes).toHaveBeenCalledWith(['subScope1'], undefined, undefined, false); + }); + + it('should navigate to URL with updated query params', async () => { + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await userEvent.click(link); + + expect(mockLocationServicePush).toHaveBeenCalled(); + const pushedUrl = mockLocationServicePush.mock.calls[0][0]; + expect(pushedUrl).toContain('/test-path'); + expect(pushedUrl).toContain('scopes=subScope1'); + expect(pushedUrl).toContain('navigation_scope=currentScope'); + }); + + it('should remove scope_node and scope_parent from URL when navigating with subScope', async () => { + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await userEvent.click(link); + + expect(mockLocationServicePush).toHaveBeenCalled(); + const pushedUrl = mockLocationServicePush.mock.calls[0][0]; + expect(pushedUrl).not.toContain('scope_node'); + expect(pushedUrl).not.toContain('scope_parent'); + }); + + it('should allow normal navigation when subScope is not provided', async () => { + const user = userEvent.setup(); + + renderWithRouter(); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await user.click(link); + + // Should not call changeScopes or locationService.push when subScope is not provided + expect(mockScopesSelectorService.changeScopes).not.toHaveBeenCalled(); + expect(mockLocationServicePush).not.toHaveBeenCalled(); + }); + + it('should handle URL with existing query params correctly', async () => { + const user = userEvent.setup(); + mockScopesDashboardsService.state.navigationScope = undefined; + mockScopesSelectorService.state.appliedScopes = [{ scopeId: 'currentScope' }]; + + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await user.click(link); + + expect(mockLocationServicePush).toHaveBeenCalled(); + const pushedUrl = mockLocationServicePush.mock.calls[0][0]; + expect(pushedUrl).toContain('scopes=subScope1'); + expect(pushedUrl).toContain('navigation_scope=currentScope'); + }); + }); }); diff --git a/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.tsx b/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.tsx index 859b6059059..b7722edcf8c 100644 --- a/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.tsx +++ b/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.tsx @@ -2,31 +2,76 @@ import { css, cx } from '@emotion/css'; import { useMemo } from 'react'; import { Link, useLocation } from 'react-router-dom-v5-compat'; -import { GrafanaTheme2, IconName, locationUtil } from '@grafana/data'; +import { GrafanaTheme2, IconName, locationUtil, UrlQueryMap, urlUtil } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; import { Icon, useStyles2 } from '@grafana/ui'; +import { useScopesServices } from '../ScopesContextProvider'; + import { isCurrentPath, normalizePath } from './scopeNavgiationUtils'; export interface ScopesNavigationTreeLinkProps { + subScope?: string; to: string; title: string; id: string; } -export function ScopesNavigationTreeLink({ to, title, id }: ScopesNavigationTreeLinkProps) { +export function ScopesNavigationTreeLink({ subScope, to, title, id }: ScopesNavigationTreeLinkProps) { const styles = useStyles2(getStyles); const linkIcon = useMemo(() => getLinkIcon(to), [to]); const locPathname = useLocation().pathname; - + const services = useScopesServices(); // Ignore query params const isCurrent = isCurrentPath(locPathname, to); + const handleClick = (e: React.MouseEvent) => { + if (subScope) { + e.preventDefault(); // Prevent default Link navigation + + // Set current scope to navigation scope and subScope to scope + const currentScope = services?.scopesSelectorService?.state.appliedScopes[0]?.scopeId; + const currentNavigationScope = services?.scopesDashboardsService?.state.navigationScope; + + // Parse the URL to extract path and existing query params + const url = new URL(to, window.location.origin); + const pathname = url.pathname; + const searchParams = new URLSearchParams(url.search); + if (!currentNavigationScope && currentScope) { + searchParams.set('navigation_scope', currentScope); + services?.scopesDashboardsService?.setNavigationScope(currentScope); + } + + // Update query params with the new subScope + searchParams.set('scopes', subScope); + // Remove scope_node and scope_parent since we're changing to a subScope + searchParams.delete('scope_node'); + searchParams.delete('scope_parent'); + + // Convert URLSearchParams to query map object for urlUtil.renderUrl + const queryMap: UrlQueryMap = {}; + searchParams.forEach((value, key) => { + queryMap[key] = value; + }); + + // Build the new URL safely using urlUtil.renderUrl + const newUrl = urlUtil.renderUrl(pathname, queryMap); + + // Change scopes first (this updates the state) + services?.scopesSelectorService?.changeScopes([subScope], undefined, undefined, false); + + // Then navigate to the URL with updated query params + locationService.push(newUrl); + } + }; + return ( diff --git a/public/app/features/scopes/selector/ScopesSelectorService.test.ts b/public/app/features/scopes/selector/ScopesSelectorService.test.ts index eef594d2335..57ff735f019 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.test.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.test.ts @@ -73,6 +73,7 @@ describe('ScopesSelectorService', () => { dashboardsService = { fetchDashboards: jest.fn().mockResolvedValue(undefined), + setNavigationScope: jest.fn(), state: { scopeNavigations: [], dashboards: [], @@ -82,6 +83,7 @@ describe('ScopesSelectorService', () => { forScopeNames: [], loading: false, searchQuery: '', + navigationScope: undefined, }, } as unknown as jest.Mocked; @@ -395,6 +397,34 @@ describe('ScopesSelectorService', () => { await service.removeAllScopes(); expect(service.state.appliedScopes).toEqual([]); }); + + it('should clear navigation scope when removing all scopes', async () => { + await service.updateNode('', true, ''); + await service.selectScope('test-scope-node'); + await service.apply(); + await service.removeAllScopes(); + expect(dashboardsService.setNavigationScope).toHaveBeenCalledWith(undefined); + }); + }); + + describe('navigation scope interaction', () => { + it('should skip fetchDashboards when navigationScope is set', async () => { + dashboardsService.state.navigationScope = 'navScope1'; + jest.clearAllMocks(); + + await service.changeScopes(['test-scope']); + + expect(dashboardsService.fetchDashboards).not.toHaveBeenCalled(); + }); + + it('should call fetchDashboards when navigationScope is not set', async () => { + dashboardsService.state.navigationScope = undefined; + jest.clearAllMocks(); + + await service.changeScopes(['test-scope']); + + expect(dashboardsService.fetchDashboards).toHaveBeenCalledWith(['test-scope']); + }); }); describe('getRecentScopes', () => { diff --git a/public/app/features/scopes/selector/ScopesSelectorService.ts b/public/app/features/scopes/selector/ScopesSelectorService.ts index 742a77ad5d4..14a16be69ac 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.ts @@ -371,12 +371,15 @@ export class ScopesSelectorService extends ScopesServiceBase s.scopeId)).then(() => { - const selectedScopeNode = scopes[0]?.scopeNodeId ? this.state.nodes[scopes[0]?.scopeNodeId] : undefined; - if (redirectOnApply) { - this.redirectAfterApply(selectedScopeNode); - } - }); + // Only fetch dashboards based on the scopes if we don't have a navigation scope set. + if (!this.dashboardsService.state.navigationScope) { + this.dashboardsService.fetchDashboards(scopes.map((s) => s.scopeId)).then(() => { + const selectedScopeNode = scopes[0]?.scopeNodeId ? this.state.nodes[scopes[0]?.scopeNodeId] : undefined; + if (redirectOnApply) { + this.redirectAfterApply(selectedScopeNode); + } + }); + } if (scopes.length > 0) { const fetchedScopes = await this.apiClient.fetchMultipleScopes(scopes.map((s) => s.scopeId)); @@ -431,7 +434,10 @@ export class ScopesSelectorService extends ScopesServiceBase this.applyScopes([], false); + public removeAllScopes = () => { + this.applyScopes([], false); + this.dashboardsService.setNavigationScope(undefined); + }; private addRecentScopes = (scopes: Scope[], parentNode?: ScopeNode) => { if (scopes.length === 0) { From 5538dfe73dfdfa51947c62cf7f6df0f338057e12 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:17:51 +0100 Subject: [PATCH 122/378] Alerting: Add RBAC for enrichment (#113296) * wip * wip 2 * prettier * fix tests * address pr feedback * address pr feedback 2 * address review comments * update useEnrichmentAbilities changing AlwaysSupported with onfig.featureToggles.alertEnrichmentfix --- .../components/rule-viewer/AlertRuleMenu.tsx | 7 +- .../rule-viewer/RuleViewer.test.tsx | 44 ++++++ .../components/rules/RulesTable.test.tsx | 3 + .../rule-view-page/extensions.tsx | 41 +++--- .../rule-view-page/navigation.test.ts | 10 +- .../unified/hooks/useAbilities.test.tsx | 126 ++++++++++++++++++ .../alerting/unified/hooks/useAbilities.ts | 31 ++++- public/app/types/accessControl.ts | 4 + 8 files changed, 244 insertions(+), 22 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx index a36a1fada67..8834fd72580 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx @@ -13,7 +13,9 @@ import { PromAlertingRuleState, RulerRuleDTO } from 'app/types/unified-alerting- import { AlertRuleAction, + EnrichmentAction, skipToken, + useEnrichmentAbility, useGrafanaPromRuleAbilities, useRulerRuleAbilities, } from '../../hooks/useAbilities'; @@ -112,6 +114,8 @@ const AlertRuleMenu = ({ const extensionsAvailable = ruleExtensionLinks.length > 0; + const [enrichmentReadSupported, enrichmentReadAllowed] = useEnrichmentAbility(EnrichmentAction.Read); + /** * Since Incident isn't available as an open-source product we shouldn't show it for Open-Source licenced editions of Grafana. * We should show it in development mode @@ -139,7 +143,8 @@ const AlertRuleMenu = ({ ruleUid && handleManageEnrichments && config.featureToggles.alertingEnrichmentPerRule && - config.featureToggles.alertEnrichment; + enrichmentReadSupported && + enrichmentReadAllowed; const menuItems = ( <> diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx index 253418c3135..9e91bd34178 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx @@ -8,6 +8,10 @@ import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmana import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting'; +import { + __clearRuleViewTabsForTests, + addEnrichmentSection, +} from '../../enterprise-components/rule-view-page/extensions'; import { getCloudRule, getGrafanaRule, @@ -86,6 +90,18 @@ const openSilenceDrawer = async () => { await screen.findByText(/Configure silences/i); }; +beforeAll(() => { + // Register the enrichment tab for all tests + addEnrichmentSection(); +}); + +afterEach(() => { + // Clear tabs after each test to prevent interference + __clearRuleViewTabsForTests(); + // Re-register for next test + addEnrichmentSection(); +}); + beforeEach(() => { grantPermissionsHelper([ AccessControlAction.AlertingRuleCreate, @@ -484,6 +500,34 @@ describe('RuleViewer', () => { ); expect(screen.getByTestId('enrichment-section')).toBeInTheDocument(); }); + + it('should show enrichment tab when user has enrichments:read permission', async () => { + grantPermissionsHelper([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingEnrichmentsRead]); + + await renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.Query); + + // Check if enrichment tab exists in the navigation + expect(screen.getByText('Alert enrichment')).toBeInTheDocument(); + }); + + it('should hide enrichment tab when user lacks enrichments:read permission', async () => { + grantPermissionsHelper([AccessControlAction.AlertingRuleRead]); + + await renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.Query); + + // Check that enrichment tab does not exist in the navigation + expect(screen.queryByText('Alert enrichment')).not.toBeInTheDocument(); + }); + + it('should show enrichment tab for admin users', async () => { + grantPermissionsHelper([AccessControlAction.AlertingRuleRead]); + jest.spyOn(require('../../utils/misc'), 'isAdmin').mockReturnValue(true); + + await renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.Query); + + // Admin should see the tab even without explicit permission + expect(screen.getByText('Alert enrichment')).toBeInTheDocument(); + }); }); }); diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx index a4be47bbf81..61c143ab843 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx @@ -7,6 +7,7 @@ import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { AlertRuleAction, useAlertRuleAbility, + useEnrichmentAbility, useGrafanaPromRuleAbilities, useGrafanaPromRuleAbility, useRulerRuleAbilities, @@ -29,6 +30,7 @@ const mocks = { useGrafanaPromRuleAbility: jest.mocked(useGrafanaPromRuleAbility), useRulerRuleAbilities: jest.mocked(useRulerRuleAbilities), useGrafanaPromRuleAbilities: jest.mocked(useGrafanaPromRuleAbilities), + useEnrichmentAbility: jest.mocked(useEnrichmentAbility), }; setPluginLinksHook(() => ({ @@ -63,6 +65,7 @@ describe('RulesTable RBAC', () => { mocks.useAlertRuleAbility.mockReturnValue([false, false]); mocks.useRulerRuleAbility.mockReturnValue([false, false]); mocks.useGrafanaPromRuleAbility.mockReturnValue([false, false]); + mocks.useEnrichmentAbility.mockReturnValue([false, false]); // Plural hooks (used by AlertRuleMenu) - need to return arrays based on input actions mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => { diff --git a/public/app/features/alerting/unified/enterprise-components/rule-view-page/extensions.tsx b/public/app/features/alerting/unified/enterprise-components/rule-view-page/extensions.tsx index a4d5034e103..bc366ec1cfe 100644 --- a/public/app/features/alerting/unified/enterprise-components/rule-view-page/extensions.tsx +++ b/public/app/features/alerting/unified/enterprise-components/rule-view-page/extensions.tsx @@ -5,6 +5,7 @@ import { t } from '@grafana/i18n'; import { FeatureBadge, useStyles2 } from '@grafana/ui'; import { useAlertRule } from '../../components/rule-viewer/RuleContext'; +import { EnrichmentAction, useEnrichmentAbility } from '../../hooks/useAbilities'; import { rulerRuleType } from '../../utils/rules'; type SetActiveTab = (tab: string) => void; @@ -14,7 +15,7 @@ type RuleViewTabBuilderArgs = { setActiveTab: SetActiveTab; }; -type RuleViewTabBuilder = (args: RuleViewTabBuilderArgs) => NavModelItem; +type RuleViewTabBuilder = (args: RuleViewTabBuilderArgs) => NavModelItem | null; type RuleViewTabBuilderConfig = { filterOnlyGrafanaAlertRules: boolean; ruleViewTabBuilder: RuleViewTabBuilder; @@ -29,21 +30,35 @@ function registerRuleViewTab(builder: RuleViewTabBuilder) { }); } -export function useRuleViewExtensionTabs(args: RuleViewTabBuilderArgs): NavModelItem[] { - const { rule } = useAlertRule(); - const isGrafanaAlertRule = rulerRuleType.grafana.alertingRule(rule.rulerRule); +export function getRuleViewExtensionTabs(args: RuleViewTabBuilderArgs, isGrafanaAlertRule: boolean): NavModelItem[] { return ruleViewTabBuilders .filter((config) => { - if (config.filterOnlyGrafanaAlertRules) { - return isGrafanaAlertRule; + // Check if rule type matches requirement + if (config.filterOnlyGrafanaAlertRules && !isGrafanaAlertRule) { + return false; } return true; }) - .map((config) => config.ruleViewTabBuilder(args)); + .map((config) => config.ruleViewTabBuilder(args)) + .filter((item): item is NavModelItem => item !== null); +} + +export function useRuleViewExtensionTabs(args: RuleViewTabBuilderArgs): NavModelItem[] { + const { rule } = useAlertRule(); + const isGrafanaAlertRule = rulerRuleType.grafana.alertingRule(rule.rulerRule); + + return getRuleViewExtensionTabs(args, isGrafanaAlertRule); } export function addEnrichmentSection() { registerRuleViewTab(({ activeTab, setActiveTab }) => { + const [, canReadEnrichments] = useEnrichmentAbility(EnrichmentAction.Read); + + // Return null if user doesn't have permission (will be filtered out) + if (!canReadEnrichments) { + return null; + } + const tabId = 'enrichment'; return { text: t('alerting.use-page-nav.page-nav.text.enrichment', 'Alert enrichment'), @@ -59,18 +74,6 @@ export function __clearRuleViewTabsForTests() { ruleViewTabBuilders.splice(0, ruleViewTabBuilders.length); } -// ONLY FOR TESTS: non-hook version for testing -export function getRuleViewExtensionTabs(args: RuleViewTabBuilderArgs, isGrafanaAlertRule: boolean): NavModelItem[] { - return ruleViewTabBuilders - .filter((config) => { - if (config.filterOnlyGrafanaAlertRules) { - return isGrafanaAlertRule; - } - return true; - }) - .map((config) => config.ruleViewTabBuilder(args)); -} - function getStyles() { return { tabSuffix: css({ diff --git a/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.test.ts b/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.test.ts index 9fdaa2a74d9..b4ea7312d86 100644 --- a/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.test.ts +++ b/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.test.ts @@ -1,12 +1,20 @@ -import { beforeEach, describe, expect, it } from '@jest/globals'; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; import { addRulePageEnrichmentSection } from '../../components/rule-viewer/tabs/extensions/RuleViewerExtension'; +import { useEnrichmentAbility } from '../../hooks/useAbilities'; import { __clearRuleViewTabsForTests, addEnrichmentSection, getRuleViewExtensionTabs } from './extensions'; +jest.mock('../../hooks/useAbilities'); + +const mocks = { + useEnrichmentAbility: jest.mocked(useEnrichmentAbility), +}; + describe('rule-view-page navigation', () => { beforeEach(() => { __clearRuleViewTabsForTests(); + mocks.useEnrichmentAbility.mockReturnValue([false, true]); }); it('does not include Alert enrichment tab when not registered', () => { diff --git a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx index 3fdd05e6c63..b08d31a631c 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx +++ b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx @@ -1,6 +1,7 @@ import { PropsWithChildren } from 'react'; import { getWrapper, render, renderHook, screen, waitFor } from 'test/test-utils'; +import { config } from '@grafana/runtime'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { setFolderAccessControl } from 'app/features/alerting/unified/mocks/server/configure'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; @@ -10,16 +11,21 @@ import { CombinedRule } from 'app/types/unified-alerting'; import { getCloudRule, getGrafanaRule, grantUserPermissions, mockDataSource } from '../mocks'; import { AlertmanagerProvider } from '../state/AlertmanagerContext'; +import { grantPermissionsHelper } from '../test/test-utils'; import { setupDataSources } from '../testSetup/datasources'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import * as misc from '../utils/misc'; import { AlertRuleAction, AlertmanagerAction, + EnrichmentAction, useAlertmanagerAbilities, useAlertmanagerAbility, useAllAlertRuleAbilities, useAllAlertmanagerAbilities, + useEnrichmentAbilities, + useEnrichmentAbility, } from './useAbilities'; /** @@ -213,6 +219,126 @@ describe('AlertRule abilities', () => { }); }); +describe('enrichment abilities', () => { + setupMswServer(); + + const originalFeatureToggle = config.featureToggles.alertEnrichment; + + beforeEach(() => { + // Default to feature toggle enabled + config.featureToggles.alertEnrichment = true; + }); + + afterEach(() => { + config.featureToggles.alertEnrichment = originalFeatureToggle; + }); + + it('should grant read and write permissions to admin users when feature is enabled', () => { + grantPermissionsHelper([]); + jest.spyOn(misc, 'isAdmin').mockReturnValue(true); + + const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() }); + + const [readSupported, readAllowed] = result.current[EnrichmentAction.Read]; + const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write]; + + expect(readSupported).toBe(true); + expect(readAllowed).toBe(true); + expect(writeSupported).toBe(true); + expect(writeAllowed).toBe(true); + }); + + it('should grant read permission when user has enrichments:read permission', () => { + jest.spyOn(misc, 'isAdmin').mockReturnValue(false); + grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsRead]); + + const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() }); + + const [readSupported, readAllowed] = result.current[EnrichmentAction.Read]; + const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write]; + + expect(readSupported).toBe(true); + expect(readAllowed).toBe(true); + expect(writeSupported).toBe(true); + expect(writeAllowed).toBe(false); + }); + + it('should grant write permission when user has enrichments:write permission', () => { + jest.spyOn(misc, 'isAdmin').mockReturnValue(false); + grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsWrite]); + + const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() }); + + const [readSupported, readAllowed] = result.current[EnrichmentAction.Read]; + const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write]; + + expect(readSupported).toBe(true); + expect(readAllowed).toBe(false); + expect(writeSupported).toBe(true); + expect(writeAllowed).toBe(true); + }); + + it('should grant both read and write permissions when user has both permissions', () => { + jest.spyOn(misc, 'isAdmin').mockReturnValue(false); + grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsRead, AccessControlAction.AlertingEnrichmentsWrite]); + + const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() }); + + const [readSupported, readAllowed] = result.current[EnrichmentAction.Read]; + const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write]; + + expect(readSupported).toBe(true); + expect(readAllowed).toBe(true); + expect(writeSupported).toBe(true); + expect(writeAllowed).toBe(true); + }); + + it('should deny all permissions when user is not admin and has no permissions', () => { + jest.spyOn(misc, 'isAdmin').mockReturnValue(false); + grantPermissionsHelper([]); + + const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() }); + + const [readSupported, readAllowed] = result.current[EnrichmentAction.Read]; + const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write]; + + expect(readSupported).toBe(true); + expect(readAllowed).toBe(false); + expect(writeSupported).toBe(true); + expect(writeAllowed).toBe(false); + }); + + it('should return correct ability for specific action using useEnrichmentAbility', () => { + jest.spyOn(misc, 'isAdmin').mockReturnValue(false); + grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsRead]); + + const { result } = renderHook(() => useEnrichmentAbility(EnrichmentAction.Read), { wrapper: wrapper() }); + + const [supported, allowed] = result.current; + + expect(supported).toBe(true); + expect(allowed).toBe(true); + }); + + it('should report enrichments as not supported when feature toggle is disabled', () => { + config.featureToggles.alertEnrichment = false; + jest.spyOn(misc, 'isAdmin').mockReturnValue(true); + grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsRead, AccessControlAction.AlertingEnrichmentsWrite]); + + const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() }); + + const [readSupported, readAllowed] = result.current[EnrichmentAction.Read]; + const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write]; + + // Enrichments not supported when feature toggle is off + expect(readSupported).toBe(false); + expect(writeSupported).toBe(false); + // Permissions would be granted if it were supported + expect(readAllowed).toBe(true); + expect(writeAllowed).toBe(true); + }); +}); + function createAlertmanagerWrapper(alertmanagerSourceName: string) { const ProviderWrapper = getWrapper({ renderWithRouter: true }); const wrapperComponent = (props: PropsWithChildren) => ( diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index 5635a3d666a..535d99d0bc0 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react'; +import { config } from '@grafana/runtime'; import { contextSrv as ctx } from 'app/core/services/context_srv'; import { PERMISSIONS_CONTACT_POINTS_READ } from 'app/features/alerting/unified/components/contact-points/permissions'; import { @@ -96,6 +97,12 @@ export enum AlertRuleAction { DeletePermanently = 'delete-alert-rule-permanently', } +// this enum lists all of the available actions we can perform with enrichments +export enum EnrichmentAction { + Read = 'read-enrichment', + Write = 'write-enrichment', +} + // this enum list all of the bulk actions we can perform on a folder export enum FolderBulkAction { Pause = 'pause-folder', // unpause permissions are the same as pause @@ -124,7 +131,7 @@ export enum AlertingAction { const AlwaysSupported = true; const NotSupported = false; -export type Action = AlertmanagerAction | AlertingAction | AlertRuleAction | FolderBulkAction; +export type Action = AlertmanagerAction | AlertingAction | AlertRuleAction | FolderBulkAction | EnrichmentAction; /** * Represents the ability to perform an action, with two distinct checks: @@ -189,6 +196,28 @@ export const useAlertingAbility = (action: AlertingAction): Ability => { return allAbilities[action]; }; +/** + * This one will check for enrichment abilities + */ +export const useEnrichmentAbilities = (): Abilities => { + const userIsAdmin = isAdmin(); + const hasReadPermission = ctx.hasPermission(AccessControlAction.AlertingEnrichmentsRead); + const hasWritePermission = ctx.hasPermission(AccessControlAction.AlertingEnrichmentsWrite); + + // Enrichments are only available when the feature toggle is enabled + const enrichmentsSupported = Boolean(config.featureToggles.alertEnrichment); + + return { + [EnrichmentAction.Read]: [enrichmentsSupported, userIsAdmin || hasReadPermission], + [EnrichmentAction.Write]: [enrichmentsSupported, userIsAdmin || hasWritePermission], + }; +}; + +export const useEnrichmentAbility = (action: EnrichmentAction): Ability => { + const allAbilities = useEnrichmentAbilities(); + return allAbilities[action]; +}; + /** * This hook will check if we support the action and have sufficient permissions for it on a single alert rule */ diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index abbf8969b05..d09459b9f9b 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -155,6 +155,10 @@ export enum AccessControlAction { AlertingTemplatesWrite = 'alert.notifications.templates:write', AlertingTemplatesDelete = 'alert.notifications.templates:delete', + // Alerting enrichments actions + AlertingEnrichmentsRead = 'alert.enrichments:read', + AlertingEnrichmentsWrite = 'alert.enrichments:write', + PluginsInstall = 'plugins:install', PluginsWrite = 'plugins:write', From 291e3ea9cfe2ca0826c57c3ebe9e93149106ae13 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Wed, 26 Nov 2025 12:00:15 +0100 Subject: [PATCH 123/378] Logs: Persist sort order in the Explore URL (#114350) * Logs: store sort order in the URL * ToolbarExtensionPoint: pass sort order to extension * Logs: send sort order in links * ToolbarExtensionPoint: pass panelState instead of sortOrder * Update test * Remove condition * Logs: initialize sort order and remove unnecessary check --- packages/grafana-data/src/types/explore.ts | 3 +- public/app/features/explore/Logs/Logs.tsx | 59 ++++++++++++------- .../extensions/ToolbarExtensionPoint.test.tsx | 25 +++++++- .../extensions/ToolbarExtensionPoint.tsx | 21 +++++-- 4 files changed, 77 insertions(+), 31 deletions(-) diff --git a/packages/grafana-data/src/types/explore.ts b/packages/grafana-data/src/types/explore.ts index c3755fa954f..71fd7eae35d 100644 --- a/packages/grafana-data/src/types/explore.ts +++ b/packages/grafana-data/src/types/explore.ts @@ -1,4 +1,4 @@ -import { DataQuery } from '@grafana/schema'; +import { DataQuery, LogsSortOrder } from '@grafana/schema'; import { PreferredVisualisationType } from './data'; import { SelectableValue } from './select'; @@ -84,6 +84,7 @@ export interface ExploreLogsPanelState { // Used for logs table visualisation, contains the refId of the dataFrame that is currently visualized refId?: string; displayedFields?: string[]; + sortOrder?: LogsSortOrder; } export interface SplitOpenOptions { diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 02b63224e76..b958aebcc62 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -197,7 +197,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { ); const [dedupStrategy, setDedupStrategy] = useState(LogsDedupStrategy.none); const [logsSortOrder, setLogsSortOrder] = useState( - store.get(SETTINGS_KEYS.logsSortOrder) || LogsSortOrder.Descending + panelState?.logs?.sortOrder ?? store.get(SETTINGS_KEYS.logsSortOrder) ?? LogsSortOrder.Descending ); const [isFlipping, setIsFlipping] = useState(false); const [displayedFields, setDisplayedFields] = useState(panelState?.logs?.displayedFields ?? []); @@ -269,6 +269,18 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { } }, [dispatch, exploreId, loading, panelState, previousLoading]); + useEffect(() => { + // Initialize URL sort order + if (!panelState?.logs?.sortOrder) { + dispatch( + changePanelState(exploreId, 'logs', { + ...panelState, + sortOrder: logsSortOrder, + }) + ); + } + }, [dispatch, exploreId, logsSortOrder, panelState]); + useEffect(() => { const visualisationType = panelState?.logs?.visualisationType ?? getDefaultVisualisationType(); setVisualisationType(visualisationType); @@ -287,23 +299,17 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { useUnmount(() => { // If we're unmounting logs (e.g. switching to another datasource), we need to remove the logs specific panel state, otherwise it will persist in the explore url - if ( - panelState?.logs?.columns || - panelState?.logs?.refId || - panelState?.logs?.labelFieldName || - panelState?.logs?.displayedFields - ) { - dispatch( - changePanelState(exploreId, 'logs', { - ...panelState?.logs, - columns: undefined, - visualisationType: visualisationType, - labelFieldName: undefined, - refId: undefined, - displayedFields: undefined, - }) - ); - } + dispatch( + changePanelState(exploreId, 'logs', { + ...panelState?.logs, + columns: undefined, + visualisationType: visualisationType, + labelFieldName: undefined, + refId: undefined, + displayedFields: undefined, + sortOrder: undefined, + }) + ); }); const updatePanelState = useCallback( @@ -398,8 +404,14 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { dispatch(changeQueries({ exploreId, queries: newQueries })); dispatch(runQueries({ exploreId })); } + dispatch( + changePanelState(exploreId, 'logs', { + ...panelState?.logs, + sortOrder: newSortOrder, + }) + ); }, - [dispatch, exploreId, logsQueries] + [dispatch, exploreId, logsQueries, panelState?.logs] ); const onChangeLogsSortOrder = useCallback( @@ -588,7 +600,12 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { const urlState = getUrlStateFromPaneState(getState().explore.panes[exploreId]!); urlState.panelsState = { ...panelState, - logs: { id: row.uid, visualisationType: visualisationType ?? getDefaultVisualisationType(), displayedFields }, + logs: { + id: row.uid, + visualisationType: visualisationType ?? getDefaultVisualisationType(), + displayedFields, + sortOrder: logsSortOrder, + }, }; urlState.range = getLogsPermalinkRange(row, logRows, absoluteRange); @@ -604,7 +621,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { logRowLevel: row.logLevel, }); }, - [absoluteRange, displayedFields, exploreId, logRows, panelState, visualisationType] + [absoluteRange, displayedFields, exploreId, logRows, logsSortOrder, panelState, visualisationType] ); const scrollToTopLogs = useCallback(() => { diff --git a/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx b/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx index d3988d68e95..6fd93785099 100644 --- a/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx +++ b/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx @@ -3,9 +3,9 @@ import userEvent from '@testing-library/user-event'; import { ReactNode } from 'react'; import { Provider } from 'react-redux'; -import { PluginExtensionPoints, PluginExtensionTypes } from '@grafana/data'; +import { ExplorePanelsState, PluginExtensionPoints, PluginExtensionTypes } from '@grafana/data'; import { usePluginLinks } from '@grafana/runtime'; -import { DataQuery } from '@grafana/schema'; +import { DataQuery, LogsSortOrder } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; import { ExplorePanelData, ExploreState } from 'app/types/explore'; @@ -27,13 +27,14 @@ const usePluginLinksMock = jest.mocked(usePluginLinks); type storeOptions = { targets: DataQuery[]; data: ExplorePanelData; + panelsState?: ExplorePanelsState; }; function renderWithExploreStore( children: ReactNode, options: storeOptions = { targets: [{ refId: 'A' }], data: createEmptyQueryResponse() } ) { - const { targets, data } = options; + const { targets, data, panelsState } = options; const store = configureStore({ explore: { panes: { @@ -43,6 +44,7 @@ function renderWithExploreStore( range: { raw: { from: 'now-1h', to: 'now' }, }, + panelsState, }, }, } as unknown as ExploreState, @@ -90,6 +92,9 @@ describe('ToolbarExtensionPoint', () => { isLoading: false, }); }); + beforeEach(() => { + jest.mocked(usePluginLinksMock).mockClear(); + }); it('should render "Add" extension point menu button', () => { renderWithExploreStore(setupToolbarExtensionPoint()); @@ -180,6 +185,20 @@ describe('ToolbarExtensionPoint', () => { expect(extensionPointId).toBe(PluginExtensionPoints.ExploreToolbarAction); }); + + it('should pass panelsState to the extensions', async () => { + const panelsState: ExplorePanelsState = { + logs: { sortOrder: LogsSortOrder.Ascending, displayedFields: ['time', 'body'] }, + }; + const targets = [{ refId: 'A' }]; + const data = createEmptyQueryResponse(); + renderWithExploreStore(setupToolbarExtensionPoint(), { targets, data, panelsState }); + + const [options] = usePluginLinksMock.mock.calls[0]; + const { context } = options; + + expect(context).toHaveProperty('panelsState', panelsState); + }); }); describe('with extension points without categories', () => { diff --git a/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx b/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx index 070e48caa47..f9d6abe3df2 100644 --- a/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx +++ b/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx @@ -1,6 +1,12 @@ import { ReactElement, useMemo, useState } from 'react'; -import { type PluginExtensionLink, PluginExtensionPoints, RawTimeRange, getTimeZone } from '@grafana/data'; +import { + type ExplorePanelsState, + type PluginExtensionLink, + PluginExtensionPoints, + RawTimeRange, + getTimeZone, +} from '@grafana/data'; import { reportInteraction, usePluginLinks } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; @@ -89,13 +95,14 @@ export type PluginExtensionExploreContext = { timeRange: RawTimeRange; timeZone: TimeZone; shouldShowAddCorrelation: boolean; + panelsSate?: ExplorePanelsState; }; function useExtensionPointContext(props: Props): PluginExtensionExploreContext { const { exploreId, timeZone } = props; const isCorrelationDetails = useSelector(selectCorrelationDetails); const isCorrelationsEditorMode = isCorrelationDetails?.editorMode || false; - const { queries, queryResponse, range } = useSelector(getExploreItemSelector(exploreId))!; + const { queries, queryResponse, range, panelsState } = useSelector(getExploreItemSelector(exploreId))!; const isLeftPane = useSelector(isLeftPaneSelector(exploreId)); const datasourceUids = queries.map((query) => query?.datasource?.uid).filter((uid) => uid !== undefined); @@ -110,16 +117,18 @@ function useExtensionPointContext(props: Props): PluginExtensionExploreContext { timeRange: range.raw, timeZone: getTimeZone({ timeZone }), shouldShowAddCorrelation: canWriteCorrelations && !isCorrelationsEditorMode && isLeftPane && numUniqueIds === 1, + panelsState, }; }, [ + canWriteCorrelations, exploreId, + isCorrelationsEditorMode, + isLeftPane, + numUniqueIds, + panelsState, queries, queryResponse, range.raw, timeZone, - canWriteCorrelations, - isCorrelationsEditorMode, - isLeftPane, - numUniqueIds, ]); } From a3dacabedf479fab9e66448b28b15e6e41fd0ded Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Wed, 26 Nov 2025 12:10:54 +0100 Subject: [PATCH 124/378] MSSQL: Current-user authentication (#113977) * Moving things around * Update frontend to support CUA * Add CUA support to backend * Copy parseURL function to where it's used * Update test * Remove experimental-strip-types * Docs * A bit more of a refactor to reduce complexity * Revert "Remove experimental-strip-types" This reverts commit 70fbc1c0cd46aee3acddaf36e9c50e6cb76d0147. * Review * Docs updates * Another docs fix --- .../datasources/mssql/configure/index.md | 153 +++++++++++++++++- pkg/tsdb/mssql/azure/connection.go | 15 +- pkg/tsdb/mssql/mssql.go | 9 +- pkg/tsdb/mssql/sqleng/connection.go | 16 +- pkg/tsdb/mssql/sqleng/handler_checkhealth.go | 10 +- pkg/tsdb/mssql/sqleng/sql_engine.go | 110 ++++++++++--- pkg/tsdb/mssql/sqleng/sql_engine_test.go | 2 +- .../mssql/azureauth/AzureAuthSettings.tsx | 2 + .../mssql/azureauth/AzureCredentialsForm.tsx | 22 ++- 9 files changed, 290 insertions(+), 49 deletions(-) diff --git a/docs/sources/datasources/mssql/configure/index.md b/docs/sources/datasources/mssql/configure/index.md index 3329d585159..4d3eb60ee81 100644 --- a/docs/sources/datasources/mssql/configure/index.md +++ b/docs/sources/datasources/mssql/configure/index.md @@ -74,6 +74,21 @@ refs: destination: /docs/grafana//datasources/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//datasources/ + configure-grafana-azure-auth: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-access/configure-authentication/azuread/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-access/configure-authentication/azuread/ + configure-grafana-azure: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-grafana/#azure + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-grafana/#azure + configure-grafana-azure-auth-scopes: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana --- # Configure the Microsoft SQL Server data source @@ -138,14 +153,19 @@ If you're using an older version of Microsoft SQL Server like 2008 and 2008R2, y **Authentication:** -| Authentication Type | Description | Credentials / Fields | -| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| **SQL Server Authentication** | Default method to connect to MSSQL. Use a SQL Server or Windows login in `DOMAIN\User` format. | - **Username**: SQL Server username
- **Password**: SQL Server password | -| **Windows Authentication**
(Integrated Security) | Uses the logged-in Windows user's credentials via single sign-on. Available only when SQL Server allows Windows Authentication. | No input required; uses the logged-in Windows user's credentials | -| **Windows AD**
(Username/Password) | Authenticates a domain user with their Active Directory username and password. | - **Username**: `user@example.com`
- **Password**: Active Directory password | -| **Windows AD**
(Keytab) | Authenticates a domain user using a keytab file. | - **Username**: `user@example.com`
- **Keytab file path**: Path to your keytab file | -| **Windows AD**
(Credential Cache) | Uses a Kerberos credential cache already loaded in memory (e.g., from a prior `kinit` command). No file needed. | - **Credential cache path**: Path to in-memory credential (e.g., `/tmp/krb5cc_1000`) | -| **Windows AD**
(Credential Cache File) | Authenticates a domain user using a credential cache file (`.ccache`). | - **Username**: `user@example.com`
- **Credential cache file path**: e.g., `/home/grot/cache.json` | +{{< admonition type="note" >}} +In order to use Azure AD Authentication the toggle `auth.azure_auth_enabled` must be set to `true` in the Grafana configuration file. +{{< /admonition >}} + +| Authentication Type | Description | Credentials / Fields | +| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **SQL Server Authentication** | Default method to connect to MSSQL. Use a SQL Server or Windows login in `DOMAIN\User` format. | - **Username**: SQL Server username
- **Password**: SQL Server password | +| **Windows Authentication**
(Integrated Security) | Uses the logged-in Windows user's credentials via single sign-on. Available only when SQL Server allows Windows Authentication. | No input required; uses the logged-in Windows user's credentials | +| **Windows AD**
(Username/Password) | Authenticates a domain user with their Active Directory username and password. | - **Username**: `user@example.com`
- **Password**: Active Directory password | +| **Windows AD**
(Keytab) | Authenticates a domain user using a keytab file. | - **Username**: `user@example.com`
- **Keytab file path**: Path to your keytab file | +| **Windows AD**
(Credential Cache) | Uses a Kerberos credential cache already loaded in memory (e.g., from a prior `kinit` command). No file needed. | - **Credential cache path**: Path to in-memory credential (e.g., `/tmp/krb5cc_1000`) | +| **Windows AD**
(Credential Cache File) | Authenticates a domain user using a credential cache file (`.ccache`). | - **Username**: `user@example.com`
- **Credential cache file path**: e.g., `/home/grot/cache.json` | +| **Azure Entra ID (formerly Azure AD) Authentication** | Authenticates the data source using Azure authentication methods. | Details on the supported authentication methods and how to configure them can be found in the [Azure authentication section](./index.md#azure-entra-id-formerly-azure-ad-authentication). | **Additional settings:** @@ -185,6 +205,123 @@ After configuring your MSSQL data source options, click **Save & test** at the b **Database Connection OK** +### Azure Entra ID (formerly Azure AD) Authentication + +The following Azure authentication methods are supported: + +- Current User authentication +- App Registration +- Managed Identity +- Azure Entra Password + +The Azure SQL Server that you are connecting to should support Azure Entra authentication to support adding the App Registration as a user in the database. For configuration details, refer to the [Azure SQL documentation](https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-configure?view=azuresql&tabs=azure-portal). + +#### Current User authentication + +This is the recommended authentication mechanism when working with SQL Server instances that are hosted in Azure. It allows users to be authenticated to and query the database using their own credentials rather than long-lived credentials. + +This authentication method requires your Grafana instance to be configured with Azure Entra ID (formerly Active Directory) authentication for login. With Azure Entra ID login, this method can be used to forward the currently logged in user’s credentials to the data source. The users credentials will then be used when requesting data from the data source. For details on how to configure your Grafana instance using Azure Entra refer to the [documentation](ref:configure-grafana-azure-auth). + +{{< admonition type="note" >}} +Additional configuration is required to ensure that the App Registration used to login a user via Azure provides an access token with the permissions required by the data source. + +The App Registration must be configured to issue both **Access Tokens** and **ID Tokens**. + +1. In the Azure Portal, open the App Registration that requires configuration. +2. Select **Authentication** in the side menu. +3. Under **Implicit grant and hybrid flows** check both the **Access tokens** and **ID tokens** boxes. +4. Save the changes to ensure the App Registration is updated. + +The App Registration must also be configured with additional **API Permissions** to provide authenticated users with access to the APIs utilised by the data source. + +1. In the Azure Portal, open the App Registration that requires configuration. +1. Select **API Permissions** in the side menu. +1. Ensure the `openid`, `profile`, `email`, and `offline_access` permissions are present under the **Microsoft Graph** section. If not, they must be added. +1. Select **Add a permission** and choose the following permissions. They must be added individually. Refer to the [Azure documentation](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-access-web-apis) for more information. + - Select **APIs my organization uses** > Search for **Azure SQL** and select it > **Delegated permissions** > `user_impersonation` > **Add permissions** + +After all permissions have been added, the Azure authentication section in Grafana must be updated. The `scopes` section must be updated to include the `.default` scope to ensure that a token with access to all APIs declared on the App Registration is requested by Grafana. After updated the scopes value should equal: `.default openid email profile`. +{{< /admonition >}} + +This method of authentication doesn't inherently support all backend functionality as a user's credentials won't be in scope. Affected functionality includes alerting, reporting, and recorded queries. Also, note that query and resource caching is disabled by default for data sources using current user authentication. + +**To enable current user authentication for Grafana:** + +1. Set the `user_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure). + + ```ini + [azure] + user_identity_enabled = true + ``` + +2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Current User**. + +### App Registration + +You must create an app registration and service principal in Azure Entra to authenticate the data source. +For configuration details, refer to the [Azure documentation for service principals](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#get-tenant-and-app-id-values-for-signing-in). + +After the app registration has been created, make note of the tenant ID, client ID, and client secret. Take the following steps to add the app registration as a SQL user: + +1. Connect to your Azure SQL database as a user with administrative permissions (the user used here must have the ability to read your Azure Entra directory e.g. by possessing the `Directory Readers` role). +2. Run `CREATE USER [$IDENTITY_NAME] FROM EXTERNAL PROVIDER;`, substituting `IDENTITY_NAME` with the app registration name. +3. Grant the created user the appropriate level of permissions for your use-case. It is recommended that users configured for data sources only have reader permissions. + +After the appropriate permissions have been granted, configure the SQL Server data source to use the app registration: + +1. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **App Registration**. +2. Set the **Azure Cloud** value to the correct value. If you are using the Azure public cloud this will be **Azure**. +3. Set the **Directory (tenant) ID**, **Application (client) ID**, and **Client Secret** values to those for your app registration. + +### Managed Identity + +{{< admonition type="note" >}} +Managed Identity is available only in [Azure Managed Grafana](https://azure.microsoft.com/en-us/products/managed-grafana) or Grafana OSS/Enterprise when deployed in Azure. It is not available in Grafana Cloud. +{{< /admonition >}} + +You can use managed identity to configure SQL Server in Grafana if you host Grafana in Azure (such as an App Service or with Azure Virtual Machines) and have managed identity enabled on your VM. +This lets you securely authenticate data sources without manually configuring credentials via Azure AD App Registrations. +For details on Azure managed identities, refer to the [Azure documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview). + +**To enable managed identity for Grafana:** + +1. Set the `managed_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure). + + ```ini + [azure] + managed_identity_enabled = true + ``` + +2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Managed Identity**. + + This hides the directory ID, application ID, and client secret fields, and the data source uses managed identity to authenticate to SQL Server. + +3. You can set the `managed_identity_client_id` field in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure) to allow a user-assigned managed identity to be used instead of the default system-assigned identity. + +Ensure that the managed identity used is added to your Azure SQL instance as a user. + +### Azure Entra Password + +{{< admonition type="warning" >}} +Azure Entra Password is not a recommended authentication mechanism as it requires configuration using a single users password. Consider an alternative authentication method such as current user authentication or app registration. +{{< /admonition >}} + +You can connect to an Azure SQL database using the username and password of a user that has permissions in the desired database. This also requires an app registration to be configured with access to the database. + +**To enable Azure Entra password for Grafana:** + +1. Set the `azure_entra_password_credentials_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure). + + ```ini + [azure] + azure_entra_password_credentials_enabled = true + ``` + +2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Azure Entra Password**. +3. Set the **User ID** value to the username of the user in the Azure SQL database. +4. Set the **Application Client ID** to the client ID of the app registration that has been added to the Azure SQL database +5. Set the **Password** value to the password of the user in the Azure SQL database. + ### Min time interval The **Min time interval** setting defines a lower limit for the [`$__interval`](ref:add-template-variables-interval) and [`$__interval_ms`][add-template-variables-interval_ms] variables. diff --git a/pkg/tsdb/mssql/azure/connection.go b/pkg/tsdb/mssql/azure/connection.go index 40978229b39..1d3c2b46cc2 100644 --- a/pkg/tsdb/mssql/azure/connection.go +++ b/pkg/tsdb/mssql/azure/connection.go @@ -4,14 +4,15 @@ import ( "fmt" "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" + "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" ) -func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentials, azureManagedIdentityClientId string, azureEntraPasswordCredentialsEnabled bool) (string, error) { +func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentials, azureSettings *azsettings.AzureSettings, userAssertion string) (string, error) { connStr := "" switch c := azureCredentials.(type) { case *azcredentials.AzureManagedIdentityCredentials: - if azureManagedIdentityClientId != "" { - connStr += fmt.Sprintf("user id=%s;", azureManagedIdentityClientId) + if azureSettings.ManagedIdentityClientId != "" { + connStr += fmt.Sprintf("user id=%s;", azureSettings.ManagedIdentityClientId) } connStr += fmt.Sprintf("fedauth=%s;", "ActiveDirectoryManagedIdentity") @@ -23,7 +24,7 @@ func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentia "ActiveDirectoryApplication", ) case *azcredentials.AzureEntraPasswordCredentials: - if azureEntraPasswordCredentialsEnabled { + if azureSettings.AzureEntraPasswordCredentialsEnabled { connStr += fmt.Sprintf("user id=%s;password=%s;applicationclientid=%s;fedauth=%s;", c.UserId, c.Password, @@ -33,6 +34,12 @@ func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentia } else { return "", fmt.Errorf("azure entra password authentication is not enabled") } + case *azcredentials.AadCurrentUserCredentials: + if userAssertion == "" { + return "", fmt.Errorf("user ID token is empty but required for current user authentication") + } + connStr += fmt.Sprintf("user id=%s;userassertion=%s;password=%s;fedauth=%s;", + azureSettings.UserIdentityTokenEndpoint.ClientId, userAssertion, azureSettings.UserIdentityTokenEndpoint.ClientSecret, "ActiveDirectoryOnBehalfOf") default: return "", fmt.Errorf("unsupported azure authentication type") } diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index ca8595edd2e..77de0b82603 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" + "github.com/grafana/grafana-azure-sdk-go/v2/azusercontext" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" @@ -43,7 +44,8 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) if err != nil { return nil, err } - return dsHandler.QueryData(ctx, req) + + return dsHandler.QueryData(azusercontext.WithUserFromQueryReq(ctx, req), req) } func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.InstanceFactoryFunc { @@ -53,6 +55,8 @@ func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc if err != nil { return nil, err } + pluginCfg := backend.PluginConfigFromContext(ctx) + jsonData := sqleng.JsonData{ MaxOpenConns: sqlCfg.DefaultMaxOpenConns, MaxIdleConns: sqlCfg.DefaultMaxIdleConns, @@ -87,6 +91,7 @@ func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc Updated: settings.Updated, UID: settings.UID, DecryptedSecureJSONData: settings.DecryptedSecureJSONData, + OrgID: pluginCfg.OrgID, } userFacingDefaultError, err := grafCfg.UserFacingDefaultError() @@ -117,5 +122,5 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque return nil, err } - return dsHandler.CheckHealth(ctx, req) + return dsHandler.CheckHealth(azusercontext.WithUserFromHealthCheckReq(ctx, req), req) } diff --git a/pkg/tsdb/mssql/sqleng/connection.go b/pkg/tsdb/mssql/sqleng/connection.go index 58a6d014219..7b779b547c8 100644 --- a/pkg/tsdb/mssql/sqleng/connection.go +++ b/pkg/tsdb/mssql/sqleng/connection.go @@ -1,14 +1,14 @@ package sqleng import ( - "context" "database/sql" "fmt" "time" "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" - "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" "github.com/grafana/grafana/pkg/tsdb/mssql/azure" "github.com/grafana/grafana/pkg/tsdb/mssql/kerberos" "github.com/grafana/grafana/pkg/tsdb/mssql/utils" @@ -17,7 +17,7 @@ import ( "github.com/microsoft/go-mssqldb/azuread" ) -func newMSSQL(ctx context.Context, driverName string, rowLimit int64, dsInfo DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*sql.DB, error) { +func newMSSQL(driverName string, rowLimit int64, dsInfo DataSourceInfo, cnnstr string, logger log.Logger, proxyClient proxy.Client) (*sql.DB, error) { var connector *mssql.Connector var err error if driverName == "azuresql" { @@ -31,12 +31,6 @@ func newMSSQL(ctx context.Context, driverName string, rowLimit int64, dsInfo Dat return nil, fmt.Errorf("mssql connector creation failed") } - proxyClient, err := settings.ProxyClient(ctx) - if err != nil { - logger.Error("mssql proxy creation failed", "error", err) - return nil, fmt.Errorf("mssql proxy creation failed") - } - if proxyClient.SecureSocksProxyEnabled() { dialer, err := proxyClient.NewSecureSocksProxyContextDialer() if err != nil { @@ -81,7 +75,7 @@ const ( kerberosCredentialCacheFile = "Windows AD: Credential cache file" // #nosec G101 ) -func generateConnectionString(dsInfo DataSourceInfo, azureManagedIdentityClientId string, azureEntraPasswordCredentialsEnabled bool, azureCredentials azcredentials.AzureCredentials, kerberosAuth kerberos.KerberosAuth, logger log.Logger) (string, error) { +func generateConnectionString(dsInfo DataSourceInfo, azureCredentials azcredentials.AzureCredentials, kerberosAuth kerberos.KerberosAuth, logger log.Logger, azureSettings *azsettings.AzureSettings, userAssertion string) (string, error) { const dfltPort = "0" var addr util.NetworkAddress if dsInfo.URL != "" { @@ -119,7 +113,7 @@ func generateConnectionString(dsInfo DataSourceInfo, azureManagedIdentityClientI switch dsInfo.JsonData.AuthenticationType { case azureAuthentication: - azureCredentialDSNFragment, err := azure.GetAzureCredentialDSNFragment(azureCredentials, azureManagedIdentityClientId, azureEntraPasswordCredentialsEnabled) + azureCredentialDSNFragment, err := azure.GetAzureCredentialDSNFragment(azureCredentials, azureSettings, userAssertion) if err != nil { return "", err } diff --git a/pkg/tsdb/mssql/sqleng/handler_checkhealth.go b/pkg/tsdb/mssql/sqleng/handler_checkhealth.go index 133be66a70a..9a4bddeb686 100644 --- a/pkg/tsdb/mssql/sqleng/handler_checkhealth.go +++ b/pkg/tsdb/mssql/sqleng/handler_checkhealth.go @@ -13,7 +13,15 @@ import ( ) func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - if err := e.db.Ping(); err != nil { + db, err := e.getDB(ctx) + if err != nil { + logCheckHealthError(ctx, e.dsInfo, err) + if strings.EqualFold(req.PluginContext.User.Role, "Admin") { + return ErrToHealthCheckResult(err) + } + return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: e.TransformQueryError(e.log, err).Error()}, nil + } + if err := db.Ping(); err != nil { logCheckHealthError(ctx, e.dsInfo, err) if strings.EqualFold(req.PluginContext.User.Role, "Admin") { return ErrToHealthCheckResult(err) diff --git a/pkg/tsdb/mssql/sqleng/sql_engine.go b/pkg/tsdb/mssql/sqleng/sql_engine.go index 3d27b0a9a01..f6997a720c5 100644 --- a/pkg/tsdb/mssql/sqleng/sql_engine.go +++ b/pkg/tsdb/mssql/sqleng/sql_engine.go @@ -2,6 +2,7 @@ package sqleng import ( "context" + "crypto/sha256" "database/sql" "encoding/json" "errors" @@ -16,9 +17,11 @@ import ( "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" + "github.com/grafana/grafana-azure-sdk-go/v2/azusercontext" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" @@ -74,6 +77,7 @@ type DataSourceInfo struct { Updated time.Time UID string DecryptedSecureJSONData map[string]string + OrgID int64 } type DataPluginConfiguration struct { @@ -97,6 +101,8 @@ type DataSourceHandler struct { azureCredentials azcredentials.AzureCredentials kerberosAuth kerberos.KerberosAuth driverName string + proxyClient proxy.Client + dbConnections sync.Map } type QueryJson struct { @@ -142,6 +148,12 @@ func NewQueryDataHandler(ctx context.Context, settings backend.DataSourceInstanc return nil, fmt.Errorf("error getting kerberos settings: %w", err) } + proxyClient, err := settings.ProxyClient(ctx) + if err != nil { + logger.Error("mssql proxy creation failed", "error", err) + return nil, fmt.Errorf("mssql proxy creation failed") + } + queryDataHandler := DataSourceHandler{ queryResultTransformer: &queryResultTransformer, macroEngine: newMssqlMacroEngine(), @@ -154,6 +166,7 @@ func NewQueryDataHandler(ctx context.Context, settings backend.DataSourceInstanc azureCredentials: azureCredentials, kerberosAuth: kerberosAuth, driverName: driverName, + proxyClient: proxyClient, } if len(config.TimeColumnNames) > 0 { @@ -164,18 +177,21 @@ func NewQueryDataHandler(ctx context.Context, settings backend.DataSourceInstanc queryDataHandler.metricColumnTypes = config.MetricColumnTypes } - cnnstr, err := generateConnectionString(config.DSInfo, azureSettings.ManagedIdentityClientId, azureSettings.AzureEntraPasswordCredentialsEnabled, azureCredentials, kerberosAuth, log) - if err != nil { - return nil, err - } + // Every auth method besides Azure AD Current User Identity can use a persistent DB connection + if config.DSInfo.JsonData.AuthenticationType != azureAuthentication || azureCredentials.AzureAuthType() != azcredentials.AzureAuthCurrentUserIdentity { + cnnstr, err := generateConnectionString(config.DSInfo, azureCredentials, kerberosAuth, log, azureSettings, "") + if err != nil { + return nil, err + } - db, err := newMSSQL(ctx, driverName, config.RowLimit, config.DSInfo, cnnstr, log, settings) - if err != nil { - logger.Error("Failed connecting to MSSQL", "err", err) - return nil, err - } + db, err := newMSSQL(driverName, config.RowLimit, config.DSInfo, cnnstr, log, proxyClient) + if err != nil { + logger.Error("Failed connecting to MSSQL", "err", err) + return nil, err + } - queryDataHandler.db = db + queryDataHandler.db = db + } return &queryDataHandler, nil } @@ -192,9 +208,52 @@ func (e *DataSourceHandler) Dispose() { e.log.Error("Failed to dispose db", "error", err) } } + + // Clear any cached user-specific connections + e.dbConnections.Range(func(_, conn interface{}) bool { + _ = conn.(*sql.DB).Close() + return true + }) + e.dbConnections.Clear() + e.log.Debug("DB disposed") } +func (e *DataSourceHandler) getDB(ctx context.Context) (*sql.DB, error) { + e.log.Debug("Getting DB...") + if e.dsInfo.JsonData.AuthenticationType != azureAuthentication || e.azureCredentials.AzureAuthType() != azcredentials.AzureAuthCurrentUserIdentity { + if e.db == nil { + return nil, fmt.Errorf("database connection is not initialized") + } + return e.db, nil + } + + userCtx, ok := azusercontext.GetCurrentUser(ctx) + if !ok { + return nil, fmt.Errorf("failed to get user from context for Azure Current User authentication") + } + cacheKey := fmt.Sprintf("mssql-%d-%x-%x-%s", e.dsInfo.OrgID, sha256.Sum256([]byte(userCtx.User.Email)), sha256.Sum256([]byte(userCtx.IdToken)), e.dsInfo.UID) + + conn, ok := e.dbConnections.Load(cacheKey) + if ok { + return conn.(*sql.DB), nil + } + + cnnstr, err := generateConnectionString(e.dsInfo, e.azureCredentials, e.kerberosAuth, e.log, e.azureSettings, userCtx.IdToken) + if err != nil { + return nil, err + } + + db, err := newMSSQL(e.driverName, e.rowLimit, e.dsInfo, cnnstr, e.log, e.proxyClient) + if err != nil { + logger.Error("Failed connecting to MSSQL", "err", err) + return nil, err + } + e.dbConnections.Store(cacheKey, db) + + return db, nil +} + func (e *DataSourceHandler) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { result := backend.NewQueryDataResponse() ch := make(chan DBDataResponse, len(req.Queries)) @@ -293,7 +352,12 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG return } - rows, err := e.db.QueryContext(queryContext, interpolatedQuery) + db, err := e.getDB(queryContext) + if err != nil { + errAppendDebug("retrieving database connection failed", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourcePlugin) + return + } + rows, err := db.QueryContext(queryContext, interpolatedQuery) if err != nil { errAppendDebug("db query error", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourceDownstream) return @@ -310,12 +374,19 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG return } + frame := e.processResponse(qm, rows, interpolatedQuery, errAppendDebug) + + queryResult.dataResponse.Frames = data.Frames{frame} + ch <- queryResult +} + +func (e *DataSourceHandler) processResponse(qm *dataQueryModel, rows *sql.Rows, interpolatedQuery string, errAppendDebug func(string, error, string, backend.ErrorSource)) *data.Frame { // Convert row.Rows to dataframe stringConverters := e.queryResultTransformer.GetConverterList() frame, err := sqlutil.FrameFromRows(rows, e.rowLimit, sqlutil.ToConverters(stringConverters...)...) if err != nil { errAppendDebug("convert frame from rows error", err, interpolatedQuery, backend.ErrorSourcePlugin) - return + return nil } if frame.Meta == nil { @@ -330,21 +401,19 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG // additionally-needed frame data stays intact and is correctly passed to our visulization. if frame.Rows() == 0 { frame.Fields = []*data.Field{} - queryResult.dataResponse.Frames = data.Frames{frame} - ch <- queryResult - return + return frame } if err := convertSQLTimeColumnsToEpochMS(frame, qm); err != nil { errAppendDebug("converting time columns failed", err, interpolatedQuery, backend.ErrorSourcePlugin) - return + return nil } if qm.Format == dataQueryFormatSeries { // time series has to have time column if qm.timeIndex == -1 { errAppendDebug("db has no time column", errors.New("time column is missing; make sure your data includes a time column for time series format or switch to a table format that doesn't require it"), interpolatedQuery, backend.ErrorSourceDownstream) - return + return nil } // Make sure to name the time field 'Time' to be backward compatible with Grafana pre-v8. @@ -362,7 +431,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG var err error if frame, err = convertSQLValueColumnToFloat(frame, i); err != nil { errAppendDebug("convert value to float failed", err, interpolatedQuery, backend.ErrorSourcePlugin) - return + return nil } } @@ -373,7 +442,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG frame, err = data.LongToWide(frame, qm.FillMissing) if err != nil { errAppendDebug("failed to convert long to wide series when converting from dataframe", err, interpolatedQuery, backend.ErrorSourcePlugin) - return + return nil } // Before 8x, a special metric column was used to name time series. The LongToWide transforms that into a metric label on the value field. @@ -408,8 +477,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG } } - queryResult.dataResponse.Frames = data.Frames{frame} - ch <- queryResult + return frame } // Interpolate provides global macros/substitutions for all sql datasources. diff --git a/pkg/tsdb/mssql/sqleng/sql_engine_test.go b/pkg/tsdb/mssql/sqleng/sql_engine_test.go index c4b21b2b849..8a354fadc2f 100644 --- a/pkg/tsdb/mssql/sqleng/sql_engine_test.go +++ b/pkg/tsdb/mssql/sqleng/sql_engine_test.go @@ -697,7 +697,7 @@ func TestGenerateConnectionString(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - connStr, err := generateConnectionString(tc.dataSource, "", false, nil, tc.kerberosCfg, logger) + connStr, err := generateConnectionString(tc.dataSource, nil, tc.kerberosCfg, logger, nil, "") require.NoError(t, err) assert.Equal(t, tc.expConnStr, connStr) }) diff --git a/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx b/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx index 0d53f030095..f38a417466e 100644 --- a/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx +++ b/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx @@ -15,6 +15,7 @@ export const AzureAuthSettings = (props: HttpSettingsBaseProps) => { const { dataSourceConfig: dsSettings, onChange } = props; const managedIdentityEnabled = config.azure.managedIdentityEnabled; const azureEntraPasswordCredentialsEnabled = config.azure.azureEntraPasswordCredentialsEnabled; + const userIdentityEnabled = config.azure.userIdentityEnabled; const credentials = useMemo(() => getCredentials(dsSettings), [dsSettings]); @@ -33,6 +34,7 @@ export const AzureAuthSettings = (props: HttpSettingsBaseProps) => { void; @@ -22,13 +23,25 @@ export const AzureCredentialsForm = (props: Props) => { azureCloudOptions, onCredentialsChange, disabled, + userIdentityEnabled, } = props; const onAuthTypeChange = (selected: SelectableValue) => { + const defaultAuthType = (() => { + if (managedIdentityEnabled) { + return 'msi'; + } + + if (userIdentityEnabled) { + return 'currentuser'; + } + + return 'clientsecret'; + })(); if (onCredentialsChange) { const updated: AzureCredentials = { ...credentials, - authType: selected.value || 'msi', + authType: selected.value || defaultAuthType, }; onCredentialsChange(updated); } @@ -131,6 +144,13 @@ export const AzureCredentialsForm = (props: Props) => { value: 'ad-password', label: t('azureauth.azure-credentials-form.auth-options-azure-entra', 'Azure Entra Password'), }); + + if (userIdentityEnabled) { + authTypeOptions.unshift({ + value: 'currentuser', + label: 'Current User', + }); + } } return ( From cef4449f14b2c9ebb92614d6ee32013cc08d08ff Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Wed, 26 Nov 2025 11:16:47 +0000 Subject: [PATCH 125/378] Folders: Send permissions query param with app platform for folder picker (#114158) --- .../rtkq/dashboard/v0alpha1/endpoints.gen.ts | 4 +-- pkg/registry/apis/dashboard/search.go | 4 +-- pkg/tests/apis/dashboard/search_test.go | 28 +++++++++---------- .../dashboard.grafana.app-v0alpha1.json | 8 +++--- .../NestedFolderPicker/useFoldersQuery.ts | 8 +++++- .../useFoldersQueryAppPlatform.ts | 8 +++--- 6 files changed, 33 insertions(+), 27 deletions(-) diff --git a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts index 264ac389e42..d2cc21d31e6 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts @@ -612,8 +612,8 @@ export type GetSearchApiArg = { tags?: string[]; /** find dashboards that reference a given libraryPanel */ libraryPanel?: string; - /** permission needed for the resource (View, Edit, Admin) */ - permission?: 'View' | 'Edit' | 'Admin'; + /** permission needed for the resource (view, edit, admin) */ + permission?: 'view' | 'edit' | 'admin'; /** sortable field */ sort?: string; /** number of results to return */ diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 6d85a66b344..19e5b71e0d9 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -136,9 +136,9 @@ func (s *SearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) * ParameterProps: spec3.ParameterProps{ Name: "permission", In: "query", - Description: "permission needed for the resource (View, Edit, Admin)", + Description: "permission needed for the resource (view, edit, admin)", Required: false, - Schema: spec.StringProperty().WithEnum("View", "Edit", "Admin"), + Schema: spec.StringProperty().WithEnum("view", "edit", "admin"), }, }, { diff --git a/pkg/tests/apis/dashboard/search_test.go b/pkg/tests/apis/dashboard/search_test.go index 129a44aee21..09d23398c3f 100644 --- a/pkg/tests/apis/dashboard/search_test.go +++ b/pkg/tests/apis/dashboard/search_test.go @@ -126,7 +126,7 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { // 2. Viewer searching with permission=View should find it { - res := callSearch(helper.Org1.Viewer, "permission=View") + res := callSearch(helper.Org1.Viewer, "permission=view") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -134,12 +134,12 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.True(t, found, "Viewer should find folder with permission=View") + require.True(t, found, "Viewer should find folder with permission=view") } // 3. Viewer searching with permission=Edit should NOT find it { - res := callSearch(helper.Org1.Viewer, "permission=Edit") + res := callSearch(helper.Org1.Viewer, "permission=edit") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -147,12 +147,12 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.False(t, found, "Viewer should NOT find folder with permission=Edit") + require.False(t, found, "Viewer should NOT find folder with permission=edit") } // 4. Editor searching with permission=Edit should find it { - res := callSearch(helper.Org1.Editor, "permission=Edit") + res := callSearch(helper.Org1.Editor, "permission=edit") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -160,12 +160,12 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.True(t, found, "Editor should find folder with permission=Edit") + require.True(t, found, "Editor should find folder with permission=edit") } // 5. Editor searching with permission=View should find it (Edit permission includes View) { - res := callSearch(helper.Org1.Editor, "permission=View") + res := callSearch(helper.Org1.Editor, "permission=view") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -173,7 +173,7 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.True(t, found, "Editor should find folder with permission=View (Edit includes View)") + require.True(t, found, "Editor should find folder with permission=view (Edit includes View)") } // 6. Editor searching without permission parameter should find it (has Edit access) @@ -191,7 +191,7 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { // 7. Admin searching with permission=View should find it (Admin has full access) { - res := callSearch(helper.Org1.Admin, "permission=View") + res := callSearch(helper.Org1.Admin, "permission=view") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -199,12 +199,12 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.True(t, found, "Admin should find folder with permission=View") + require.True(t, found, "Admin should find folder with permission=view") } // 8. Admin searching with permission=Edit should find it (Admin has full access) { - res := callSearch(helper.Org1.Admin, "permission=Edit") + res := callSearch(helper.Org1.Admin, "permission=edit") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -212,12 +212,12 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.True(t, found, "Admin should find folder with permission=Edit") + require.True(t, found, "Admin should find folder with permission=edit") } // 9. Admin searching with permission=Admin should find it (Admin has full access) { - res := callSearch(helper.Org1.Admin, "permission=Admin") + res := callSearch(helper.Org1.Admin, "permission=admin") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -225,7 +225,7 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.True(t, found, "Admin should find folder with permission=Admin") + require.True(t, found, "Admin should find folder with permission=admin") } // 10. Admin searching without permission parameter should find it (has Admin access) diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json index 06bad0ba004..238db87aff4 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json @@ -1823,13 +1823,13 @@ { "name": "permission", "in": "query", - "description": "permission needed for the resource (View, Edit, Admin)", + "description": "permission needed for the resource (view, edit, admin)", "schema": { "type": "string", "enum": [ - "View", - "Edit", - "Admin" + "view", + "edit", + "admin" ] } }, diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts b/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts index d852d9c241f..19ceab28b77 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts +++ b/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts @@ -22,7 +22,13 @@ export function useFoldersQuery({ rootFolderItem, }: UseFoldersQueryProps) { const resultLegacy = useFoldersQueryLegacy({ isBrowsing, openFolders, permission, rootFolderUID, rootFolderItem }); - const resultAppPlatform = useFoldersQueryAppPlatform({ isBrowsing, openFolders, rootFolderUID, rootFolderItem }); + const resultAppPlatform = useFoldersQueryAppPlatform({ + isBrowsing, + openFolders, + permission, + rootFolderUID, + rootFolderItem, + }); // Running the hooks themselves don't have any side effects, so we can just conditionally use one or the other // requestNextPage function from the result diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts b/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts index 34d068589c9..ae6c8574adb 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts +++ b/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts @@ -27,14 +27,14 @@ const collator = new Intl.Collator(); * does not have pagination at the moment. */ -type Props = Omit; export function useFoldersQueryAppPlatform({ isBrowsing, openFolders, /* rootFolderUID: configure which folder to start browsing from */ rootFolderUID, rootFolderItem, -}: Props) { + permission, +}: UseFoldersQueryProps) { const dispatch = useDispatch(); // Keep a list of all request subscriptions so we can unsubscribe from them when the component is unmounted @@ -89,7 +89,7 @@ export function useFoldersQueryAppPlatform({ return; } - const args = { folder: finalParentUid, type: 'folder' } as const; + const args = { folder: finalParentUid, type: 'folder', permission } as const; // Make a request const subscription = dispatch(dashboardAPIv0alpha1.endpoints.getSearch.initiate(args)); @@ -101,7 +101,7 @@ export function useFoldersQueryAppPlatform({ // the subscriptions are saved in a ref so they can be unsubscribed on unmount requestsRef.current = requestsRef.current.concat([subscription]); }, - [state, dispatch] + [state, dispatch, permission] ); // Unsubscribe from all requests when the component is unmounted From 8c7170727b24e33255ea6bcf613ffc724799bf7f Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Wed, 26 Nov 2025 12:54:50 +0100 Subject: [PATCH 126/378] `grafana-iam`: Prevent crashloops of the standalone IAM server (#114473) * `grafana-iam`: Prevent crashloops of the standalone IAM server --- pkg/registry/apis/iam/register.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index a464568c12f..99c9dda7d8d 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -219,7 +219,9 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge } storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store) - storage[teamResource.StoragePath("groups")] = b.teamGroupsHandler + if b.teamGroupsHandler != nil { + storage[teamResource.StoragePath("groups")] = b.teamGroupsHandler + } teamBindingResource := iamv0.TeamBindingResourceInfo teamBindingUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamBindingResource, opts.OptsGetter) From 21c1d9aedd0c5e2fdf2ce66f269e31a5e38f08b5 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Wed, 26 Nov 2025 12:58:00 +0100 Subject: [PATCH 127/378] Secrets: Remove unused methods and dependencies from secure value service (#114467) --- pkg/registry/apis/secret/contracts/secure_value.go | 3 +-- pkg/registry/apis/secret/service/secure_value.go | 10 ---------- pkg/registry/apis/secret/testutils/testutils.go | 2 +- pkg/server/wire_gen.go | 8 ++++---- pkg/storage/secret/metadata/secure_value_test.go | 4 ++-- 5 files changed, 8 insertions(+), 19 deletions(-) diff --git a/pkg/registry/apis/secret/contracts/secure_value.go b/pkg/registry/apis/secret/contracts/secure_value.go index bfb763be3f6..09702fa7f5f 100644 --- a/pkg/registry/apis/secret/contracts/secure_value.go +++ b/pkg/registry/apis/secret/contracts/secure_value.go @@ -11,7 +11,7 @@ import ( ) // The maximum size of a secure value in bytes when written as raw input. -const SecureValueRawInputMaxSizeBytes = 24576 // 24 KiB +const SecureValueRawInputMaxSizeBytes = 24 << 10 // 24 KiB type DecryptSecureValue struct { Keeper *string @@ -47,7 +47,6 @@ type SecureValueService interface { List(ctx context.Context, namespace xkube.Namespace) (*secretv1beta1.SecureValueList, error) Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, bool, error) Delete(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error) - SetKeeperAsActive(ctx context.Context, namespace xkube.Namespace, keeperName string) error } type SecureValueClient interface { diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go index 91f6a2e73ff..30b7f4a625d 100644 --- a/pkg/registry/apis/secret/service/secure_value.go +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -27,7 +27,6 @@ var _ contracts.SecureValueService = (*SecureValueService)(nil) type SecureValueService struct { tracer trace.Tracer accessClient claims.AccessClient - database contracts.Database secureValueMetadataStorage contracts.SecureValueMetadataStorage secureValueValidator contracts.SecureValueValidator secureValueMutator contracts.SecureValueMutator @@ -39,7 +38,6 @@ type SecureValueService struct { func ProvideSecureValueService( tracer trace.Tracer, accessClient claims.AccessClient, - database contracts.Database, secureValueMetadataStorage contracts.SecureValueMetadataStorage, secureValueValidator contracts.SecureValueValidator, secureValueMutator contracts.SecureValueMutator, @@ -50,7 +48,6 @@ func ProvideSecureValueService( return &SecureValueService{ tracer: tracer, accessClient: accessClient, - database: database, secureValueMetadataStorage: secureValueMetadataStorage, secureValueValidator: secureValueValidator, secureValueMutator: secureValueMutator, @@ -369,10 +366,3 @@ func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespa return sv, nil } - -func (s *SecureValueService) SetKeeperAsActive(ctx context.Context, namespace xkube.Namespace, name string) error { - if err := s.keeperMetadataStorage.SetAsActive(ctx, namespace, name); err != nil { - return fmt.Errorf("calling keeper metadata storage to set keeper as active: %w", err) - } - return nil -} diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index 6a5f1e40d30..37394905b79 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -152,7 +152,7 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { secureValueValidator := validator.ProvideSecureValueValidator() secureValueMutator := mutator.ProvideSecureValueMutator() - secureValueService := service.ProvideSecureValueService(tracer, accessClient, database, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, keeperService, nil) + secureValueService := service.ProvideSecureValueService(tracer, accessClient, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, keeperService, nil) decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, nil) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 279a67f4134..64ffd48ca9f 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -459,8 +459,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } ossDashboardStats := builders.ProvideDashboardStats() documentBuilderSupplier := search.ProvideDocumentBuilders(sqlStore, ossDashboardStats) - databaseDatabase := database4.ProvideDatabase(sqlStore, tracer) clockClock := clock.ProvideClock() + databaseDatabase := database4.ProvideDatabase(sqlStore, tracer) secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(clockClock, databaseDatabase, tracer, registerer) if err != nil { return nil, err @@ -508,7 +508,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - secureValueService := service5.ProvideSecureValueService(tracer, accessClient, databaseDatabase, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, ossKeeperService, registerer) + secureValueService := service5.ProvideSecureValueService(tracer, accessClient, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, ossKeeperService, registerer) inlineSecureValueSupport, err := inline.ProvideInlineSecureValueService(cfg, tracer, secureValueService, accessClient) if err != nil { return nil, err @@ -1107,8 +1107,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } ossDashboardStats := builders.ProvideDashboardStats() documentBuilderSupplier := search.ProvideDocumentBuilders(sqlStore, ossDashboardStats) - databaseDatabase := database4.ProvideDatabase(sqlStore, tracer) clockClock := clock.ProvideClock() + databaseDatabase := database4.ProvideDatabase(sqlStore, tracer) secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(clockClock, databaseDatabase, tracer, registerer) if err != nil { return nil, err @@ -1156,7 +1156,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - secureValueService := service5.ProvideSecureValueService(tracer, accessClient, databaseDatabase, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, ossKeeperService, registerer) + secureValueService := service5.ProvideSecureValueService(tracer, accessClient, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, ossKeeperService, registerer) inlineSecureValueSupport, err := inline.ProvideInlineSecureValueService(cfg, tracer, secureValueService, accessClient) if err != nil { return nil, err diff --git a/pkg/storage/secret/metadata/secure_value_test.go b/pkg/storage/secret/metadata/secure_value_test.go index 0eba9bbf9ce..dafd96fc3ad 100644 --- a/pkg/storage/secret/metadata/secure_value_test.go +++ b/pkg/storage/secret/metadata/secure_value_test.go @@ -618,7 +618,7 @@ func TestSecureValueServiceExampleBased(t *testing.T) { }, "actor-uid") require.NoError(t, err) - require.NoError(t, sut.SecureValueService.SetKeeperAsActive(t.Context(), xkube.Namespace(k1.Namespace), k1.Name)) + require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(k1.Namespace), k1.Name)) value := secretv1beta1.NewExposedSecureValue("v1") sv1, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(&secretv1beta1.SecureValue{ @@ -643,7 +643,7 @@ func TestSecureValueServiceExampleBased(t *testing.T) { }, }, "actor-uid") require.NoError(t, err) - require.NoError(t, sut.SecureValueService.SetKeeperAsActive(t.Context(), xkube.Namespace(k2.Namespace), k2.Name)) + require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(k2.Namespace), k2.Name)) // - Read secure value created with inactive keeper readSv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv1.Namespace), sv1.Name) From ae2e5f0df77cd1afe2a04e22ad1f7c29201a450c Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Nov 2025 13:11:44 +0100 Subject: [PATCH 128/378] NPM: Fix e2e-selectors change detection (#114471) fix git cmd --- scripts/publish-npm-packages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/publish-npm-packages.sh b/scripts/publish-npm-packages.sh index eaa2e595824..34dc06fc009 100755 --- a/scripts/publish-npm-packages.sh +++ b/scripts/publish-npm-packages.sh @@ -60,7 +60,7 @@ if (( ${#failed_packages[@]} > 0 )); then fi # Check if any files in packages/grafana-e2e-selectors were changed. If so, add a 'modified' tag to the package -CHANGES_COUNT=$(git diff HEAD~1..HEAD --name-only -- packages/grafana-e2e-selectors | awk 'END{print NR}') +CHANGES_COUNT=$(git show --name-only --format= HEAD -- packages/grafana-e2e-selectors | awk 'END{print NR}') if (( CHANGES_COUNT > 0 )); then # Wait a little bit to allow the package to be published to the registry sleep 5s From e1a2f178e7459986d8c10bb04d5fffc67d5652fc Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 26 Nov 2025 13:41:06 +0100 Subject: [PATCH 129/378] App Plugins: Allow to define experimental pages (#114232) --- pkg/middleware/auth.go | 29 ++++++ pkg/middleware/auth_test.go | 96 ++++++++++++++++++++ pkg/services/navtree/navtreeimpl/applinks.go | 5 + 3 files changed, 130 insertions(+) diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 719d2ab5cb5..f013d9d2bfa 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -1,6 +1,7 @@ package middleware import ( + "context" "errors" "net/http" "net/url" @@ -21,6 +22,13 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" + "github.com/open-feature/go-sdk/openfeature" +) + +var openfeatureClient = openfeature.NewDefaultClient() + +const ( + pluginPageFeatureFlagPrefix = "plugin-page-visible." ) type AuthOptions struct { @@ -146,6 +154,12 @@ func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, log return } + if !PageIsFeatureToggleEnabled(c.Req.Context(), c.Req.URL.Path) { + logger.Debug("Forbidden experimental plugin page", "plugin", pluginID, "path", c.Req.URL.Path) + accessForbidden(c) + return + } + permitted := true path := normalizeIncludePath(c.Req.URL.Path) hasAccess := ac.HasAccess(accessControl, c) @@ -294,3 +308,18 @@ func shouldForceLogin(c *contextmodel.ReqContext) bool { return forceLogin } + +// PageIsFeatureToggleEnabled checks if a page is enabled via OpenFeature feature flags. +// It returns false if the feature flag is set and set to false. +// The feature flag key format is: "plugin-page-visible." +func PageIsFeatureToggleEnabled(ctx context.Context, path string) bool { + flagKey := pluginPageFeatureFlagPrefix + filepath.Clean(path) + enabled := openfeatureClient.Boolean( + ctx, + flagKey, + true, + openfeature.TransactionContext(ctx), + ) + + return enabled +} diff --git a/pkg/middleware/auth_test.go b/pkg/middleware/auth_test.go index fdca1d04ee3..19a7d68559e 100644 --- a/pkg/middleware/auth_test.go +++ b/pkg/middleware/auth_test.go @@ -1,12 +1,17 @@ package middleware import ( + "context" "errors" "fmt" "net/http" "net/http/httptest" + "sync" "testing" + "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/memprovider" + oftesting "github.com/open-feature/go-sdk/openfeature/testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -28,6 +33,8 @@ import ( "github.com/grafana/grafana/pkg/web" ) +var openfeatureTestMutex sync.Mutex + func setupAuthMiddlewareTest(t *testing.T, identity *authn.Identity, authErr error) *contexthandler.ContextHandler { return contexthandler.ProvideService(setting.NewCfg(), &authntest.FakeService{ ExpectedErr: authErr, @@ -422,6 +429,60 @@ func TestCanAdminPlugin(t *testing.T) { } } +func TestPageIsFeatureToggleEnabled(t *testing.T) { + type testCase struct { + desc string + path string + flags map[string]bool + expectedResult bool + } + + tests := []testCase{ + { + desc: "returns true when feature flag is enabled", + path: "/a/my-plugin/settings", + flags: map[string]bool{ + pluginPageFeatureFlagPrefix + "/a/my-plugin/settings": true, + }, + expectedResult: true, + }, + { + desc: "returns false when feature flag is disabled", + path: "/a/my-plugin/settings", + flags: map[string]bool{ + pluginPageFeatureFlagPrefix + "/a/my-plugin/settings": false, + }, + expectedResult: false, + }, + { + desc: "returns false when feature flag is disabled with trailing slash", + path: "/a/my-plugin/settings/", + flags: map[string]bool{ + pluginPageFeatureFlagPrefix + "/a/my-plugin/settings": false, + }, + expectedResult: false, + }, + { + desc: "returns true when feature flag does not exist", + path: "/a/my-plugin/settings", + flags: map[string]bool{}, + expectedResult: true, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + + setupTestProvider(t, tt.flags) + + result := PageIsFeatureToggleEnabled(ctx, tt.path) + + assert.Equal(t, tt.expectedResult, result) + }) + } +} + func contextProvider(modifiers ...func(c *contextmodel.ReqContext)) web.Handler { return func(c *web.Context) { reqCtx := &contextmodel.ReqContext{ @@ -437,3 +498,38 @@ func contextProvider(modifiers ...func(c *contextmodel.ReqContext)) web.Handler c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), reqCtx)) } } + +// setupTestProvider creates a test OpenFeature provider with the given flags. +// Uses a global lock to prevent concurrent provider changes across tests. +func setupTestProvider(t *testing.T, flags map[string]bool) oftesting.TestProvider { + t.Helper() + + // Lock to prevent concurrent provider changes + openfeatureTestMutex.Lock() + + testProvider := oftesting.NewTestProvider() + flagsMap := map[string]memprovider.InMemoryFlag{} + + for key, value := range flags { + flagsMap[key] = memprovider.InMemoryFlag{ + DefaultVariant: "defaultVariant", + Variants: map[string]any{ + "defaultVariant": value, + }, + } + } + + testProvider.UsingFlags(t, flagsMap) + + err := openfeature.SetProviderAndWait(testProvider) + require.NoError(t, err) + + t.Cleanup(func() { + testProvider.Cleanup() + _ = openfeature.SetProviderAndWait(openfeature.NoopProvider{}) + // Unlock after cleanup to allow other tests to run + openfeatureTestMutex.Unlock() + }) + + return testProvider +} diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 4f8015f5687..b9b0edac970 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -128,6 +129,10 @@ func (s *ServiceImpl) processAppPlugin(plugin pluginstore.Plugin, c *contextmode } if include.Type == "page" { + if !middleware.PageIsFeatureToggleEnabled(c.Req.Context(), include.Path) { + s.log.Debug("Skipping page", "plugin", plugin.ID, "path", include.Path) + continue + } link := &navtree.NavLink{ Text: include.Name, Icon: include.Icon, From 5ba3139d4a055ca5f0c54a4e6f743412c92ca4e3 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Nov 2025 13:51:16 +0100 Subject: [PATCH 130/378] E2E Selectors: Fix readme typo (#114480) fix typo --- packages/grafana-e2e-selectors/src/selectors/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-e2e-selectors/src/selectors/README.md b/packages/grafana-e2e-selectors/src/selectors/README.md index ef419dac1fa..134197e6a6a 100644 --- a/packages/grafana-e2e-selectors/src/selectors/README.md +++ b/packages/grafana-e2e-selectors/src/selectors/README.md @@ -16,7 +16,7 @@ const components = { A few things to keep in mind: -- Strive to use e2e selector for all components in grafana/ui. +- Strive to use e2e selectors for all components in grafana/ui. - Don't ever delete selectors. Even though a selector may not be used in the Grafana repository, it can still be used in external plugins. - Only create new selector in case you're creating a new piece of UI. If you're changing an existing piece of UI that already has a selector defined, you need to keep using that selector. Otherwise you might break plugin end-to-end tests. - Prefer using string selectors in favour of function selectors. The purpose of the selectors is to provide a canonical way to select elements. From c94bf34d0bb2ff3be8beee61e5dc9f2e164a9bbb Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 26 Nov 2025 14:27:31 +0100 Subject: [PATCH 131/378] Alerting: Patch missing expression model refIds (#114477) --- .../components/expressions/Expression.tsx | 2 - .../rule-editor/ExpressionsEditor.tsx | 11 +-- .../QueryAndExpressionsStep.tsx | 4 -- .../__snapshots__/reducer.test.tsx.snap | 52 -------------- .../reducer.test.tsx | 17 ----- .../query-and-alert-condition/reducer.ts | 17 ----- .../alerting/unified/utils/rule-form.test.ts | 69 +++++++++++++++++++ .../alerting/unified/utils/rule-form.ts | 59 +++++++++++----- 8 files changed, 115 insertions(+), 116 deletions(-) diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index 79f805d52ad..0e5d5fd4b79 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -48,7 +48,6 @@ interface ExpressionProps { onSetCondition: (refId: string) => void; onUpdateRefId: (oldRefId: string, newRefId: string) => void; onRemoveExpression: (refId: string) => void; - onUpdateExpressionType: (refId: string, type: ExpressionQueryType) => void; onChangeQuery: (query: ExpressionQuery) => void; } @@ -62,7 +61,6 @@ export const Expression: FC = ({ onSetCondition, onUpdateRefId, onRemoveExpression, - onUpdateExpressionType, // this method is not used? maybe we should remove it onChangeQuery, }) => { const styles = useStyles2(getStyles); diff --git a/public/app/features/alerting/unified/components/rule-editor/ExpressionsEditor.tsx b/public/app/features/alerting/unified/components/rule-editor/ExpressionsEditor.tsx index 87952c4b19f..bf1a9985ec2 100644 --- a/public/app/features/alerting/unified/components/rule-editor/ExpressionsEditor.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/ExpressionsEditor.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import { GrafanaTheme2, PanelData } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; import { isExpressionQuery } from 'app/features/expressions/guards'; -import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types'; +import { ExpressionQuery } from 'app/features/expressions/types'; import { AlertQuery } from 'app/types/unified-alerting-dto'; import { Expression } from '../expressions/Expression'; @@ -18,7 +18,6 @@ interface Props { queries: AlertQuery[]; onRemoveExpression: (refId: string) => void; onUpdateRefId: (oldRefId: string, newRefId: string) => void; - onUpdateExpressionType: (refId: string, type: ExpressionQueryType) => void; onUpdateQueryExpression: (query: ExpressionQuery) => void; } @@ -29,12 +28,15 @@ export const ExpressionsEditor = ({ panelData, onUpdateRefId, onRemoveExpression, - onUpdateExpressionType, onUpdateQueryExpression, }: Props) => { const expressionQueries = useMemo(() => { return queries.reduce((acc: ExpressionQuery[], query) => { - return isExpressionQuery(query.model) ? acc.concat(query.model) : acc; + if (isExpressionQuery(query.model)) { + acc.push(query.model); + } + + return acc; }, []); }, [queries]); const styles = useStyles2(getStyles); @@ -64,7 +66,6 @@ export const ExpressionsEditor = ({ onSetCondition={onSetCondition} onRemoveExpression={onRemoveExpression} onUpdateRefId={onUpdateRefId} - onUpdateExpressionType={onUpdateExpressionType} onChangeQuery={onUpdateQueryExpression} /> ); diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index 844443a4dff..d19ac4bb619 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -73,7 +73,6 @@ import { updateExpression, updateExpressionRefId, updateExpressionTimeRange, - updateExpressionType, } from './reducer'; import { useAdvancedMode } from './useAdvancedMode'; import { useAlertQueryRunner } from './useAlertQueryRunner'; @@ -591,9 +590,6 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange, mod dispatch(removeExpression(refId)); }} onUpdateRefId={onUpdateRefId} - onUpdateExpressionType={(refId, type) => { - dispatch(updateExpressionType({ refId, type })); - }} onUpdateQueryExpression={(model) => { dispatch(updateExpression(model)); }} diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/reducer.test.tsx.snap b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/reducer.test.tsx.snap index 292a6904f80..33f3b0ea4e8 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/reducer.test.tsx.snap +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/reducer.test.tsx.snap @@ -442,55 +442,3 @@ exports[`Query and expressions reducer should update an expression refId and rew ], } `; - -exports[`Query and expressions reducer should update expression type 1`] = ` -{ - "queries": [ - { - "datasourceUid": "abc123", - "model": { - "refId": "A", - }, - "queryType": "query", - "refId": "A", - }, - { - "datasourceUid": "__expr__", - "model": { - "conditions": [ - { - "evaluator": { - "params": [ - 0, - 0, - ], - "type": "gt", - }, - "operator": { - "type": "and", - }, - "query": { - "params": [], - }, - "reducer": { - "params": [], - "type": "avg", - }, - "type": "query", - }, - ], - "datasource": { - "name": "Expression", - "type": "__expr__", - "uid": "__expr__", - }, - "expression": "", - "refId": "B", - "type": "reduce", - }, - "queryType": "", - "refId": "B", - }, - ], -} -`; diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.test.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.test.tsx index 31858408509..a4a214b8348 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.test.tsx @@ -22,7 +22,6 @@ import { updateExpression, updateExpressionRefId, updateExpressionTimeRange, - updateExpressionType, } from './reducer'; const reduceExpression: AlertQuery = { @@ -388,22 +387,6 @@ describe('Query and expressions reducer', () => { expect(newState).toMatchSnapshot(); }); - - it('should update expression type', () => { - const initialState: QueriesAndExpressionsState = { - queries: [alertQuery, expressionQuery], - }; - - const newState = queriesAndExpressionsReducer( - initialState, - updateExpressionType({ - refId: 'B', - type: ExpressionQueryType.reduce, - }) - ); - - expect(newState).toMatchSnapshot(); - }); it('should remove first reducer', () => { const initialState: QueriesAndExpressionsState = { queries: [alertQuery, reduceExpression, thresholdExpression], diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts index 372e028bbcc..1514533cce0 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts @@ -283,23 +283,6 @@ export const queriesAndExpressionsReducer = createReducer(initialState, (builder queryType: 'expression', }); } - }) - .addCase(updateExpressionType, (state, action) => { - state.queries = state.queries.map((query) => { - return query.refId === action.payload.refId - ? { - ...query, - model: { - ...expressionDatasource.newQuery({ - type: action.payload.type, - conditions: [{ ...defaultCondition, query: { params: [] } }], - expression: '', - }), - refId: action.payload.refId, - }, - } - : query; - }); }); }); diff --git a/public/app/features/alerting/unified/utils/rule-form.test.ts b/public/app/features/alerting/unified/utils/rule-form.test.ts index 2e9b7a5730b..a5410e6ebd6 100644 --- a/public/app/features/alerting/unified/utils/rule-form.test.ts +++ b/public/app/features/alerting/unified/utils/rule-form.test.ts @@ -7,6 +7,7 @@ import { GrafanaAlertStateDecision, GrafanaRuleDefinition, RulerAlertingRuleDTO, + RulerGrafanaRuleDTO, } from 'app/types/unified-alerting-dto'; import { EvalFunction } from '../../state/alertDef'; @@ -20,6 +21,7 @@ import { alertingRulerRuleToRuleForm, cleanAnnotations, cleanLabels, + fixMissingRefIdsInExpressionModel, formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO, getContactPointsFromDTO, @@ -520,3 +522,70 @@ describe('getDefaultExpressions', () => { expect(thresholdModel.expression).toBe('X'); }); }); + +describe('fixMissingRefIdsInExpressionModel', () => { + it('should return non-Grafana managed rules unchanged', () => { + const cloudAlertingRule: RulerAlertingRuleDTO = { + alert: 'CloudAlert', + expr: 'up == 0', + for: '5m', + labels: { severity: 'critical' }, + annotations: { summary: 'Instance down' }, + }; + + const result = fixMissingRefIdsInExpressionModel(cloudAlertingRule); + + expect(result).toEqual(cloudAlertingRule); + expect(result).toBe(cloudAlertingRule); // should be the exact same reference + }); + + it('should copy refId from query to model when model.refId is missing in Grafana managed rules', () => { + const ruleWithMissingRefId: RulerGrafanaRuleDTO = { + grafana_alert: { + uid: 'test-uid', + title: 'Test Alert', + namespace_uid: 'namespace-uid', + rule_group: 'test-group', + condition: 'B', + no_data_state: GrafanaAlertStateDecision.NoData, + exec_err_state: GrafanaAlertStateDecision.Alerting, + is_paused: false, + data: [ + { + refId: 'A', + datasourceUid: 'datasource-uid', + queryType: '', + relativeTimeRange: { from: 600, to: 0 }, + // @ts-ignore + model: { + // refId is missing here + datasource: { + type: 'grafana-testdata-datasource', + uid: 'PD8C576611E62080A', + }, + }, + }, + { + refId: 'B', + datasourceUid: ExpressionDatasourceUID, + queryType: '', + // @ts-ignore + model: { + // refId is missing here + type: ExpressionQueryType.reduce, + expression: 'A', + }, + }, + ], + }, + for: '5m', + labels: {}, + annotations: {}, + }; + + const result = fixMissingRefIdsInExpressionModel(ruleWithMissingRefId); + + expect(result.grafana_alert.data[0].model.refId).toBe('A'); + expect(result.grafana_alert.data[1].model.refId).toBe('B'); + }); +}); diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 077934e21e1..0e83a276851 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -1,3 +1,5 @@ +import { produce } from 'immer'; + import { DataSourceInstanceSettings, IntervalValues, @@ -278,14 +280,16 @@ function getEditorSettingsFromDTO(ga: GrafanaRuleDefinition) { export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleFormValues { const { ruleSourceName, namespace, group, rule } = ruleWithLocation; - const isGrafanaRecordingRule = rulerRuleType.grafana.recordingRule(rule); + const normalizedRule = fixMissingRefIdsInExpressionModel(rule); + + const isGrafanaRecordingRule = rulerRuleType.grafana.recordingRule(normalizedRule); const defaultFormValues = getDefaultFormValues(isGrafanaRecordingRule ? RuleFormType.grafanaRecording : undefined); if (isGrafanaRulesSource(ruleSourceName)) { // GRAFANA-MANAGED RULES if (isGrafanaRecordingRule) { // grafana recording rule - const ga = rule.grafana_alert; + const ga = normalizedRule.grafana_alert; return { ...defaultFormValues, name: ga.title, @@ -294,16 +298,16 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF evaluateEvery: group.interval || defaultFormValues.evaluateEvery, queries: ga.data, condition: ga.condition, - annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(rule.annotations, false)), - labels: listifyLabelsOrAnnotations(rule.labels, true), + annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(normalizedRule.annotations, false)), + labels: listifyLabelsOrAnnotations(normalizedRule.labels, true), folder: { title: namespace, uid: ga.namespace_uid }, isPaused: ga.is_paused, metric: ga.record?.metric, targetDatasourceUid: ga.record?.target_datasource_uid || defaultFormValues.targetDatasourceUid, }; - } else if (rulerRuleType.grafana.rule(rule)) { + } else if (rulerRuleType.grafana.rule(normalizedRule)) { // grafana alerting rule - const ga = rule.grafana_alert; + const ga = normalizedRule.grafana_alert; const routingSettings: AlertManagerManualRouting | undefined = getContactPointsFromDTO(ga); if (ga.no_data_state !== undefined && ga.exec_err_state !== undefined) { return { @@ -312,14 +316,14 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF type: RuleFormType.grafana, group: group.name, evaluateEvery: group.interval || defaultFormValues.evaluateEvery, - evaluateFor: rule.for || '0', - keepFiringFor: rule.keep_firing_for || '0', + evaluateFor: normalizedRule.for || '0', + keepFiringFor: normalizedRule.keep_firing_for || '0', noDataState: ga.no_data_state, execErrState: ga.exec_err_state, queries: ga.data, condition: ga.condition, - annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(rule.annotations, false)), - labels: listifyLabelsOrAnnotations(rule.labels, true), + annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(normalizedRule.annotations, false)), + labels: listifyLabelsOrAnnotations(normalizedRule.labels, true), folder: { title: namespace, uid: ga.namespace_uid }, isPaused: ga.is_paused, @@ -338,7 +342,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF } } else { // DATASOURCE-MANAGED RULES - if (rulerRuleType.dataSource.alertingRule(rule)) { + if (rulerRuleType.dataSource.alertingRule(normalizedRule)) { const datasourceUid = getDataSourceSrv().getInstanceSettings(ruleSourceName)?.uid ?? ''; const defaultQuery = { @@ -346,27 +350,27 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF datasourceUid, queryType: '', relativeTimeRange: getDefaultRelativeTimeRange(), - expr: rule.expr, + expr: normalizedRule.expr, model: { refId: 'A', hide: false, - expr: rule.expr, + expr: normalizedRule.expr, }, }; - const alertingRuleValues = alertingRulerRuleToRuleForm(rule); + const alertingRuleValues = alertingRulerRuleToRuleForm(normalizedRule); return { ...defaultFormValues, ...alertingRuleValues, queries: [defaultQuery], - annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(rule.annotations, false)), + annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(normalizedRule.annotations, false)), type: RuleFormType.cloudAlerting, dataSourceName: ruleSourceName, namespace, group: group.name, }; - } else if (rulerRuleType.dataSource.recordingRule(rule)) { + } else if (rulerRuleType.dataSource.recordingRule(normalizedRule)) { const datasourceUid = getDataSourceSrv().getInstanceSettings(ruleSourceName)?.uid ?? ''; const defaultQuery = { @@ -374,15 +378,15 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF datasourceUid, queryType: '', relativeTimeRange: getDefaultRelativeTimeRange(), - expr: rule.expr, + expr: normalizedRule.expr, model: { refId: 'A', hide: false, - expr: rule.expr, + expr: normalizedRule.expr, }, }; - const recordingRuleValues = recordingRulerRuleToRuleForm(rule); + const recordingRuleValues = recordingRulerRuleToRuleForm(normalizedRule); return { ...defaultFormValues, @@ -399,6 +403,23 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF } } +/** + * This function isn't supposed to be needed, but we've noticed some customers are creating rules via Provisioning or + * other interfaces where they aren't including the RefId in the "model" of the expression so copy the refId from the query definition. + */ +export function fixMissingRefIdsInExpressionModel(rule: T): T { + // non-Grafana managed rules don't use expression nodes so we return the rule as-is + if (!rulerRuleType.grafana.rule(rule)) { + return rule; + } + + return produce(rule, (draft) => { + draft.grafana_alert.data.forEach((query) => { + query.model.refId = query.model.refId ?? query.refId; + }); + }); +} + export function grafanaRuleDtoToFormValues(rule: RulerGrafanaRuleDTO, namespace: string): RuleFormValues { const isGrafanaRecordingRule = rulerRuleType.grafana.recordingRule(rule); const defaultFormValues = getDefaultFormValues(isGrafanaRecordingRule ? RuleFormType.grafanaRecording : undefined); From f0a394e67b02eaade338bdf50ae7b272d63b01e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 26 Nov 2025 14:30:18 +0100 Subject: [PATCH 132/378] Dashboards: Use new sidebar in dynamic dashboards (#114245) * Dynamic dashboards sidebar wip * Progress * Outline in view mode cannot change name * Only one pane at a time * Adding starbutton and custom precence * undo / redo working * Progress * Update * Progress * Update * Public badge, provisining badge * playlist stop * Update * some initial unit tests * Fix export tooltip * Close pane when leaving edit mode if pane is an element * Update * e2e fixes * fixing e2e * Fix lint suppressions * e2e fixes * fixing more e2e * fixing e2e * fix e2e * e2e fixes * Fixinfg e2e * fixing e2e --- .../dashboard-group-panels.spec.ts | 24 +- .../dashboard-outline.spec.ts | 3 + .../dashboards-add-panel.spec.ts | 3 + .../dashboards-edit-custom-variables.spec.ts | 1 + .../dashboards-panel-layouts.spec.ts | 37 ++- .../dashboards-repeats-auto-grid.spec.ts | 36 +- .../dashboards-repeats-custom-grid.spec.ts | 4 +- .../dashboards-repeats-tabs-layout.spec.ts | 8 +- .../dashboards-title-description.spec.ts | 6 +- e2e-playwright/dashboard-new-layouts/utils.ts | 1 + eslint-suppressions.json | 5 - .../src/selectors/components.ts | 8 +- .../src/selectors/pages.ts | 8 + .../src/components/Sidebar/Sidebar.tsx | 3 +- .../src/components/Sidebar/SidebarButton.tsx | 60 ++-- .../components/Sidebar/SidebarPaneHeader.tsx | 2 + .../src/components/Sidebar/useSidebar.tsx | 6 +- .../edit-pane/DashboardEditPane.tsx | 51 ++- .../DashboardEditPaneRenderer.test.tsx | 69 ++-- .../edit-pane/DashboardEditPaneRenderer.tsx | 307 ++++++++---------- .../edit-pane/DashboardEditPaneSplitter.tsx | 178 +++++----- .../edit-pane/DashboardEditableElement.tsx | 30 +- .../edit-pane/DashboardExportButton.tsx | 57 ++++ .../edit-pane/DashboardOutline.test.tsx | 2 +- .../edit-pane/DashboardOutline.tsx | 25 +- .../edit-pane/EditPaneHeader.tsx | 38 +-- .../dashboard-scene/edit-pane/shared.ts | 5 - .../edit-pane/useEditableElement.ts | 21 -- .../edit-pane/useOutlineRename.tsx | 6 +- .../scene/DashboardControls.tsx | 42 ++- .../dashboard-scene/scene/DashboardScene.tsx | 10 +- .../scene/GoToSnapshotOriginButton.tsx | 2 +- .../scene/NavToolbarActions.tsx | 4 +- .../scene/new-toolbar/RightActions.tsx | 96 +----- .../actions/ExportDashboardButton.tsx | 39 --- .../actions/OpenSnapshotOriginButton.tsx | 2 +- .../new-toolbar/actions/SaveDashboard.tsx | 12 +- .../actions/ShareExportDashboardButton.tsx | 4 +- .../new-toolbar/actions/ToolbarSwitch.tsx | 14 +- .../variables/VariableEditableElement.tsx | 8 +- public/locales/en-US/grafana.json | 47 ++- 41 files changed, 634 insertions(+), 650 deletions(-) create mode 100644 public/app/features/dashboard-scene/edit-pane/DashboardExportButton.tsx delete mode 100644 public/app/features/dashboard-scene/edit-pane/useEditableElement.ts delete mode 100644 public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts index ffdd52e24f6..f077f48a7e0 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts @@ -414,13 +414,13 @@ test.describe( ).toBeVisible(); // Go back to dashboard options - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true }); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); // Expand layouts section await page.getByLabel('Expand Group layout category').click(); // Select tabs layout - await page.getByLabel('Tabs').click(); + await page.getByLabel('layout-selection-option-Tabs').click(); await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible(); await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible(); @@ -518,14 +518,14 @@ test.describe( await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput) .fill('Test row 1'); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // clear the title input to simulate no title and click away to trigger onBlur await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('Test row 1')).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput) .fill(''); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // title should be set to a default name await expect( @@ -543,14 +543,14 @@ test.describe( await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput) .fill('Test row 2'); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // clear the title input to simulate no title and click away to trigger onBlur await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('Test row 2')).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput) .fill(''); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // title should be set to a default name + 1 to avoid duplicates await expect( @@ -755,13 +755,13 @@ test.describe( await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible(); // Go back to dashboard options - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true }); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); // Expand layouts section await page.getByLabel('Expand Group layout category').click(); // Select rows layout - await page.getByLabel('Rows').click(); + await page.getByLabel('layout-selection-option-Rows').click(); await dashboardPage .getByGrafanaSelector(selectors.components.DashboardRow.wrapper('New tab 1')) @@ -903,14 +903,14 @@ test.describe( await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput) .fill('Test tab 1'); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // clear the title input to simulate no title and click away to trigger onBlur await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Test tab 1')).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput) .fill(''); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // title should be set to a default name await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); @@ -923,14 +923,14 @@ test.describe( await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput) .fill('Test tab 2'); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // clear the title input to simulate no title and click away to trigger onBlur await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Test tab 2')).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput) .fill(''); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // title should be set to a default name + 1 to avoid duplicates await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts index ef38379f006..00a84a188a5 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts @@ -21,6 +21,7 @@ test.describe( const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click(); // Should be able to click Variables item in outline to see add variable button await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); @@ -28,6 +29,8 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) ).toBeVisible(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click(); + // Clicking a panel should scroll that panel in view await expect(page.getByText('Dashboard panel 48')).toBeHidden(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Panel #48')).click(); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts index ffea8243a2f..c2af96349cf 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts @@ -22,6 +22,9 @@ test.describe( const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + const undockButton = page.getByRole('button', { name: 'Undock menu' }); + await undockButton.click(); + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await page.evaluate(() => { diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts index 24c61e5e960..4715dfc7128 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts @@ -199,6 +199,7 @@ test.describe( .click(); // Open the modal editor in the side pane + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.node('Variables')).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('foo')).click(); await openModal(dashboardPage, selectors); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts index 087551a55cd..66d634eefa7 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts @@ -32,9 +32,9 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); await expect( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) @@ -50,6 +50,7 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await checkAutoGridLayoutInputs(dashboardPage, selectors); }); @@ -63,9 +64,10 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); // Get initial positions - standard width should have panels on different rows const firstPanelTop = await getPanelTop(dashboardPage, selectors); @@ -98,6 +100,7 @@ test.describe( await page.reload(); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await expect( dashboardPage.getByGrafanaSelector( @@ -123,9 +126,10 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth) @@ -134,7 +138,7 @@ test.describe( await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth) - .fill('900'); + .fill('1100'); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth) .blur(); @@ -148,12 +152,13 @@ test.describe( await verifyPanelsStackedVertically(dashboardPage, selectors); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await expect( dashboardPage.getByGrafanaSelector( selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth ) - ).toHaveValue('900'); + ).toHaveValue('1100'); await verifyPanelsStackedVertically(dashboardPage, selectors); @@ -180,9 +185,9 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns) @@ -198,6 +203,7 @@ test.describe( await verifyPanelsStackedVertically(dashboardPage, selectors); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await expect( dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns) @@ -215,9 +221,9 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); const regularRowHeight = await getPanelHeight(dashboardPage, selectors); @@ -250,6 +256,7 @@ test.describe( }).toPass(); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await expect( dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) @@ -270,9 +277,9 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); const regularRowHeight = await getPanelHeight(dashboardPage, selectors); @@ -303,6 +310,7 @@ test.describe( }).toPass(); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await expect( dashboardPage.getByGrafanaSelector( @@ -327,9 +335,9 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); // Set narrow column width first to ensure panels fit horizontally await dashboardPage @@ -357,6 +365,7 @@ test.describe( }).toPass(); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await expect( dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen) diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts index 28ca51e3b28..7cf22108e1e 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts @@ -37,9 +37,10 @@ test.describe( }, () => { test('can enable repeats', async ({ dashboardPage, selectors, page }) => { - await importTestDashboard(page, selectors, 'Auto grid repeats - add repeats'); + await importTestDashboard(page, selectors, 'Auto-grid repeats - add repeats'); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await switchToAutoGrid(page); @@ -70,11 +71,12 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - update on variable change', + 'Auto-grid repeats - update on variable change', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await switchToAutoGrid(page); await saveDashboard(dashboardPage, page, selectors); @@ -113,6 +115,7 @@ test.describe( ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await switchToAutoGrid(page); @@ -138,11 +141,13 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - update through panel editor', + 'Auto-grid repeats - update through panel editor', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); + await switchToAutoGrid(page); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -202,11 +207,13 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - update through directly loaded panel editor', + 'Auto-grid repeats - update through directly loaded panel editor', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); + await switchToAutoGrid(page); await saveDashboard(dashboardPage, page, selectors); @@ -257,11 +264,12 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - move repeated panels', + 'Auto-grid repeats - move repeated panels', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await switchToAutoGrid(page); @@ -304,11 +312,13 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - move repeated panels', + 'Auto-grid repeats - move repeated panels 2', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); + await switchToAutoGrid(page); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -332,9 +342,7 @@ test.describe( const repeatedPanelUrl = page.url(); - await dashboardPage - .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) - .click(); + await page.keyboard.press('Escape'); await dashboardPage .getByGrafanaSelector(selectors.components.Panels.Panel.title(`${repeatTitleBase}${repeatOptions.at(0)}`)) @@ -367,11 +375,13 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - view embedded repeated panel', + 'Auto-grid repeats - view embedded repeated panel', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); + await switchToAutoGrid(page); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -393,11 +403,13 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - remove repeats', + 'Auto-grid repeats - remove repeats', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); + await switchToAutoGrid(page); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -453,5 +465,5 @@ test.describe( async function switchToAutoGrid(page: Page) { await page.getByLabel('Expand Panel layout category').click(); - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); } diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts index 2d235571f97..8cc1f552377 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts @@ -303,9 +303,7 @@ test.describe( const repeatedPanelUrl = page.url(); - await dashboardPage - .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) - .click(); + await page.keyboard.press('Escape'); await dashboardPage .getByGrafanaSelector(selectors.components.Panels.Panel.title(`${repeatTitleBase}${repeatOptions.at(0)}`)) diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts index 100ec864b0d..f78bbc5e08e 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts @@ -316,9 +316,7 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toBeVisible(); - await dashboardPage - .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) - .click(); + await page.keyboard.press('Escape'); // repeated panel in original tab repeat await dashboardPage @@ -341,9 +339,7 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2')) ).toBeVisible(); - await dashboardPage - .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) - .click(); + await page.keyboard.press('Escape'); // repeated panel in repeated tab await dashboardPage diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts index 67dc17f648b..d9a82364a70 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts @@ -21,11 +21,7 @@ test.describe( const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); - - // Check that current dashboard title is visible in breadcrumb - await expect( - dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb('Annotation filtering')) - ).toBeVisible(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); const titleInput = page.locator('[aria-label="dashboard-options Title field property editor"] input'); await expect(titleInput).toHaveValue('Annotation filtering'); diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index da629ac8141..c1e00e1a67b 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -48,6 +48,7 @@ export const flows = { }, async newEditPaneVariableClick(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 2450d3721d7..fde5e53587d 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1907,11 +1907,6 @@ "count": 2 } }, - "public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx": { "react-hooks/rules-of-hooks": { "count": 4 diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index e56318477c0..98fa6053d11 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -57,6 +57,11 @@ export const versionedComponents = { '12.1.0': 'data-testid DashboardEditPaneSplitter primary body', }, }, + Sidebar: { + closePane: { + '12.4.0': 'data-testid Sidebar close pane', + }, + }, EditPaneHeader: { deleteButton: { '12.1.0': 'data-testid EditPaneHeader delete panel', @@ -70,9 +75,6 @@ export const versionedComponents = { duplicate: { '12.1.0': 'data-testid EditPaneHeader duplicate', }, - backButton: { - '12.1.0': 'data-testid EditPaneHeader back', - }, }, TimePicker: { openButton: { diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 96d033d5b14..79422ee8db9 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -183,6 +183,14 @@ export const versionedPages = { url: { [MIN_GRAFANA_VERSION]: (uid: string) => `/d/${uid}`, }, + Sidebar: { + optionsButton: { + '12.4.0': 'data-testid Dashboard Sidebar options button', + }, + outlineButton: { + '12.4.0': 'data-testid Dashboard Sidebar outline button', + }, + }, DashNav: { nav: { [MIN_GRAFANA_VERSION]: 'Dashboard navigation', diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx index 2d9b407a1ad..8c1bf784e53 100644 --- a/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx @@ -59,8 +59,9 @@ export function SiderbarToolbar({ children }: SiderbarToolbarProps) { {context.hasOpenPane && ( )}
diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx index f436d9630f6..9f5e0f20d71 100644 --- a/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx +++ b/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { useContext } from 'react'; +import React, { ButtonHTMLAttributes, useContext } from 'react'; import { GrafanaTheme2, IconName, isIconName } from '@grafana/data'; @@ -11,38 +11,48 @@ import { Tooltip } from '../Tooltip/Tooltip'; import { SidebarContext } from './useSidebar'; -export interface Props { +export interface Props extends ButtonHTMLAttributes { icon: IconName; active?: boolean; - onClick?: () => void; - title: string; tooltip?: string; + title: string; } -export function SidebarButton({ icon, active, onClick, title, tooltip }: Props) { - const styles = useStyles2(getStyles); - const context = useContext(SidebarContext); +export const SidebarButton = React.forwardRef( + ({ icon, active, onClick, title, tooltip, ...restProps }, ref) => { + const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); - if (!context) { - throw new Error('Sidebar.Button must be used within a Sidebar component'); + if (!context) { + throw new Error('Sidebar.Button must be used within a Sidebar component'); + } + + const buttonClass = cx( + styles.button, + context.compact && styles.compact, + active && styles.active, + context.position === 'left' && styles.leftButton + ); + + return ( + + + + ); } +); - const buttonClass = cx( - styles.button, - context.compact && styles.compact, - active && styles.active, - context.position === 'left' && styles.leftButton - ); - - return ( - - - - ); -} +SidebarButton.displayName = 'SidebarButton'; function renderIcon(icon: IconName | React.ReactNode, compact?: boolean) { if (!icon) { diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx index 75393777879..42fe30b083c 100644 --- a/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx +++ b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import { ReactNode } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { useStyles2 } from '../../themes/ThemeContext'; @@ -27,6 +28,7 @@ export function SidebarPaneHeader({ children, onClose, title }: Props) { onClick={onClose} aria-label={t('grafana-ui.sidebar.close', 'Close')} tooltip={t('grafana-ui.sidebar.close', 'Close')} + data-testid={selectors.components.Sidebar.closePane} /> )} diff --git a/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx b/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx index 03d04b2f81a..1d51b3fdd79 100644 --- a/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx +++ b/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx @@ -16,7 +16,7 @@ export interface SidebarContextValue { bottomMargin: number; edgeMargin: number; contentMargin: number; - onDockChange: () => void; + onToggleDock: () => void; onResize: (diff: number) => void; } @@ -56,7 +56,7 @@ export function useSidebar({ // Used to accumulate drag distance to know when to change compact mode const [_, setCompactDrag] = React.useState(0); - const onDockChange = useCallback(() => setIsDocked((prev) => !prev), []); + const onToggleDock = useCallback(() => setIsDocked((prev) => !prev), []); const prop = position === 'right' ? 'paddingRight' : 'paddingLeft'; const toolbarWidth = @@ -98,7 +98,7 @@ export function useSidebar({ return { isDocked, - onDockChange, + onToggleDock, onResize, outerWrapperProps, position, diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index 5a1b3624a2b..64f0c774ea8 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -26,8 +26,12 @@ export interface DashboardEditPaneState extends SceneObjectState { undoStack: DashboardEditActionEventPayload[]; redoStack: DashboardEditActionEventPayload[]; + openPane?: DashboardSidebarPaneName; + isDocked?: boolean; } +export type DashboardSidebarPaneName = 'element' | 'outline' | 'filters'; + export class DashboardEditPane extends SceneObjectBase { public constructor() { super({ @@ -192,6 +196,7 @@ export class DashboardEditPane extends SceneObjectBase { this.setState({ selectionContext: { ...this.state.selectionContext, selected: [], enabled: false }, selection: undefined, + openPane: this.state.openPane === 'element' ? undefined : this.state.openPane, }); } @@ -227,7 +232,6 @@ export class DashboardEditPane extends SceneObjectBase { } const elementSelection = this.state.selection ?? new ElementSelection([[id, obj.getRef()]]); - const { selection, contextItems: selected } = elementSelection.getStateWithValue(id, obj, !!multi); this.updateSelection(new ElementSelection(selection), selected); @@ -255,17 +259,58 @@ export class DashboardEditPane extends SceneObjectBase { document.activeElement.blur(); } - this.setState({ selection, selectionContext: { ...this.state.selectionContext, selected } }); + this.setState({ + selection, + selectionContext: { ...this.state.selectionContext, selected }, + openPane: selection ? 'element' : undefined, + }); } - public clearSelection() { + /** + * @param force If force = true it will clear selection even when docked + * @returns + */ + public clearSelection(force = false) { if (!this.state.selection) { return; } + // If we are docked then clearing selection should select dashboard itself + // Unless the user explicitly closes pane + if (this.state.isDocked && !force) { + const obj = this.state.selection?.getFirstObject(); + const dashboard = getDashboardSceneFor(this); + if (obj !== dashboard) { + this.selectObject(dashboard, dashboard.state.key!); + } + return; + } + this.updateSelection(undefined, []); } + public openPane(openPane: DashboardSidebarPaneName) { + if (this.state.selection) { + this.clearSelection(true); + } + + if (openPane === this.state.openPane) { + this.setState({ openPane: undefined }); + } else { + this.setState({ openPane }); + } + } + + public closePane() { + if (this.state.selection) { + this.clearSelection(true); + } + + if (this.state.openPane) { + this.setState({ openPane: undefined }); + } + } + private newObjectAddedToCanvas(obj: SceneObject) { this.selectObject(obj, obj.state.key!); this.state.selection?.markAsNewElement(); diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx index 78192a0c8fe..41c81222661 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx @@ -1,18 +1,17 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { act, screen } from '@testing-library/react'; +import { render } from 'test/test-utils'; import { getPanelPlugin } from '@grafana/data/test'; import { selectors } from '@grafana/e2e-selectors'; -import { setPluginImportUtils } from '@grafana/runtime'; +import { setPluginImportUtils, config } from '@grafana/runtime'; import { SceneGridLayout, SceneTimeRange, SceneVariableSet, VizPanel } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; -import { DashboardInteractions } from '../utils/interactions'; import { activateFullSceneTree } from '../utils/test-utils'; -import { DashboardEditPaneRenderer } from './DashboardEditPaneRenderer'; +import { DashboardEditPaneSplitter } from './DashboardEditPaneSplitter'; setPluginImportUtils({ importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), @@ -26,14 +25,9 @@ jest.mock('../utils/interactions', () => ({ }, })); -jest.mock('react-router-dom-v5-compat', () => ({ - ...jest.requireActual('react-router-dom-v5-compat'), - useLocation: () => ({ - pathname: '/dashboard/test', - search: '', - hash: '', - state: null, - }), +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + useChromeHeaderHeight: jest.fn().mockReturnValue(80), })); export function buildTestScene() { @@ -47,26 +41,43 @@ export function buildTestScene() { }), }), }); - activateFullSceneTree(testScene); return testScene; } describe('DashboardEditPaneRenderer', () => { - describe('outline interactions tracking', () => { - it('should call DashboardInteractions.outlineClicked when clicking on dashboard outline', async () => { - const user = userEvent.setup(); - const scene = buildTestScene(); - render( - {}} - /> - ); - const outlineButton = screen.getByTestId(selectors.components.PanelEditor.Outline.section); - await user.click(outlineButton); + config.featureToggles.dashboardNewLayouts = true; - expect(DashboardInteractions.dashboardOutlineClicked).toHaveBeenCalled(); - }); + it('Should render sidebar', async () => { + const scene = buildTestScene(); + + act(() => activateFullSceneTree(scene)); + + render(); + + expect(await screen.findByTestId(selectors.pages.Dashboard.Sidebar.outlineButton)).toBeInTheDocument(); }); + + it('Should sync sidebar docked state with edit pane state', async () => { + const scene = buildTestScene(); + render(); + + act(() => screen.getByLabelText('Outline').click()); + + expect(await screen.findByTestId('sidebar-dock-toggle')).toBeInTheDocument(); + + act(() => screen.getByTestId('sidebar-dock-toggle').click()); + + expect(scene.state.editPane.state.isDocked).toBe(true); + }); + + // describe('outline interactions tracking', () => { + // it('should call DashboardInteractions.outlineClicked when clicking on dashboard outline', async () => { + // const user = userEvent.setup(); + // const scene = buildTestScene(); + // render(); + // const outlineButton = screen.getByTestId(selectors.components.PanelEditor.Outline.section); + // await user.click(outlineButton); + // expect(DashboardInteractions.dashboardOutlineClicked).toHaveBeenCalled(); + // }); + // }); }); diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx index 9ca7fbab86e..80ec361476a 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx @@ -1,211 +1,160 @@ -import { css, cx } from '@emotion/css'; -import { Resizable } from 're-resizable'; -import { useLocalStorage } from 'react-use'; +import { useMemo } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Trans, t } from '@grafana/i18n'; +import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { useSceneObjectState } from '@grafana/scenes'; -import { useStyles2, useSplitter, ToolbarButton, ScrollContainer, Text, Icon, clearButtonStyles } from '@grafana/ui'; +import { Sidebar } from '@grafana/ui'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { DashboardInteractions } from '../utils/interactions'; +import { DashboardScene } from '../scene/DashboardScene'; +import { onOpenSnapshotOriginalDashboard } from '../scene/GoToSnapshotOriginButton'; +import { ManagedDashboardNavBarBadge } from '../scene/ManagedDashboardNavBarBadge'; +import { ToolbarActionProps } from '../scene/new-toolbar/types'; +import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction'; import { DashboardEditPane } from './DashboardEditPane'; +import { ShareExportDashboardButton } from './DashboardExportButton'; import { DashboardOutline } from './DashboardOutline'; import { ElementEditPane } from './ElementEditPane'; -import { useEditableElement } from './useEditableElement'; export interface Props { editPane: DashboardEditPane; - isEditPaneCollapsed: boolean; - openOverlay?: boolean; - onToggleCollapse: () => void; + dashboard: DashboardScene; + isDocked?: boolean; } /** * Making the EditPane rendering completely standalone (not using editPane.Component) in order to pass custom react props */ -export function DashboardEditPaneRenderer({ editPane, isEditPaneCollapsed, onToggleCollapse, openOverlay }: Props) { - const { selection } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); - const styles = useStyles2(getStyles); - const clearButton = useStyles2(clearButtonStyles); - const editableElement = useEditableElement(selection, editPane); +export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Props) { + const { selection, openPane } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); + const { isEditing, meta, uid } = dashboard.useState(); + const hasUid = Boolean(uid); const selectedObject = selection?.getFirstObject(); - const isNewElement = selection?.isNewElement() ?? false; - const [outlineCollapsed, setOutlineCollapsed] = useLocalStorage( - 'grafana.dashboard.edit-pane.outline.collapsed', - false - ); - const [outlinePaneSize = 0.4, setOutlinePaneSize] = useLocalStorage('grafana.dashboard.edit-pane.outline.size', 0.4); - // splitter for template and payload editor - const splitter = useSplitter({ - direction: 'column', - handleSize: 'sm', - // if Grafana Alertmanager, split 50/50, otherwise 100/0 because there is no payload editor - initialSize: 1 - outlinePaneSize, - dragPosition: 'middle', - onSizeChanged: (size) => { - setOutlinePaneSize(1 - size); - }, - }); + const editableElement = useMemo(() => { + if (selection) { + return selection.createSelectionElement(); + } - if (!editableElement) { - return null; - } - - if (isEditPaneCollapsed) { - return ( - <> -
- -
- - {openOverlay && ( - - - - )} - - ); - } - - if (outlineCollapsed) { - splitter.primaryProps.style.flexGrow = 1; - splitter.primaryProps.style.minHeight = 'unset'; - splitter.secondaryProps.style.flexGrow = 0; - splitter.secondaryProps.style.minHeight = 'min-content'; - } else { - splitter.primaryProps.style.minHeight = 'unset'; - splitter.secondaryProps.style.minHeight = 'unset'; - } + return undefined; + }, [selection]); return ( -
-
-
+ <> + {editableElement && ( + -
-
-
- - {!outlineCollapsed && ( -
- - - -
- )} -
-
-
+ + )} + {openPane === 'outline' && ( + + + + )} + + {isEditing && ( + <> + {config.featureToggles.dashboardUndoRedo && ( + <> + + + + )} + editPane.selectObject(dashboard, dashboard.state.key!)} + title={t('dashboard.sidebar.dashboard-options.title', 'Options')} + tooltip={t('dashboard.sidebar.dashboard-options.tooltip', 'Dashboard options')} + data-testid={selectors.pages.Dashboard.Sidebar.optionsButton} + active={selectedObject === dashboard ? true : false} + /> + dashboard.openV2SchemaEditor()} + /> + + + )} + {hasUid && } + editPane.openPane('outline')} + title={t('dashboard.sidebar.outline.title', 'Outline')} + tooltip={t('dashboard.sidebar.outline.tooltip', 'Content outline')} + data-testid={selectors.pages.Dashboard.Sidebar.outlineButton} + active={openPane === 'outline'} + > + {dashboard.isManaged() && Boolean(meta.canEdit) && } + {renderEnterpriseItems()} + {Boolean(meta.isSnapshot) && ( + onOpenSnapshotOriginalDashboard(dashboard.getSnapshotUrl())} + /> + )} + + ); } -function getStyles(theme: GrafanaTheme2) { - return { - wrapper: css({ - display: 'flex', - flexDirection: 'column', - flex: '1 1 0', - marginTop: theme.spacing(2), - borderLeft: `1px solid ${theme.colors.border.weak}`, - borderTop: `1px solid ${theme.colors.border.weak}`, - background: theme.colors.background.primary, - borderTopLeftRadius: theme.shape.radius.default, - }), - overlayWrapper: css({ - right: 0, - bottom: 0, - top: theme.spacing(2), - position: 'absolute !important' as 'absolute', - background: theme.colors.background.primary, - borderLeft: `1px solid ${theme.colors.border.weak}`, - borderTop: `1px solid ${theme.colors.border.weak}`, - boxShadow: theme.shadows.z3, - zIndex: theme.zIndex.navbarFixed, - flexGrow: 1, - }), - paneContent: css({ - overflow: 'hidden', - display: 'flex', - flexDirection: 'column', - }), - rotate180: css({ - rotate: '180deg', - }), - tabsbar: css({ - padding: theme.spacing(0, 1), - margin: theme.spacing(0.5, 0), - }), - expandOptionsWrapper: css({ - display: 'flex', - flexDirection: 'column', - padding: theme.spacing(2, 1, 2, 0), - }), - splitter: css({ - '&::after': { - background: 'transparent', - transform: 'unset', - width: '100%', - height: '1px', - top: '100%', - left: '0', - }, - }), - outlineCollapseButton: css({ - display: 'flex', - padding: theme.spacing(0.5, 2), - gap: theme.spacing(1), - justifyContent: 'space-between', - alignItems: 'center', - background: theme.colors.background.secondary, +function renderEnterpriseItems() { + const dashboard = getDashboardSrv().getCurrent()!; + const showProps = { dashboard }; - '&:hover': { - background: theme.colors.action.hover, - }, - }), - outlineContainer: css({ - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - overflow: 'hidden', - }), - }; + return dynamicDashNavActions.right.map((action, index) => { + if (action.show(showProps)) { + const ActionComponent = action.component; + return ; + } + return null; + }); +} + +function UndoButton({ dashboard }: ToolbarActionProps) { + const editPane = dashboard.state.editPane; + const { undoStack } = editPane.useState(); + const undoAction = undoStack[undoStack.length - 1]; + const undoWord = t('dashboard.sidebar.undo', 'Undo'); + const tooltip = `${undoWord}${undoAction?.description ? ` ${undoAction.description}` : ''}`; + + return ( + editPane.undoAction()} + title={undoWord} + tooltip={tooltip} + /> + ); +} + +function RedoButton({ dashboard }: ToolbarActionProps) { + const editPane = dashboard.state.editPane; + const { redoStack } = editPane.useState(); + const redoAction = redoStack[redoStack.length - 1]; + const redoWord = t('dashboard.sidebar.redo', 'Redo'); + const tooltip = `${redoWord}${redoAction?.description ? ` ${redoAction.description}` : ''}`; + + return ( + editPane.redoAction()} + /> + ); } diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx index 9ede14fe11d..ce7df7a460b 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx @@ -1,19 +1,22 @@ import { css, cx } from '@emotion/css'; -import React, { CSSProperties, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config, useChromeHeaderHeight } from '@grafana/runtime'; import { useSceneObjectState } from '@grafana/scenes'; -import { ElementSelectionContext, useStyles2 } from '@grafana/ui'; +import { ElementSelectionContext, useSidebar, useStyles2, Sidebar } from '@grafana/ui'; +import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import NativeScrollbar, { DivScrollElement } from 'app/core/components/NativeScrollbar'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { useSnappingSplitter } from '../panel-edit/splitter/useSnappingSplitter'; import { DashboardScene } from '../scene/DashboardScene'; import { NavToolbarActions } from '../scene/NavToolbarActions'; +import { PublicDashboardBadge } from '../scene/new-toolbar/actions/PublicDashboardBadge'; +import { StarButton } from '../scene/new-toolbar/actions/StarButton'; +import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction'; import { DashboardEditPaneRenderer } from './DashboardEditPaneRenderer'; -import { useEditPaneCollapsed } from './shared'; interface Props { dashboard: DashboardScene; @@ -26,7 +29,10 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls const headerHeight = useChromeHeaderHeight(); const { editPane } = dashboard.state; const styles = useStyles2(getStyles, headerHeight ?? 0); - const [isCollapsed, setIsCollapsed] = useEditPaneCollapsed(); + const hasUid = Boolean(dashboard.state.uid); + const canStar = Boolean(dashboard.state.meta.canStar); + + //const [isCollapsed, setIsCollapsed] = useEditPaneCollapsed(); if (!config.featureToggles.dashboardNewLayouts) { return ( @@ -40,21 +46,6 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls ); } - const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } = - useSnappingSplitter({ - direction: 'row', - dragPosition: 'end', - initialSize: 330, - handleSize: 'sm', - usePixels: true, - collapseBelowPixels: 250, - collapsed: isCollapsed, - }); - - useEffect(() => { - setIsCollapsed(splitterState.collapsed); - }, [splitterState.collapsed, setIsCollapsed]); - /** * Enable / disable selection based on dashboard isEditing state */ @@ -66,15 +57,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls } }, [isEditing, editPane]); - const { selectionContext } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); - const containerStyle: CSSProperties = {}; - - if (!isEditing) { - primaryProps.style.flexGrow = 1; - primaryProps.style.width = '100%'; - primaryProps.style.minWidth = 'unset'; - containerStyle.overflow = 'unset'; - } + const { selectionContext, openPane } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); const onBodyRef = (ref: HTMLDivElement | null) => { if (ref) { @@ -82,54 +65,73 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls } }; - return ( -
- -
{ - if (evt.shiftKey) { - return; - } + const sidebarContext = useSidebar({ + hasOpenPane: Boolean(openPane), + contentMargin: 1, + position: 'right', + }); - editPane.clearSelection(); - }} - > - -
{controls}
-
-
- {body} -
+ /** + * Sync docked state to editPane state + */ + useEffect(() => { + editPane.setState({ isDocked: sidebarContext.isDocked }); + }, [sidebarContext.isDocked, editPane]); + + const onClearSelection: React.PointerEventHandler = (evt) => { + if (evt.shiftKey) { + return; + } + + editPane.clearSelection(); + }; + + return ( +
+ + + {hasUid && canStar && } + {hasUid && canStar && } + {renderDynamicNavActions()} + + } + /> +
+ {controls} +
+
+
+ {body}
+ + +
- {isEditing && ( - <> -
-
- 0} - /> -
- - )}
); } +function renderDynamicNavActions() { + const dashboard = getDashboardSrv().getCurrent()!; + const showProps = { dashboard }; + + return dynamicDashNavActions.left.map((action, index) => { + if (action.show(showProps)) { + const ActionComponent = action.component; + return ; + } + return null; + }); +} + function getStyles(theme: GrafanaTheme2, headerHeight: number) { return { canvasWrappperOld: css({ @@ -138,22 +140,33 @@ function getStyles(theme: GrafanaTheme2, headerHeight: number) { flexDirection: 'column', flexGrow: 1, }), - canvasWithSplitter: css({ - overflow: 'unset', - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - }), - canvasWithSplitterEditing: css({ - overflow: 'unset', - }), - bodyWrapper: css({ - label: 'body-wrapper', + container: css({ + label: 'container', display: 'flex', flexDirection: 'column', flexGrow: 1, position: 'relative', }), + bodyWrapper: css({ + label: 'body-wrapper', + display: 'flex', + flexDirection: 'row', + flexGrow: 1, + position: 'relative', + flex: '1 1 0', + overflow: 'hidden', + }), + bodyWithToolbar: css({ + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + minHeight: 0, + overflow: 'auto', + scrollbarWidth: 'thin', + scrollbarGutter: 'stable', + // without top padding the fixed controls headers is rendered over the selection outline. + padding: theme.spacing(0.125, 1, 2, 2), + }), body: css({ label: 'body', display: 'flex', @@ -181,11 +194,6 @@ function getStyles(theme: GrafanaTheme2, headerHeight: number) { // borderLeft: `1px solid ${theme.colors.border.weak}`, // background: theme.colors.background.primary, }), - splitter: css({ - '&:after': { - display: 'none', - }, - }), controlsWrapperSticky: css({ [theme.breakpoints.up('md')]: { position: 'sticky', diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index 8c81d88b345..c68696e439d 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -8,10 +8,9 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { DashboardScene } from '../scene/DashboardScene'; import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector'; -import { EditSchemaV2Button } from '../scene/new-toolbar/actions/EditSchemaV2Button'; import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; -import { dashboardEditActions, undoRedoWasClicked } from './shared'; +import { dashboardEditActions } from './shared'; function useEditPaneOptions( this: DashboardEditableElement, @@ -69,19 +68,16 @@ export class DashboardEditableElement implements EditableDashboardElement { public renderActions(): ReactNode { return ( - <> - - - + ); } } @@ -104,7 +100,7 @@ export function DashboardTitleInput({ dashboard, id }: { dashboard: DashboardSce }} onBlur={(e) => { const titleUnchanged = valueBeforeEdit.current === e.currentTarget.value; - const shouldSkip = titleUnchanged || undoRedoWasClicked(e); + const shouldSkip = titleUnchanged; if (shouldSkip) { return; } @@ -135,7 +131,7 @@ export function DashboardDescriptionInput({ dashboard, id }: { dashboard: Dashbo }} onBlur={(e) => { const descriptionUnchanged = valueBeforeEdit.current === e.currentTarget.value; - const shouldSkip = descriptionUnchanged || undoRedoWasClicked(e); + const shouldSkip = descriptionUnchanged; if (shouldSkip) { return; } diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardExportButton.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardExportButton.tsx new file mode 100644 index 00000000000..35d8afbdb4d --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/DashboardExportButton.tsx @@ -0,0 +1,57 @@ +import { selectors } from '@grafana/e2e-selectors'; +import { t } from '@grafana/i18n'; +import { locationService } from '@grafana/runtime'; +import { Dropdown, Sidebar } from '@grafana/ui'; +import { appEvents } from 'app/core/app_events'; +import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; +import { ShowConfirmModalEvent } from 'app/types/events'; + +import { DashboardScene } from '../scene/DashboardScene'; +import ExportMenu from '../sharing/ExportButton/ExportMenu'; +import { DashboardInteractions } from '../utils/interactions'; + +interface Props { + dashboard: DashboardScene; +} + +const newExportButtonSelector = selectors.pages.Dashboard.DashNav.NewExportButton; + +export function ShareExportDashboardButton({ dashboard }: Props) { + return ( + } placement="left-end"> + { + if (dashboard.state.isEditing && dashboard.state.isDirty) { + evt.preventDefault(); + evt.stopPropagation(); + + appEvents.publish( + new ShowConfirmModalEvent({ + title: t('dashboard.sidebar.export.unsaved-modal.title', 'Save changes to dashboard?'), + text: t( + 'dashboard.sidebar.export.unsaved-modal.text', + 'You have unsaved changes to this dashboard. You need to save them before you can share it.' + ), + icon: 'exclamation-triangle', + noText: t('common.discard', 'Discard'), + yesText: t('common.save', 'Save'), + yesButtonVariant: 'primary', + onConfirm: () => dashboard.openSaveDrawer({}), + }) + ); + } else { + locationService.partial({ shareView: shareDashboardType.export }); + + DashboardInteractions.sharingCategoryClicked({ + item: shareDashboardType.export, + shareResource: getTrackingSource(), + }); + } + }} + /> + + ); +} diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx index 81d56cab57d..7b461551773 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx @@ -101,7 +101,7 @@ describe('DashboardOutline', () => { render( - + ); // select Row lvl 1 diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx index 48a9c738856..4ce6536ac5c 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { SceneObject } from '@grafana/scenes'; -import { Box, Icon, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui'; +import { Box, Icon, Sidebar, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui'; import { isRepeatCloneOrChildOf } from '../utils/clone'; import { DashboardInteractions } from '../utils/interactions'; @@ -17,28 +17,36 @@ import { useOutlineRename } from './useOutlineRename'; export interface Props { editPane: DashboardEditPane; + isEditing: boolean | undefined; } -export function DashboardOutline({ editPane }: Props) { +export function DashboardOutline({ editPane, isEditing }: Props) { const dashboard = getDashboardSceneFor(editPane); return ( - - - + <> + editPane.closePane()} + /> + + + + ); } interface DashboardOutlineNodeProps { sceneObject: SceneObject; editPane: DashboardEditPane; + isEditing: boolean | undefined; depth: number; index: number; } -function DashboardOutlineNode({ sceneObject, editPane, depth, index }: DashboardOutlineNodeProps) { +function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index }: DashboardOutlineNodeProps) { const styles = useStyles2(getStyles); - const { key } = sceneObject.useState(); + const key = sceneObject.state.key; const [isCollapsed, setIsCollapsed] = useState(depth > 0); const { isSelected, onSelect } = useElementSelection(key); const isCloned = useMemo(() => isRepeatCloneOrChildOf(sceneObject), [sceneObject]); @@ -49,7 +57,7 @@ function DashboardOutlineNode({ sceneObject, editPane, depth, index }: Dashboard const children = editableElement.getOutlineChildren?.() ?? []; const elementInfo = editableElement.getEditableElementInfo(); const instanceName = elementInfo.instanceName === '' ? noTitleText : elementInfo.instanceName; - const outlineRename = useOutlineRename(editableElement); + const outlineRename = useOutlineRename(editableElement, isEditing); const isContainer = editableElement.getOutlineChildren ? true : false; const onNodeClicked = (e: React.MouseEvent) => { @@ -131,6 +139,7 @@ function DashboardOutlineNode({ sceneObject, editPane, depth, index }: Dashboard sceneObject={child} editPane={editPane} depth={depth + 1} + isEditing={isEditing} index={i} /> )) diff --git a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx index b2a7a149715..ea14464fc64 100644 --- a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx +++ b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx @@ -1,9 +1,6 @@ -import { css } from '@emotion/css'; - -import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; -import { Button, Menu, Stack, Text, useStyles2, Dropdown, Icon, IconButton } from '@grafana/ui'; +import { Button, Menu, Stack, Dropdown, Icon, Sidebar } from '@grafana/ui'; import { trackDeleteDashboardElement } from 'app/features/dashboard-scene/utils/tracking'; import { EditableDashboardElement } from '../scene/types/EditableDashboardElement'; @@ -17,15 +14,11 @@ interface EditPaneHeaderProps { export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) { const elementInfo = element.getEditableElementInfo(); - const styles = useStyles2(getStyles); const onCopy = element.onCopy?.bind(element); const onDuplicate = element.onDuplicate?.bind(element); const onDelete = element.onDelete?.bind(element); const onConfirmDelete = element.onConfirmDelete?.bind(element); - // temporary simple solution, should select parent element - const onGoBack = () => editPane.clearSelection(); - const canGoBack = editPane.state.selection; const onDeleteElement = () => { if (onConfirmDelete) { @@ -37,20 +30,7 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) { }; return ( -
- - {canGoBack && ( - - )} - {elementInfo.typeName} - + editPane.closePane()}> {element.renderActions && element.renderActions()} {(onCopy || onDuplicate) && ( @@ -95,18 +75,6 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) { /> )} -
+
); } - -function getStyles(theme: GrafanaTheme2) { - return { - wrapper: css({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: theme.spacing(1, 2), - borderBottom: `1px solid ${theme.colors.border.weak}`, - }), - }; -} diff --git a/public/app/features/dashboard-scene/edit-pane/shared.ts b/public/app/features/dashboard-scene/edit-pane/shared.ts index 6d9a2279563..39fd5fec9a7 100644 --- a/public/app/features/dashboard-scene/edit-pane/shared.ts +++ b/public/app/features/dashboard-scene/edit-pane/shared.ts @@ -14,7 +14,6 @@ import { import { DashboardScene } from '../scene/DashboardScene'; import { SceneGridRowEditableElement } from '../scene/layout-default/SceneGridRowEditableElement'; -import { redoButtonId, undoButtonID } from '../scene/new-toolbar/RightActions'; import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement'; import { LocalVariableEditableElement } from '../settings/variables/LocalVariableEditableElement'; import { VariableAdd, VariableAddEditableElement } from '../settings/variables/VariableAddEditableElement'; @@ -299,7 +298,3 @@ function makeEditAction { - if (!selection) { - const dashboard = getDashboardSceneFor(editPane); - return new ElementSelection([[dashboard.state.uid!, dashboard.getRef()]]).createSelectionElement(); - } - - return selection.createSelectionElement(); - }, [selection, editPane]); -} diff --git a/public/app/features/dashboard-scene/edit-pane/useOutlineRename.tsx b/public/app/features/dashboard-scene/edit-pane/useOutlineRename.tsx index 81f3a4556f0..d87d83994eb 100644 --- a/public/app/features/dashboard-scene/edit-pane/useOutlineRename.tsx +++ b/public/app/features/dashboard-scene/edit-pane/useOutlineRename.tsx @@ -8,10 +8,14 @@ export interface OutlineRenameState { error?: string; } -export function useOutlineRename(editableElement: EditableDashboardElement) { +export function useOutlineRename(editableElement: EditableDashboardElement, isEditing: boolean | undefined) { const [state, setState] = useState({}); const onNameDoubleClicked = (evt: React.MouseEvent) => { + if (!isEditing) { + return; + } + if (!editableElement.onChangeName) { return; } diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 5268ea4d865..da18ca17dd4 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -2,6 +2,8 @@ import { css, cx } from '@emotion/css'; import { GrafanaTheme2, VariableHide } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { Trans } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { SceneObjectState, SceneObjectBase, @@ -15,7 +17,8 @@ import { SceneObjectUrlValues, CancelActivationHandler, } from '@grafana/scenes'; -import { Box, useStyles2 } from '@grafana/ui'; +import { Box, Button, useStyles2 } from '@grafana/ui'; +import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { PanelEditControls } from '../panel-edit/PanelEditControls'; import { getDashboardSceneFor } from '../utils/utils'; @@ -25,6 +28,9 @@ import { DashboardDataLayerControls } from './DashboardDataLayerControls'; import { DashboardLinksControls } from './DashboardLinksControls'; import { DashboardScene } from './DashboardScene'; import { VariableControls } from './VariableControls'; +import { EditDashboardSwitch } from './new-toolbar/actions/EditDashboardSwitch'; +import { SaveDashboard } from './new-toolbar/actions/SaveDashboard'; +import { ShareDashboardButton } from './new-toolbar/actions/ShareDashboardButton'; export interface DashboardControlsState extends SceneObjectState { timePicker: SceneTimePicker; @@ -32,7 +38,7 @@ export interface DashboardControlsState extends SceneObjectState { hideTimeControls?: boolean; hideVariableControls?: boolean; hideLinksControls?: boolean; - // Hides the dashbaord-controls dropdown menu + // Hides the dashboard-controls dropdown menu hideDashboardControls?: boolean; } @@ -171,6 +177,7 @@ function DashboardControlsRenderer({ model }: SceneComponentProps )} {!hideDashboardControls && model.hasDashboardControls() && } + {config.featureToggles.dashboardNewLayouts && }
{!hideVariableControls && ( <> @@ -185,6 +192,37 @@ function DashboardControlsRenderer({ model }: SceneComponentProps + {showShareButton && } + {isEditing && } + {!isPlaying && canEditDashboard && } + {isPlaying && ( + + )} + + ); +} + function renderHiddenVariables(dashboard: DashboardScene) { const { variables } = sceneGraph.getVariables(dashboard).useState(); const renderAsHiddenVariables = variables.filter((v) => v.UNSAFE_renderAsHidden); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 1c75360a7b5..a542dd92826 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -275,7 +275,7 @@ export class DashboardScene extends SceneObjectBase impleme this._initialUrlState = locationService.getLocation(); // Switch to edit mode - this.setState({ isEditing: true }); + this.setState({ isEditing: true, editable: true }); // Propagate change edit mode change to children this.state.body.editModeChanged?.(true); @@ -692,16 +692,16 @@ export class DashboardScene extends SceneObjectBase impleme canEditDashboard() { const { meta } = this.state; - return Boolean(meta.canEdit || meta.canMakeEditable || config.viewersCanEdit); + return !meta.isSnapshot && Boolean(meta.canEdit || meta.canMakeEditable || config.viewersCanEdit); } public getInitialSaveModel() { return this.serializer.initialSaveModel; } - public getSnapshotUrl = () => { - return this.serializer.getSnapshotUrl(); - }; + public getSnapshotUrl() { + return this.serializer.getSnapshotUrl() ?? ''; + } /** Hacky temp function until we refactor transformSaveModelToScene a bit */ setInitialSaveModel(model?: Dashboard, meta?: DashboardMeta, apiVersion?: string): void; diff --git a/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx index 2efcad5a256..80c8487f1c5 100644 --- a/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx +++ b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx @@ -20,7 +20,7 @@ export function GoToSnapshotOriginButton(props: { originalURL: string }) { ); } -const onOpenSnapshotOriginalDashboard = (originalUrl: string) => { +export const onOpenSnapshotOriginalDashboard = (originalUrl: string) => { const relativeURL = originalUrl ?? ''; const sanitizedRelativeURL = textUtil.sanitizeUrl(relativeURL); try { diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 68ccadaba13..959913c8f8f 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -153,9 +153,7 @@ export function ToolbarActions({ dashboard }: Props) { toolbarActions.push({ group: 'icon-actions', condition: meta.isSnapshot && !isEditing, - render: () => ( - - ), + render: () => , }); if (!isEditingPanel && !isEditing) { diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx index 59dfc714669..2e2f979d51d 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx @@ -1,8 +1,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { ToolbarButton, ToolbarButtonRow, useStyles2 } from '@grafana/ui'; +import { ToolbarButtonRow, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; @@ -11,45 +10,35 @@ import { isLibraryPanel } from '../../utils/utils'; import { DashboardScene } from '../DashboardScene'; import { BackToDashboardButton } from './actions/BackToDashboardButton'; -import { DashboardSettingsButton } from './actions/DashboardSettingsButton'; import { DiscardLibraryPanelButton } from './actions/DiscardLibraryPanelButton'; import { DiscardPanelButton } from './actions/DiscardPanelButton'; -import { EditDashboardSwitch } from './actions/EditDashboardSwitch'; -import { ExportDashboardButton } from './actions/ExportDashboardButton'; import { MakeDashboardEditableButton } from './actions/MakeDashboardEditableButton'; import { PlayListNextButton } from './actions/PlayListNextButton'; import { PlayListPreviousButton } from './actions/PlayListPreviousButton'; import { PlayListStopButton } from './actions/PlayListStopButton'; import { SaveDashboard } from './actions/SaveDashboard'; import { SaveLibraryPanelButton } from './actions/SaveLibraryPanelButton'; -import { ShareDashboardButton } from './actions/ShareDashboardButton'; import { UnlinkLibraryPanelButton } from './actions/UnlinkLibraryPanelButton'; -import { ToolbarActionProps } from './types'; import { getDynamicActions, renderActionElements } from './utils'; export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => { - const { editPanel, editable, editview, isEditing, uid, meta, viewPanel } = dashboard.useState(); + const { editPanel, editable, editview, isEditing, meta, viewPanel } = dashboard.useState(); const { isPlaying } = playlistSrv.useState(); const styles = useStyles2(getStyles); const isEditable = Boolean(editable); const canSave = Boolean(meta.canSave); - const hasUid = Boolean(uid); const isEditingDashboard = Boolean(isEditing); const hasEditView = Boolean(editview); const isEditingPanel = Boolean(editPanel); const isViewingPanel = Boolean(viewPanel); const isEditingLibraryPanel = isEditingPanel && isLibraryPanel(editPanel!.state.panelRef.resolve()); const isShowingDashboard = !hasEditView && !isViewingPanel && !isEditingPanel; - const isEditingAndShowingDashboard = isEditingDashboard && isShowingDashboard; - const isSnapshot = Boolean(meta.isSnapshot); const canSaveInFolder = contextSrv.hasEditPermissionInFolders; const canEditDashboard = dashboard.canEditDashboard(); const showPanelButtons = isEditingPanel && !hasEditView && !isViewingPanel; const showPlayButtons = isPlaying && isShowingDashboard && !isEditingDashboard; - const showShareButton = hasUid && !isSnapshot && !isPlaying && !isEditingPanel; - const showUndoRedoButtons = isEditingAndShowingDashboard && !!config.featureToggles.dashboardUndoRedo; return ( @@ -106,28 +95,10 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => { group: 'panel', condition: showPanelButtons && isEditingLibraryPanel, }, - { - key: 'dashboard-undo', - component: UndoButton, - group: 'dashboard', - condition: showUndoRedoButtons, - }, - { - key: 'dashboard-redo', - component: RedoButton, - group: 'dashboard', - condition: showUndoRedoButtons, - }, - { - key: 'dashboard-settings', - component: DashboardSettingsButton, - group: 'dashboard', - condition: isEditingAndShowingDashboard && canEditDashboard, - }, { key: 'save-dashboard', component: SaveDashboard, - group: 'save-edit', + group: 'panel', condition: isEditingDashboard && !isEditingLibraryPanel && (canSave || canSaveInFolder), }, { @@ -136,31 +107,6 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => { group: 'save-edit', condition: !isEditing && canEditDashboard && !isViewingPanel && !isEditable && !isPlaying, }, - { - key: 'edit-dashboard-switch', - component: EditDashboardSwitch, - group: 'save-edit', - condition: - canEditDashboard && - !isEditingPanel && - !isEditingLibraryPanel && - !isViewingPanel && - isEditable && - !isPlaying && - !isEditingPanel, - }, - { - key: 'new-export-dashboard-button', - component: ExportDashboardButton, - group: 'export-share', - condition: showShareButton, - }, - { - key: 'new-share-dashboard-button', - component: ShareDashboardButton, - group: 'export-share', - condition: showShareButton, - }, ], dashboard )} @@ -168,42 +114,6 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => { ); }; -export const undoButtonID = 'undo-button'; -function UndoButton({ dashboard }: ToolbarActionProps) { - const editPane = dashboard.state.editPane; - const { undoStack } = editPane.useState(); - const undoAction = undoStack[undoStack.length - 1]; - const tooltip = `Undo${undoAction?.description ? ` '${undoAction.description}'` : ''}`; - - return ( - editPane.undoAction()} - tooltip={tooltip} - /> - ); -} - -export const redoButtonId = 'redo-button'; -function RedoButton({ dashboard }: ToolbarActionProps) { - const editPane = dashboard.state.editPane; - const { redoStack } = editPane.useState(); - const redoAction = redoStack[redoStack.length - 1]; - const tooltip = `Redo${redoAction?.description ? ` '${redoAction.description}'` : ''}`; - - return ( - editPane.redoAction()} - /> - ); -} - const getStyles = (theme: GrafanaTheme2) => ({ container: css({ paddingLeft: theme.spacing(0.5) }), }); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx deleted file mode 100644 index d8004d00b13..00000000000 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { t } from '@grafana/i18n'; -import { config, locationService } from '@grafana/runtime'; -import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; - -import ExportMenu from '../../../sharing/ExportButton/ExportMenu'; -import { DashboardInteractions } from '../../../utils/interactions'; -import { ToolbarActionProps } from '../types'; - -import { ShareExportDashboardButton } from './ShareExportDashboardButton'; - -const newExportButtonSelector = e2eSelectors.pages.Dashboard.DashNav.NewExportButton; - -export const ExportDashboardButton = ({ dashboard }: ToolbarActionProps) => { - const buttonTooltip = config.featureToggles.kubernetesDashboards - ? t('dashboard.toolbar.new.export.tooltip.as-code', 'Export as code') - : t('dashboard.toolbar.new.export.tooltip.json', 'Export as JSON'); - - return ( - } - groupTestId={newExportButtonSelector.container} - buttonLabel={t('dashboard.toolbar.new.export.title', 'Export')} - buttonTooltip={buttonTooltip} - buttonTestId={newExportButtonSelector.container} - onButtonClick={() => { - locationService.partial({ shareView: shareDashboardType.export }); - - DashboardInteractions.sharingCategoryClicked({ - item: shareDashboardType.export, - shareResource: getTrackingSource(), - }); - }} - arrowLabel={t('dashboard.toolbar.new.export.arrow', 'Export')} - arrowTestId={newExportButtonSelector.arrowMenu} - dashboard={dashboard} - /> - ); -}; diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/OpenSnapshotOriginButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/OpenSnapshotOriginButton.tsx index c59d06a74ae..51d04d2272a 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/OpenSnapshotOriginButton.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/OpenSnapshotOriginButton.tsx @@ -2,5 +2,5 @@ import { GoToSnapshotOriginButton } from '../../GoToSnapshotOriginButton'; import { ToolbarActionProps } from '../types'; export const OpenSnapshotOriginButton = ({ dashboard }: ToolbarActionProps) => ( - + ); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveDashboard.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveDashboard.tsx index 0c33cf19b68..f2819e44c1c 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveDashboard.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveDashboard.tsx @@ -6,10 +6,12 @@ import { contextSrv } from 'app/core/services/context_srv'; import { ToolbarActionProps } from '../types'; export const SaveDashboard = ({ dashboard }: ToolbarActionProps) => { - const { meta, isDirty, uid } = dashboard.state; + const { meta, isDirty, uid, editview, editPanel } = dashboard.state; const isNew = !Boolean(uid || dashboard.isManaged()); const isManaged = dashboard.isManaged(); + // In dashboard settings we still use the nav toolbar for a short while + const buttonSize = Boolean(editview) || editPanel ? 'sm' : 'md'; // if we only can save if (isNew) { @@ -17,7 +19,7 @@ export const SaveDashboard = ({ dashboard }: ToolbarActionProps) => { diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ToolbarSwitch.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ToolbarSwitch.tsx index 59262fc152e..0ec0232e985 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ToolbarSwitch.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ToolbarSwitch.tsx @@ -43,7 +43,7 @@ export const ToolbarSwitch = ({ onClick={disabled ? undefined : onClick} >
- +
@@ -53,11 +53,11 @@ export const ToolbarSwitch = ({ const getStyles = (theme: GrafanaTheme2) => ({ container: css({ border: `1px solid ${theme.components.input.borderColor}`, - padding: theme.spacing(0.25), + padding: theme.spacing(0.5), backgroundColor: theme.components.input.background, borderRadius: theme.shape.radius.default, - width: theme.spacing(5.5), - height: theme.spacing(3), + width: theme.spacing(6.5), + height: theme.spacing(theme.components.height.md), cursor: 'pointer', display: 'flex', flexDirection: 'row', @@ -90,19 +90,19 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'flex', alignItems: 'center', justifyContent: 'center', - width: theme.spacing(2.5), + width: theme.spacing(3.5), height: '100%', transform: 'translateX(0)', position: 'relative', borderRadius: styleMixins.getInternalRadius(theme, 2), - border: `1px solid ${theme.colors.secondary.border}`, + border: `1px solid ${theme.colors.border.weak}`, [theme.transitions.handleMotion('no-preference', 'reduce')]: { transition: 'all 0.2s ease-in-out', }, }), boxChecked: css({ - transform: `translateX(calc(100% - ${theme.spacing(0.25)}))`, + transform: `translateX(calc(100% - 14px))`, borderColor: 'transparent', }), }); diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx index 78876f4ee55..00e072e4d6f 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx @@ -9,7 +9,7 @@ import { Input, TextArea, Button, Field, Box, Stack } from '@grafana/ui'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; -import { dashboardEditActions, undoRedoWasClicked } from '../../edit-pane/shared'; +import { dashboardEditActions } from '../../edit-pane/shared'; import { useEditPaneInputAutoFocus } from '../../scene/layouts-shared/utils'; import { BulkActionElement } from '../../scene/types/BulkActionElement'; import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement'; @@ -161,7 +161,7 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable onChange={onChange} onBlur={(e) => { const labelUnchanged = oldName.current === name; - const shouldSkip = labelUnchanged || undoRedoWasClicked(e); + const shouldSkip = labelUnchanged; if (nameError) { setNameError(undefined); @@ -200,7 +200,7 @@ function VariableLabelInput({ variable, id }: VariableInputProps) { onChange={(e) => variable.setState({ label: e.currentTarget.value })} onBlur={(e) => { const labelUnchanged = oldLabel.current === e.currentTarget.value; - const shouldSkip = labelUnchanged || undoRedoWasClicked(e); + const shouldSkip = labelUnchanged; if (shouldSkip) { return; @@ -232,7 +232,7 @@ function VariableDescriptionTextArea({ variable, id }: VariableInputProps) { onChange={(e) => variable.setState({ description: e.currentTarget.value })} onBlur={(e) => { const labelUnchanged = oldDescription.current === e.currentTarget.value; - const shouldSkip = labelUnchanged || undoRedoWasClicked(e); + const shouldSkip = labelUnchanged; if (shouldSkip) { return; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index e8d4b15d05f..dad607c0de0 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4172,6 +4172,7 @@ "clear": "Clear", "collapse": "Collapse", "disabled": "Disabled", + "discard": "Discard", "edit": "Edit", "help": "Help", "loading": "Loading...", @@ -4789,7 +4790,6 @@ "variable": "{{type}} variable", "variable-set": "Variables" }, - "open": "Open options pane", "row": { "header": { "hide": "Hide", @@ -5140,6 +5140,7 @@ "title-matched_other": "Matched {{count}}/{{totalCount}} options" }, "outline": { + "pane-header": "Content outline", "repeated-item": "Repeat", "tree-item": { "empty": "(empty)", @@ -5351,6 +5352,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Loading configuration" }, + "sidebar": { + "dashboard-options": { + "title": "Options", + "tooltip": "Dashboard options" + }, + "edit-schema": { + "title": "Code", + "tooltip": "Edit as code" + }, + "export": { + "title": "Export", + "unsaved-modal": { + "text": "You have unsaved changes to this dashboard. You need to save them before you can share it.", + "title": "Save changes to dashboard?" + } + }, + "outline": { + "title": "Outline", + "tooltip": "Content outline" + }, + "redo": "Redo", + "snapshot": { + "tooltip": "Open original dashboard" + }, + "undo": "Undo" + }, "solo-panel": { "loading-initializing-dashboard": "Loading & initializing dashboard", "title-not-found": "Panel with id {{panelId}} not found" @@ -5454,11 +5481,8 @@ "tooltip": "This dashboard was marked as read only" }, "export": { - "arrow": "Export", - "title": "Export", "tooltip": { - "as-code": "Export as code", - "json": "Export as JSON" + "as-code": "Export as code" } }, "more-save-options": "More save options", @@ -5510,6 +5534,9 @@ "save-library-panel": "Save library panel", "settings": "Dashboard settings", "share-button": "Share", + "snapshot": { + "title": "Source" + }, "star-add-error": "Failed to add to starred", "star-added": "Added to starred", "star-remove-error": "Failed to remove from starred", @@ -5839,9 +5866,6 @@ "name-values-separated-comma": "Values separated by comma", "selection-options": "Selection options" }, - "dashboard-edit-pane-renderer": { - "outline": "Outline" - }, "dashboard-link-form": { "back-to-list": "Back to list", "label-icon": "Icon", @@ -8121,13 +8145,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Go back" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { From d49993ddab08bf0d8daa544ec110ad710c7c27c0 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 26 Nov 2025 15:45:36 +0200 Subject: [PATCH 133/378] Provisioning: Disable imports for new dashboard (#114419) * Provisioning: Disable imports for new dashboard * Refactor --- .../DashboardEmpty/DashboardEmpty.tsx | 10 +++++----- .../DashboardEmpty/DashboardEmptyHooks.ts | 20 +++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmpty.tsx index bef4318e339..8c623c2c7ca 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmpty.tsx @@ -15,7 +15,7 @@ import { SuggestedDashboards } from '../DashboardLibrary/SuggestedDashboards'; import { DashboardEmptyExtensionPoint } from './DashboardEmptyExtensionPoint'; import { - useIsReadOnlyRepo, + useRepositoryStatus, useOnAddVisualization, useOnAddLibraryPanel, useOnImportDashboard, @@ -149,10 +149,10 @@ export interface Props { // We pass the default empty UI through to the extension point so that the extension can conditionally render it if needed. // For example, an extension might want to render custom UI for a specific experiment cohort, and the default UI for everyone else. const DashboardEmpty = (props: Props) => { - const isReadOnlyRepo = useIsReadOnlyRepo(props); - const onAddVisualization = useOnAddVisualization({ ...props, isReadOnlyRepo }); - const onAddLibraryPanel = useOnAddLibraryPanel({ ...props, isReadOnlyRepo }); - const onImportDashboard = useOnImportDashboard({ ...props, isReadOnlyRepo }); + const { isReadOnlyRepo, isProvisioned } = useRepositoryStatus(props); + const onAddVisualization = useOnAddVisualization({ ...props, isReadOnlyRepo, isProvisioned }); + const onAddLibraryPanel = useOnAddLibraryPanel({ ...props, isReadOnlyRepo, isProvisioned }); + const onImportDashboard = useOnImportDashboard({ ...props, isReadOnlyRepo, isProvisioned }); return ( { - const { isReadOnlyRepo } = useGetResourceRepositoryView({ +export const useRepositoryStatus = ({ dashboard }: Props) => { + const { isReadOnlyRepo, repository } = useGetResourceRepositoryView({ folderName: dashboard instanceof DashboardScene ? dashboard.state.meta.folderUid : dashboard.meta.folderUid, }); - return isReadOnlyRepo; + const isFolderProvisioned = Boolean(repository); + const isProvisioned = isFolderProvisioned || (dashboard instanceof DashboardScene && dashboard.isManagedRepository()); + + return { + isReadOnlyRepo, + isProvisioned, + }; }; interface HookProps extends Props { isReadOnlyRepo: boolean; + isProvisioned: boolean; } export const useOnAddVisualization = ({ dashboard, canCreate, isReadOnlyRepo }: HookProps) => { @@ -53,9 +60,7 @@ export const useOnAddVisualization = ({ dashboard, canCreate, isReadOnlyRepo }: }, [canCreate, isReadOnlyRepo, dashboard, dispatch, initialDatasource]); }; -export const useOnAddLibraryPanel = ({ dashboard, canCreate, isReadOnlyRepo }: HookProps) => { - const isProvisioned = dashboard instanceof DashboardScene && dashboard.isManagedRepository(); - +export const useOnAddLibraryPanel = ({ dashboard, canCreate, isReadOnlyRepo, isProvisioned }: HookProps) => { return useMemo(() => { if (!canCreate || isProvisioned || isReadOnlyRepo) { return undefined; @@ -72,8 +77,7 @@ export const useOnAddLibraryPanel = ({ dashboard, canCreate, isReadOnlyRepo }: H }, [canCreate, isProvisioned, isReadOnlyRepo, dashboard]); }; -export const useOnImportDashboard = ({ dashboard, canCreate, isReadOnlyRepo }: HookProps) => { - const isProvisioned = dashboard instanceof DashboardScene && dashboard.isManagedRepository(); +export const useOnImportDashboard = ({ canCreate, isReadOnlyRepo, isProvisioned }: HookProps) => { return useMemo(() => { if (!canCreate || isProvisioned || isReadOnlyRepo) { return undefined; From 9606e9c51cb70d096df60d903e9c1de44514912f Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Wed, 26 Nov 2025 07:48:18 -0600 Subject: [PATCH 134/378] Docs: Clarify the uid is metadata.name (#114432) --- .../developer-resources/api-reference/http-api/dashboard.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/developer-resources/api-reference/http-api/dashboard.md b/docs/sources/developer-resources/api-reference/http-api/dashboard.md index 5f28d4c54b1..2cc7aa84bb3 100644 --- a/docs/sources/developer-resources/api-reference/http-api/dashboard.md +++ b/docs/sources/developer-resources/api-reference/http-api/dashboard.md @@ -767,12 +767,12 @@ Status Codes: Deletes a dashboard via the dashboard uid. -- namespace: to read more about the namespace to use, see the [API overview](https://grafana.com/docs/grafana//developers/http_api/apis/). -- uid: the unique identifier of the dashboard to update. this will be the _name_ in the dashboard response +- **`namespace`**: To read more about the namespace to use, see the [API overview](https://grafana.com/docs/grafana//developers/http_api/apis/). +- **`uid`**: The unique identifier of the dashboard to update. This is the `metadata.name` field in the dashboard response and _not_ the `metadata.uid` field. **Required permissions** -See note in the [introduction]({{< ref "#dashboard-api" >}}) for an explanation. +See note in the [introduction](#new-dashboard-apis) for an explanation. | Action | Scope | From 84fbe6bc7ba6e9562d941551ff907797650e7dcd Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:51:31 +0100 Subject: [PATCH 135/378] Alerting: Analyze an alert rule with Grafana Assistant (#114420) * fix * rename to analyze * Enable Analyze rule for GMA recording rules * Fix declare incident button condition --------- Co-authored-by: Konrad Lalik --- .../unified/PanelAlertTabContent.test.tsx | 3 + .../alerting/unified/RuleList.test.tsx | 3 + .../assistant/AnalizeRuleButton.tsx | 139 ++++++++++++++++++ .../components/rule-viewer/AlertRuleMenu.tsx | 6 + .../rule-viewer/RuleViewer.test.tsx | 4 + .../rules/RuleActionsButtons.test.tsx | 4 + .../rules/RuleListGroupView.test.tsx | 4 + .../rules/RuleListStateView.test.tsx | 4 + .../components/rules/RulesTable.test.tsx | 4 + .../group-details/GroupDetailsPage.test.tsx | 4 + .../rule-list/DataSourceGroupLoader.test.tsx | 4 + .../unified/rule-list/FilterView.test.tsx | 4 + .../rule-list/GrafanaGroupLoader.test.tsx | 76 ++++++++++ .../PanelDataAlertingTab.test.tsx | 9 +- public/locales/en-US/grafana.json | 1 + 15 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 public/app/features/alerting/unified/components/assistant/AnalizeRuleButton.tsx diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx index dfe3549eeee..dac15642d7b 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx @@ -28,6 +28,9 @@ import { Annotation } from './utils/constants'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; jest.mock('./api/ruler'); +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); jest.spyOn(alertingAbilities, 'useAlertRuleAbility'); const prometheusModuleSettings = { alerting: true, module: 'core:plugin/prometheus' }; diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index f58aadb9afd..3849c4708aa 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -45,6 +45,9 @@ jest.mock('@grafana/runtime', () => ({ jest.mock('./api/buildInfo'); jest.mock('./api/prometheus'); jest.mock('./api/ruler'); +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); jest.spyOn(actions, 'rulesInSameGroupHaveInvalidFor').mockReturnValue([]); jest.spyOn(apiRuler, 'rulerUrlBuilder'); diff --git a/public/app/features/alerting/unified/components/assistant/AnalizeRuleButton.tsx b/public/app/features/alerting/unified/components/assistant/AnalizeRuleButton.tsx new file mode 100644 index 00000000000..d3aa6db4ec0 --- /dev/null +++ b/public/app/features/alerting/unified/components/assistant/AnalizeRuleButton.tsx @@ -0,0 +1,139 @@ +import { useMemo } from 'react'; + +import { OpenAssistantProps, createAssistantContextItem, useAssistant } from '@grafana/assistant'; +import { t } from '@grafana/i18n'; +import { reportInteraction } from '@grafana/runtime'; +import { Menu } from '@grafana/ui'; +import { GrafanaAlertingRule, GrafanaRecordingRule, GrafanaRule } from 'app/types/unified-alerting'; + +import { prometheusRuleType } from '../../utils/rules'; + +interface AnalyzeRuleButtonProps { + /** Alert rule to analyze */ + rule: GrafanaRule; +} + +/** + * A menu item component that analyze an alert rule. + * Automatically creates context from alert data and opens the assistant in assistant mode. + */ +export function AnalyzeRuleButton(props: AnalyzeRuleButtonProps) { + const { isAvailable, openAssistant } = useAssistant(); + + if (!isAvailable || !openAssistant) { + return null; + } + + return ; +} + +function AnalyzeRuleButtonView({ + rule, + openAssistant, +}: AnalyzeRuleButtonProps & { + openAssistant: (props: OpenAssistantProps) => void; +}) { + // Create alert rule context from alert rule data + const alertContext = useMemo(() => { + return createAssistantContextItem('structured', { + title: `Alert: ${rule.name}`, + data: { + rule: { + name: rule.name, + uid: rule.uid, + labels: rule.labels, + query: rule.query, + }, + }, + }); + }, [rule]); + + // Generate default prompt + const analyzeRulePrompt = useMemo(() => buildAnalyzeRulePrompt(rule), [rule]); + + const handleClick = () => { + reportInteraction('grafana_assistant_app_analyze_rule_button_clicked', { + origin: 'alerting', + alertName: rule.name, + alertState: prometheusRuleType.grafana.alertingRule(rule) ? rule.state : undefined, + }); + + openAssistant({ + origin: 'alerting', + mode: 'assistant', + prompt: analyzeRulePrompt, + context: [alertContext], + autoSend: true, + }); + }; + + return ( + + ); +} + +/** + * Builds a prompt for analyzing a rule (alerting or recording). + * Automatically detects the rule type and uses the appropriate prompt builder. + */ +function buildAnalyzeRulePrompt(rule: GrafanaRule): string { + if (prometheusRuleType.grafana.alertingRule(rule)) { + return buildAnalyzeAlertingRulePrompt(rule); + } else if (prometheusRuleType.grafana.recordingRule(rule)) { + return buildAnalyzeRecordingRulePrompt(rule); + } + // Fallback (should not happen for GrafanaRule, but TypeScript requires it) + return `Analyze the rule "${rule.name}".`; +} + +/** + * Builds a prompt for analyzing an alerting rule. + * Includes state, activeAt timestamp, annotations, and labels. + */ +function buildAnalyzeAlertingRulePrompt(rule: GrafanaAlertingRule): string { + const state = rule.state || 'firing'; + const timeInfo = rule.activeAt ? ` starting at ${new Date(rule.activeAt).toISOString()}` : ''; + + let prompt = `Analyze the ${state} alert "${rule.name}"${timeInfo}.`; + + const description = rule.annotations?.description || rule.annotations?.summary || ''; + if (description) { + prompt += ` ${description}`; + } + + const labelsStr = rule.labels + ? Object.entries(rule.labels) + .map(([k, v]) => `${k}="${v}"`) + .join(', ') + : ''; + if (labelsStr) { + prompt += ` Labels: ${labelsStr}.`; + } + + return prompt; +} + +/** + * Builds a prompt for analyzing a recording rule. + * Includes name, query, and labels (no state or activeAt). + */ +function buildAnalyzeRecordingRulePrompt(rule: GrafanaRecordingRule): string { + const labelsStr = rule.labels + ? Object.entries(rule.labels) + .map(([k, v]) => `${k}="${v}"`) + .join(', ') + : ''; + + let prompt = `Analyze the recording rule "${rule.name}".`; + + if (labelsStr) { + prompt += ` Labels: ${labelsStr}.`; + } + + return prompt; +} diff --git a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx index 8834fd72580..a473eae60a1 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx @@ -1,5 +1,6 @@ import { PropsOf } from '@emotion/react'; +import { useAssistant } from '@grafana/assistant'; import { AppEvents } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; @@ -29,6 +30,7 @@ import { rulerRuleType, } from '../../utils/rules'; import { createRelativeUrl } from '../../utils/url'; +import { AnalyzeRuleButton } from '../assistant/AnalizeRuleButton'; import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton'; interface Props { @@ -126,6 +128,9 @@ const AlertRuleMenu = ({ prometheusRuleType.alertingRule(promRule) && promRule.state === PromAlertingRuleState.Firing; + const { isAvailable: isAssistantAvailable } = useAssistant(); + const shouldShowAnalyzeRuleButton = isAssistantAvailable && prometheusRuleType.grafana.rule(promRule); + const shareUrl = createShareLink(identifier); const showDivider = @@ -172,6 +177,7 @@ const AlertRuleMenu = ({ )} {/* TODO Migrate Declare Incident to plugin links extensions */} {shouldShowDeclareIncidentButton && } + {shouldShowAnalyzeRuleButton && } {canDuplicate && ( ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + // metadata and interactive elements const ELEMENTS = { loading: byText(/Loading rule/i), diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx index 5de673ccb14..a88d1bef83f 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx @@ -21,6 +21,10 @@ import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { setupDataSources } from '../../testSetup/datasources'; import { fromCombinedRule, stringifyIdentifier } from '../../utils/rule-id'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + setupMswServer(); jest.mock('app/core/services/context_srv'); const mockContextSrv = jest.mocked(contextSrv); diff --git a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx index 19100a72600..42a7594027b 100644 --- a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx @@ -15,6 +15,10 @@ import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { RuleListGroupView } from './RuleListGroupView'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + jest.spyOn(analytics, 'logInfo'); const ui = { diff --git a/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx b/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx index 260c7436a18..51739c6ae4c 100644 --- a/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx @@ -10,6 +10,10 @@ import { } from 'app/features/alerting/unified/mocks'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + setPluginLinksHook(() => ({ links: [], isLoading: false, diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx index 61c143ab843..a263b4711ca 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx @@ -18,6 +18,10 @@ import { mimirDataSource } from '../../mocks/server/configure'; import { RulesTable } from './RulesTable'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + jest.mock('../../hooks/useAbilities'); const mocks = { diff --git a/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx b/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx index 90485290325..4e1bb3056fb 100644 --- a/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx +++ b/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx @@ -23,6 +23,10 @@ import { alertingFactory } from '../mocks/server/db'; import GroupDetailsPage from './GroupDetailsPage'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + jest.mock('react-virtualized-auto-sizer', () => { return ({ children }: Props) => children({ diff --git a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx index 53b80f1ebaf..a057b5030a3 100644 --- a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx +++ b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx @@ -17,6 +17,10 @@ import { fromRulerRuleAndGroupIdentifierV2 } from '../utils/rule-id'; import { DataSourceGroupLoader } from './DataSourceGroupLoader'; import { createViewLinkFromIdentifier } from './DataSourceRuleListItem'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + setPluginLinksHook(() => ({ links: [], isLoading: false })); setPluginComponentsHook(() => ({ components: [], isLoading: false })); diff --git a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx index 3a7348048da..5c8768f58ec 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx @@ -12,6 +12,10 @@ import { RulesFilter } from '../search/rulesSearchParser'; import { FilterView } from './FilterView'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + setPluginLinksHook(() => ({ links: [], isLoading: false })); setPluginComponentsHook(() => ({ components: [], isLoading: false })); diff --git a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx index 3105c3efd6b..7385215637a 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx @@ -1,6 +1,7 @@ import { render } from 'test/test-utils'; import { byLabelText, byRole } from 'testing-library-selector'; +import { useAssistant } from '@grafana/assistant'; import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; import { AccessControlAction } from 'app/types/accessControl'; import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; @@ -22,6 +23,12 @@ import { intervalToSeconds } from '../utils/time'; import { GrafanaGroupLoader } from './GrafanaGroupLoader'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: jest.fn(), + createAssistantContextItem: jest.fn((type, data) => ({ type, ...data })), +})); +const mockUseAssistant = jest.mocked(useAssistant); + setPluginLinksHook(() => ({ links: [], isLoading: false })); setPluginComponentsHook(() => ({ components: [], isLoading: false })); @@ -41,11 +48,18 @@ const ui = { export: () => byRole('menuitem', { name: /export/i }), delete: () => byRole('menuitem', { name: /delete/i }), pause: () => byRole('menuitem', { name: /pause/i }), + analyzeRule: () => byRole('menuitem', { name: /analyze rule/i }), }, }; describe('GrafanaGroupLoader', () => { beforeEach(() => { + mockUseAssistant.mockReturnValue({ + isAvailable: false, + openAssistant: jest.fn(), + closeAssistant: jest.fn(), + toggleAssistant: jest.fn(), + }); grantUserPermissions([ AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleDelete, @@ -213,6 +227,68 @@ describe('GrafanaGroupLoader', () => { const menuItems = byRole('menuitem').getAll(); expect(menuItems.length).toBe(6); }); + + it('should render Analyze rule menu item when assistant is available', async () => { + mockUseAssistant.mockReturnValue({ + isAvailable: true, + openAssistant: jest.fn(), + closeAssistant: jest.fn(), + toggleAssistant: jest.fn(), + }); + + setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]); + + const groupIdentifier = getGroupIdentifier(grafanaRulerGroup); + + const { user } = render( + + ); + + const [rule1] = grafanaRulerGroup.rules; + const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find(); + + // Click the More button to open the menu + const moreButton = ui.moreButton().get(ruleListItem); + await user.click(moreButton); + + // Check that Analyze rule menu item is present + expect(ui.menuItems.analyzeRule().get()).toBeInTheDocument(); + + // With assistant enabled, there should be 7 menu items (6 + Analyze rule) + const menuItems = byRole('menuitem').getAll(); + expect(menuItems.length).toBe(7); + }); + + it('should not render Analyze rule menu item when assistant is not available', async () => { + mockUseAssistant.mockReturnValue({ + isAvailable: false, + openAssistant: jest.fn(), + closeAssistant: jest.fn(), + toggleAssistant: jest.fn(), + }); + + setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]); + + const groupIdentifier = getGroupIdentifier(grafanaRulerGroup); + + const { user } = render( + + ); + + const [rule1] = grafanaRulerGroup.rules; + const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find(); + + // Click the More button to open the menu + const moreButton = ui.moreButton().get(ruleListItem); + await user.click(moreButton); + + // Check that Analyze rule menu item is NOT present + expect(ui.menuItems.analyzeRule().query()).not.toBeInTheDocument(); + + // Without assistant, there should be 6 menu items + const menuItems = byRole('menuitem').getAll(); + expect(menuItems.length).toBe(6); + }); }); function rulerGroupToPromGroup(group: RulerRuleGroupDTO): GrafanaPromRuleGroupDTO { diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx index 15e323aa8a9..fd15ec40350 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx @@ -40,14 +40,15 @@ import { PanelDataAlertingTab, PanelDataAlertingTabRendered } from './PanelDataA jest.mock('app/features/alerting/unified/api/prometheus'); jest.mock('app/features/alerting/unified/api/ruler'); +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + jest.spyOn(ruleActionButtons, 'matchesWidth').mockReturnValue(false); jest.spyOn(ruler, 'rulerUrlBuilder'); jest.spyOn(alertingAbilities, 'useAlertRuleAbility'); -setPluginLinksHook(() => ({ - links: [], - isLoading: false, -})); +setPluginLinksHook(() => ({ links: [], isLoading: false })); const dataSources = { prometheus: mockDataSource( diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index dad607c0de0..9f8b19218de 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -477,6 +477,7 @@ "noOptionsMessage-no-datasources-found": "No datasources found" }, "alert-menu": { + "analyze-rule": "Analyze rule", "copy-link": "Copy link", "duplicate": "Duplicate", "export": "Export", From 759d49a1df0b30bde07289c0aecf7e042ba7914c Mon Sep 17 00:00:00 2001 From: Andres Torres Date: Wed, 26 Nov 2025 09:58:49 -0500 Subject: [PATCH 136/378] feat(setting): Adding setting service client (#114428) --- .github/CODEOWNERS | 1 + pkg/services/setting/service.go | 384 +++++++++++++++++++ pkg/services/setting/service_test.go | 542 +++++++++++++++++++++++++++ 3 files changed, 927 insertions(+) create mode 100644 pkg/services/setting/service.go create mode 100644 pkg/services/setting/service_test.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 61ab3a10b1f..465c49ee713 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -185,6 +185,7 @@ /pkg/services/search/ @grafana/grafana-search-and-storage /pkg/services/searchusers/ @grafana/grafana-search-and-storage /pkg/services/secrets/ @grafana/grafana-operator-experience-squad +/pkg/services/setting/ @grafana/grafana-backend-services-squad /pkg/services/shorturls/ @grafana/sharing-squad /pkg/services/sqlstore/ @grafana/grafana-search-and-storage /pkg/services/ssosettings/ @grafana/identity-squad diff --git a/pkg/services/setting/service.go b/pkg/services/setting/service.go new file mode 100644 index 00000000000..5afc9e22159 --- /dev/null +++ b/pkg/services/setting/service.go @@ -0,0 +1,384 @@ +package setting + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" + "gopkg.in/ini.v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + utilnet "k8s.io/apimachinery/pkg/util/net" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/client-go/dynamic" + clientrest "k8s.io/client-go/rest" + "k8s.io/client-go/transport" + + authlib "github.com/grafana/authlib/authn" + logging "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/semconv" +) + +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/setting") + +const LogPrefix = "setting.service" + +const DefaultPageSize = int64(500) +const DefaultQPS = float32(10) +const DefaultBurst = 25 + +const ( + ApiGroup = "setting.grafana.app" + apiVersion = "v0alpha1" + resource = "settings" + kind = "Setting" + listKind = "SettingList" +) + +var settingGroupVersion = schema.GroupVersionResource{ + Group: ApiGroup, + Version: apiVersion, + Resource: resource, +} + +var settingGroupListKind = map[schema.GroupVersionResource]string{ + settingGroupVersion: listKind, +} + +type remoteSettingServiceMetrics struct { + listDuration *prometheus.HistogramVec + listResultSize *prometheus.HistogramVec +} + +// Service retrieves configuration settings from a remote settings service. +// +// The service uses label selectors to filter settings. Settings are labeled with +// "section" and "key" labels matching their spec fields. +// +// Example - Select all settings: +// +// ctx := request.WithNamespace(context.Background(), "my-namespace") +// ini, err := service.ListAsIni(ctx, metav1.LabelSelector{}) +// +// Example - Select settings from specific sections: +// +// selector := metav1.LabelSelector{ +// MatchExpressions: []metav1.LabelSelectorRequirement{ +// { +// Key: "section", +// Operator: metav1.LabelSelectorOpIn, +// Values: []string{"database", "server"}, +// }, +// }, +// } +// ini, err := service.ListAsIni(ctx, selector) +// +// Example - Select settings from a single section with specific labels: +// +// selector := metav1.LabelSelector{ +// MatchLabels: map[string]string{ +// "section": "database", +// }, +// } +// settings, err := service.List(ctx, selector) +type Service interface { + prometheus.Collector + // ListAsIni retrieves settings filtered by a label selector from the namespace in context + // and returns them as an ini.File. + // + // The namespace must be present in the context, ie: via request.WithNamespace. + // An empty selector returns all settings in the namespace. + ListAsIni(ctx context.Context, selector metav1.LabelSelector) (*ini.File, error) + + // List retrieves settings filtered by a label selector from the namespace in context + // and returns them as a slice of Setting structs. + // + // The namespace must be present in the context, ie: via request.WithNamespace. + // An empty selector returns all settings in the namespace. + List(ctx context.Context, selector metav1.LabelSelector) ([]*Setting, error) +} + +type remoteSettingService struct { + dynamicClient dynamic.Interface + log logging.Logger + pageSize int64 + metrics remoteSettingServiceMetrics +} + +var _ Service = (*remoteSettingService)(nil) +var _ prometheus.Collector = (*remoteSettingService)(nil) + +// Config configures a Service. +type Config struct { + // URL is the base URL for the remote settings service (required). + URL string + // TokenExchangeClient authenticates requests (required if WrapTransport is not set). + TokenExchangeClient *authlib.TokenExchangeClient + // WrapTransport wraps the HTTP transport for authentication. + // Takes precedence over TokenExchangeClient when both are set. + // At least one of WrapTransport or TokenExchangeClient is required. + WrapTransport transport.WrapperFunc + // TLSClientConfig configures TLS for the client connection. + TLSClientConfig clientrest.TLSClientConfig + // QPS limits requests per second (defaults to DefaultQPS). + QPS float32 + // Burst allows request bursts above QPS (defaults to DefaultBurst). + Burst int + // PageSize sets the number of items per API page (defaults to DefaultPageSize). + PageSize int64 +} + +// Setting represents the parsed spec of a Setting resource. +type Setting struct { + // Setting section + Section string `json:"section"` + // Setting key + Key string `json:"key"` + // Setting value + Value string `json:"value"` +} + +// New creates a Service from the provided configuration. +func New(config Config) (Service, error) { + log := logging.New(LogPrefix) + dynamicClient, err := getDynamicClient(config, log) + if err != nil { + return nil, err + } + pageSize := DefaultPageSize + if config.PageSize > 0 { + pageSize = config.PageSize + } + + metrics := initMetrics() + + return &remoteSettingService{ + dynamicClient: dynamicClient, + pageSize: pageSize, + log: log, + metrics: metrics, + }, nil +} + +func (m *remoteSettingService) ListAsIni(ctx context.Context, labelSelector metav1.LabelSelector) (*ini.File, error) { + namespace, ok := request.NamespaceFrom(ctx) + ns := semconv.GrafanaNamespaceName(namespace) + ctx, span := tracer.Start(ctx, "remoteSettingService.ListAsIni", + trace.WithAttributes(ns)) + defer span.End() + + if !ok || namespace == "" { + return nil, tracing.Errorf(span, "missing namespace in context") + } + + settings, err := m.List(ctx, labelSelector) + if err != nil { + return nil, err + } + iniFile, err := m.toIni(settings) + if err != nil { + return nil, tracing.Error(span, err) + } + return iniFile, nil +} + +func (m *remoteSettingService) List(ctx context.Context, labelSelector metav1.LabelSelector) ([]*Setting, error) { + namespace, ok := request.NamespaceFrom(ctx) + ns := semconv.GrafanaNamespaceName(namespace) + ctx, span := tracer.Start(ctx, "remoteSettingService.List", + trace.WithAttributes(ns)) + defer span.End() + if !ok || namespace == "" { + return nil, tracing.Errorf(span, "missing namespace in context") + } + log := m.log.FromContext(ctx).New(ns.Key, ns.Value, "function", "remoteSettingService.List", "traceId", span.SpanContext().TraceID()) + + startTime := time.Now() + var status string + defer func() { + duration := time.Since(startTime).Seconds() + m.metrics.listDuration.WithLabelValues(status).Observe(duration) + }() + + selector, err := metav1.LabelSelectorAsSelector(&labelSelector) + if err != nil { + status = "error" + return nil, tracing.Error(span, err) + } + if selector.Empty() { + log.Debug("empty selector. Fetching all settings") + } + + var allSettings []*Setting + var continueToken string + hasNext := true + totalPages := 0 + // Using an upper limit to prevent infinite loops + for hasNext && totalPages < 1000 { + totalPages++ + opts := metav1.ListOptions{ + Limit: m.pageSize, + Continue: continueToken, + } + if !selector.Empty() { + opts.LabelSelector = selector.String() + } + + settingsList, lErr := m.dynamicClient.Resource(settingGroupVersion).Namespace(namespace).List(ctx, opts) + if lErr != nil { + status = "error" + return nil, tracing.Error(span, lErr) + } + for i := range settingsList.Items { + setting, pErr := parseSettingResource(&settingsList.Items[i]) + if pErr != nil { + status = "error" + return nil, tracing.Error(span, pErr) + } + allSettings = append(allSettings, setting) + } + continueToken = settingsList.GetContinue() + if continueToken == "" { + hasNext = false + } + } + + status = "success" + m.metrics.listResultSize.WithLabelValues(status).Observe(float64(len(allSettings))) + + return allSettings, nil +} + +func parseSettingResource(setting *unstructured.Unstructured) (*Setting, error) { + spec, found, err := unstructured.NestedMap(setting.Object, "spec") + if err != nil { + return nil, fmt.Errorf("failed to get spec from setting: %w", err) + } + if !found { + return nil, fmt.Errorf("spec not found in setting %s", setting.GetName()) + } + + var result Setting + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(spec, &result); err != nil { + return nil, fmt.Errorf("failed to convert spec to Setting: %w", err) + } + + return &result, nil +} + +func (m *remoteSettingService) toIni(settings []*Setting) (*ini.File, error) { + conf := ini.Empty() + for _, setting := range settings { + if !conf.HasSection(setting.Section) { + _, _ = conf.NewSection(setting.Section) + } + _, err := conf.Section(setting.Section).NewKey(setting.Key, setting.Value) + if err != nil { + return nil, err + } + } + return conf, nil +} + +func getDynamicClient(config Config, log logging.Logger) (dynamic.Interface, error) { + if config.URL == "" { + return nil, fmt.Errorf("URL cannot be empty") + } + if config.WrapTransport == nil && config.TokenExchangeClient == nil { + return nil, fmt.Errorf("must set either TokenExchangeClient or WrapTransport") + } + + wrapTransport := config.WrapTransport + if config.WrapTransport == nil { + log.Debug("using default wrapTransport with TokenExchangeClient") + wrapTransport = func(rt http.RoundTripper) http.RoundTripper { + return &authRoundTripper{ + tokenClient: config.TokenExchangeClient, + transport: rt, + } + } + } + + qps := DefaultQPS + if config.QPS > 0 { + qps = config.QPS + } + + burst := DefaultBurst + if config.Burst > 0 { + burst = config.Burst + } + + return dynamic.NewForConfig(&clientrest.Config{ + Host: config.URL, + WrapTransport: wrapTransport, + TLSClientConfig: config.TLSClientConfig, + QPS: qps, + Burst: burst, + }) +} + +// authRoundTripper wraps an HTTP transport with token-based authentication. +type authRoundTripper struct { + tokenClient *authlib.TokenExchangeClient + transport http.RoundTripper +} + +var _ http.RoundTripper = (*authRoundTripper)(nil) + +func (a *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + token, err := a.tokenClient.Exchange(req.Context(), authlib.TokenExchangeRequest{ + Audiences: []string{ApiGroup}, + Namespace: "*", + }) + if err != nil { + return nil, fmt.Errorf("failed to exchange token: %w", err) + } + req = utilnet.CloneRequest(req) + + req.Header.Set("X-Access-Token", fmt.Sprintf("Bearer %s", token.Token)) + return a.transport.RoundTrip(req) +} + +func initMetrics() remoteSettingServiceMetrics { + metrics := remoteSettingServiceMetrics{ + listDuration: prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "settings", + Subsystem: "service", + Name: "list_settings_duration_seconds", + Help: "Duration of remote settings service List operations", + NativeHistogramBucketFactor: 1.1, + }, + []string{"status"}, // status: "success" or "error" + ), + listResultSize: prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "settings", + Subsystem: "service", + Name: "list_settings_result_size", + Help: "Number of settings returned by remote settings service List operations", + NativeHistogramBucketFactor: 1.1, + }, + []string{"status"}, // status: "success" or "error" + ), + } + return metrics +} + +func (m *remoteSettingService) Describe(descs chan<- *prometheus.Desc) { + m.metrics.listDuration.Describe(descs) + m.metrics.listResultSize.Describe(descs) +} + +func (m *remoteSettingService) Collect(metrics chan<- prometheus.Metric) { + m.metrics.listDuration.Collect(metrics) + m.metrics.listResultSize.Collect(metrics) +} diff --git a/pkg/services/setting/service_test.go b/pkg/services/setting/service_test.go new file mode 100644 index 00000000000..949ccc73a86 --- /dev/null +++ b/pkg/services/setting/service_test.go @@ -0,0 +1,542 @@ +package setting + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/client-go/dynamic/fake" + k8testing "k8s.io/client-go/testing" + + authlib "github.com/grafana/authlib/authn" + "github.com/grafana/grafana/pkg/infra/log" +) + +func TestRemoteSettingService_ListAsIni(t *testing.T) { + t.Run("should filter settings by label selector", func(t *testing.T) { + // Create multiple settings, only some matching the selector + setting1 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "postgres"}) + setting2 := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"}) + setting3 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "host", Value: "localhost"}) + + client := newTestClient(500, setting1, setting2, setting3) + + // Create a selector that should match only database settings + selector := metav1.LabelSelector{ + MatchLabels: map[string]string{ + "section": "database", + }, + } + + ctx := request.WithNamespace(context.Background(), "test-namespace") + result, err := client.ListAsIni(ctx, selector) + + require.NoError(t, err) + assert.NotNil(t, result) + // Should only have database settings, not server settings + assert.True(t, result.HasSection("database")) + assert.Equal(t, "postgres", result.Section("database").Key("type").String()) + assert.Equal(t, "localhost", result.Section("database").Key("host").String()) + // Should NOT have server settings + assert.False(t, result.HasSection("server")) + }) + + t.Run("should return all settings with empty selector", func(t *testing.T) { + // Create multiple settings across different sections + setting1 := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"}) + setting2 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "mysql"}) + + client := newTestClient(500, setting1, setting2) + + // Empty selector should select everything + selector := metav1.LabelSelector{} + + ctx := request.WithNamespace(context.Background(), "test-namespace") + result, err := client.ListAsIni(ctx, selector) + + require.NoError(t, err) + assert.NotNil(t, result) + // Should have all settings from all sections + assert.True(t, result.HasSection("server")) + assert.Equal(t, "3000", result.Section("server").Key("port").String()) + assert.True(t, result.HasSection("database")) + assert.Equal(t, "mysql", result.Section("database").Key("type").String()) + }) +} + +func TestRemoteSettingService_List(t *testing.T) { + t.Run("should handle single page response", func(t *testing.T) { + setting := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"}) + + client := newTestClient(500, setting) + + ctx := request.WithNamespace(context.Background(), "test-namespace") + result, err := client.List(ctx, metav1.LabelSelector{}) + + require.NoError(t, err) + assert.Len(t, result, 1) + + spec := result[0] + assert.Equal(t, "server", spec.Section) + assert.Equal(t, "port", spec.Key) + assert.Equal(t, "3000", spec.Value) + }) + + t.Run("should handle multiple pages", func(t *testing.T) { + totalPages := 3 + pageSize := 5 + + pages := make([][]*unstructured.Unstructured, totalPages) + for pageNum := 0; pageNum < totalPages; pageNum++ { + for idx := 0; idx < pageSize; idx++ { + item := newUnstructuredSetting( + "test-namespace", + Setting{ + Section: fmt.Sprintf("section-%d", pageNum), + Key: fmt.Sprintf("key-%d", idx), + Value: fmt.Sprintf("val-%d-%d", pageNum, idx), + }, + ) + pages[pageNum] = append(pages[pageNum], item) + } + } + + scheme := runtime.NewScheme() + dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind) + listCallCount := 0 + dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) { + listCallCount++ + + continueToken := fmt.Sprintf("continue-%d", listCallCount) + if listCallCount == totalPages { + continueToken = "" + } + + if listCallCount <= totalPages { + list := &unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "apiVersion": ApiGroup + "/" + apiVersion, + "kind": listKind, + }, + } + list.SetContinue(continueToken) + for _, item := range pages[listCallCount-1] { + list.Items = append(list.Items, *item) + } + return true, list, nil + } + + return false, nil, nil + }) + + client := &remoteSettingService{ + dynamicClient: dynamicClient, + pageSize: int64(pageSize), + log: log.NewNopLogger(), + metrics: initMetrics(), + } + + ctx := request.WithNamespace(context.Background(), "test-namespace") + result, err := client.List(ctx, metav1.LabelSelector{}) + + require.NoError(t, err) + assert.Len(t, result, totalPages*pageSize) + assert.Equal(t, totalPages, listCallCount) + }) + + t.Run("should pass label selector when provided", func(t *testing.T) { + scheme := runtime.NewScheme() + dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind) + dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) { + listAction := action.(k8testing.ListActionImpl) + assert.Equal(t, "app=grafana", listAction.ListOptions.LabelSelector) + return true, &unstructured.UnstructuredList{}, nil + }) + + client := &remoteSettingService{ + dynamicClient: dynamicClient, + pageSize: 500, + log: log.NewNopLogger(), + metrics: initMetrics(), + } + + ctx := request.WithNamespace(context.Background(), "test-namespace") + _, err := client.List(ctx, metav1.LabelSelector{MatchLabels: map[string]string{"app": "grafana"}}) + + require.NoError(t, err) + }) + + t.Run("should stop pagination at 1000 pages", func(t *testing.T) { + scheme := runtime.NewScheme() + dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind) + listCallCount := 0 + dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) { + listCallCount++ + // Always return a continue token to simulate infinite pagination + list := &unstructured.UnstructuredList{} + list.SetContinue("continue-forever") + return true, list, nil + }) + + client := &remoteSettingService{ + dynamicClient: dynamicClient, + pageSize: 10, + log: log.NewNopLogger(), + metrics: initMetrics(), + } + + ctx := request.WithNamespace(context.Background(), "test-namespace") + _, err := client.List(ctx, metav1.LabelSelector{}) + + require.NoError(t, err) + assert.Equal(t, 1000, listCallCount, "Should stop at 1000 pages to prevent infinite loops") + }) + + t.Run("should return error when parsing setting fails", func(t *testing.T) { + scheme := runtime.NewScheme() + dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind) + dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) { + // Return a malformed setting without spec + list := &unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "apiVersion": ApiGroup + "/" + apiVersion, + "kind": listKind, + }, + } + malformedSetting := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": ApiGroup + "/" + apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "name": "malformed", + "namespace": "test-namespace", + }, + // Missing spec + }, + } + list.Items = append(list.Items, *malformedSetting) + return true, list, nil + }) + + client := &remoteSettingService{ + dynamicClient: dynamicClient, + pageSize: 500, + log: log.NewNopLogger(), + metrics: initMetrics(), + } + + ctx := request.WithNamespace(context.Background(), "test-namespace") + result, err := client.List(ctx, metav1.LabelSelector{}) + + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "spec not found") + }) +} + +func TestParseSettingResource(t *testing.T) { + t.Run("should parse valid setting resource", func(t *testing.T) { + setting := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "postgres"}) + + result, err := parseSettingResource(setting) + + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "database", result.Section) + assert.Equal(t, "type", result.Key) + assert.Equal(t, "postgres", result.Value) + }) + + t.Run("should return error when spec is missing", func(t *testing.T) { + setting := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": ApiGroup + "/" + apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "name": "test-setting", + "namespace": "test-namespace", + }, + // No spec + }, + } + + result, err := parseSettingResource(setting) + + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "spec not found") + }) +} + +func TestRemoteSettingService_ToIni(t *testing.T) { + t.Run("should convert settings to ini format", func(t *testing.T) { + settings := []*Setting{ + {Section: "database", Key: "type", Value: "postgres"}, + {Section: "database", Key: "host", Value: "localhost"}, + {Section: "server", Key: "http_port", Value: "3000"}, + } + + client := &remoteSettingService{ + pageSize: 500, + log: log.NewNopLogger(), + } + + result, err := client.toIni(settings) + + require.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.HasSection("database")) + assert.True(t, result.HasSection("server")) + assert.Equal(t, "postgres", result.Section("database").Key("type").String()) + assert.Equal(t, "localhost", result.Section("database").Key("host").String()) + assert.Equal(t, "3000", result.Section("server").Key("http_port").String()) + }) + + t.Run("should handle empty settings list", func(t *testing.T) { + var settings []*Setting + + client := &remoteSettingService{ + pageSize: 500, + log: log.NewNopLogger(), + } + + result, err := client.toIni(settings) + + require.NoError(t, err) + assert.NotNil(t, result) + sections := result.Sections() + assert.Len(t, sections, 1) // Only default section + }) + + t.Run("should create section if it does not exist", func(t *testing.T) { + settings := []*Setting{ + {Section: "new_section", Key: "new_key", Value: "new_value"}, + } + + client := &remoteSettingService{ + pageSize: 500, + log: log.NewNopLogger(), + } + + result, err := client.toIni(settings) + + require.NoError(t, err) + assert.True(t, result.HasSection("new_section")) + assert.Equal(t, "new_value", result.Section("new_section").Key("new_key").String()) + }) + + t.Run("should handle multiple keys in same section", func(t *testing.T) { + settings := []*Setting{ + {Section: "auth", Key: "disable_login_form", Value: "false"}, + {Section: "auth", Key: "disable_signout_menu", Value: "true"}, + } + + client := &remoteSettingService{ + pageSize: 500, + log: log.NewNopLogger(), + } + + result, err := client.toIni(settings) + + require.NoError(t, err) + assert.True(t, result.HasSection("auth")) + authSection := result.Section("auth") + assert.Equal(t, "false", authSection.Key("disable_login_form").String()) + assert.Equal(t, "true", authSection.Key("disable_signout_menu").String()) + }) +} + +func TestNew(t *testing.T) { + t.Run("should create client with default page size", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + } + + client, err := New(config) + + require.NoError(t, err) + assert.NotNil(t, client) + remoteClient := client.(*remoteSettingService) + assert.Equal(t, DefaultPageSize, remoteClient.pageSize) + }) + + t.Run("should create client with custom page size", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + PageSize: 100, + } + + client, err := New(config) + + require.NoError(t, err) + assert.NotNil(t, client) + remoteClient := client.(*remoteSettingService) + assert.Equal(t, int64(100), remoteClient.pageSize) + }) + + t.Run("should use default page size when zero is provided", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + PageSize: 0, + } + + client, err := New(config) + + require.NoError(t, err) + assert.NotNil(t, client) + remoteClient := client.(*remoteSettingService) + assert.Equal(t, DefaultPageSize, remoteClient.pageSize) + }) + + t.Run("should return error when config is invalid", func(t *testing.T) { + config := Config{ + URL: "", // Invalid: empty URL + } + + client, err := New(config) + + require.Error(t, err) + assert.Nil(t, client) + assert.Contains(t, err.Error(), "URL cannot be empty") + }) +} + +func TestGetDynamicClient(t *testing.T) { + logger := log.NewNopLogger() + + t.Run("should return error when SettingServiceURL is empty", func(t *testing.T) { + config := Config{ + URL: "", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + } + + client, err := getDynamicClient(config, logger) + + require.Error(t, err) + assert.Nil(t, client) + assert.Contains(t, err.Error(), "URL cannot be empty") + }) + + t.Run("should return error when both TokenExchangeClient and WrapTransport are nil", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + TokenExchangeClient: nil, + WrapTransport: nil, + } + + client, err := getDynamicClient(config, logger) + + require.Error(t, err) + assert.Nil(t, client) + assert.Contains(t, err.Error(), "must set either TokenExchangeClient or WrapTransport") + }) + + t.Run("should create client with WrapTransport", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + } + + client, err := getDynamicClient(config, logger) + + require.NoError(t, err) + assert.NotNil(t, client) + }) + + t.Run("should not fail when QPS and Burst are not provided", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + } + + client, err := getDynamicClient(config, logger) + + require.NoError(t, err) + assert.NotNil(t, client) + }) + + t.Run("should not fail when custom QPS and Burst are provided", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + QPS: 10.0, + Burst: 20, + } + + client, err := getDynamicClient(config, logger) + + require.NoError(t, err) + assert.NotNil(t, client) + }) + + t.Run("should use WrapTransport when both WrapTransport and TokenExchangeClient are provided", func(t *testing.T) { + wrapTransportCalled := false + tokenExchangeClient := &authlib.TokenExchangeClient{} + + config := Config{ + URL: "https://example.com", + TokenExchangeClient: tokenExchangeClient, + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { + wrapTransportCalled = true + return rt + }, + } + + client, err := getDynamicClient(config, logger) + + require.NoError(t, err) + assert.NotNil(t, client) + assert.True(t, wrapTransportCalled, "WrapTransport should be called and take precedence over TokenExchangeClient") + }) +} + +// Helper function to create an unstructured Setting object for tests +func newUnstructuredSetting(namespace string, spec Setting) *unstructured.Unstructured { + // Generate resource name in the format {section}--{key} + name := fmt.Sprintf("%s--%s", spec.Section, spec.Key) + + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": ApiGroup + "/" + apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + }, + "spec": map[string]interface{}{ + "section": spec.Section, + "key": spec.Key, + "value": spec.Value, + }, + }, + } + // Always set section and key labels + obj.SetLabels(map[string]string{ + "section": spec.Section, + "key": spec.Key, + }) + return obj +} + +// Helper function to create a test client with the dynamic fake client +func newTestClient(pageSize int64, objects ...runtime.Object) *remoteSettingService { + scheme := runtime.NewScheme() + dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind, objects...) + + return &remoteSettingService{ + dynamicClient: dynamicClient, + pageSize: pageSize, + log: log.NewNopLogger(), + metrics: initMetrics(), + } +} From 4130bd9cd300ec7ce0fb492b3a7229968c9167b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Sencer=20=C3=96zcan?= <32759850+mustafasencer@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:15:24 +0100 Subject: [PATCH 137/378] Revert "K8s: read resource configs from API Enablement for API Builders" (#114475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "K8s: read resource configs from API Enablement for API Builders (#114…" This reverts commit 0c2707bbc41b60124d2217547f607e08a2a3f013. --- pkg/registry/apis/ofrep/register.go | 4 + pkg/services/apiserver/builder/helper.go | 106 +++++------------- pkg/services/apiserver/builder/openapi.go | 19 +--- .../apiserver/builder/request_handler.go | 10 +- pkg/services/apiserver/service.go | 8 +- 5 files changed, 36 insertions(+), 111 deletions(-) diff --git a/pkg/registry/apis/ofrep/register.go b/pkg/registry/apis/ofrep/register.go index d6a443cfee7..cd6f8f0eb29 100644 --- a/pkg/registry/apis/ofrep/register.go +++ b/pkg/registry/apis/ofrep/register.go @@ -64,6 +64,10 @@ func NewAPIBuilder(providerType string, url *url.URL, insecure bool, caFile stri } func RegisterAPIService(apiregistration builder.APIRegistrar, cfg *setting.Cfg) (*APIBuilder, error) { + if !cfg.OpenFeature.APIEnabled { + return nil, nil + } + var staticEvaluator featuremgmt.StaticFlagEvaluator // No static evaluator needed for non-static provider var err error if cfg.OpenFeature.ProviderType == setting.StaticProviderType { diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index ae90295c01d..603c49f3cb2 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -22,7 +22,6 @@ import ( k8srequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/generic" genericapiserver "k8s.io/apiserver/pkg/server" - serverstorage "k8s.io/apiserver/pkg/server/storage" "k8s.io/apiserver/pkg/util/openapi" k8sscheme "k8s.io/client-go/kubernetes/scheme" k8stracing "k8s.io/component-base/tracing" @@ -73,36 +72,13 @@ var PathRewriters = []filters.PathRewriter{ }, } -// GetDefaultBuildHandlerChainFuncForAggregator is a replica of GetDefaultBuildHandlerChainFunc except it skips custom routes handling -func GetDefaultBuildHandlerChainFuncForAggregator() BuildHandlerChainFunc { - return func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler { - // filters.WithRequester needs to be after the K8s chain because it depends on the K8s user in context - handler := filters.WithRequester(delegateHandler) - - // Call DefaultBuildHandlerChain on the main entrypoint http.Handler - // See https://github.com/kubernetes/apiserver/blob/v0.28.0/pkg/server/config.go#L906 - // DefaultBuildHandlerChain provides many things, notably CORS, HSTS, cache-control, authz and latency tracking - handler = genericapiserver.DefaultBuildHandlerChain(handler, c) - - handler = filters.WithAcceptHeader(handler) - handler = filters.WithPathRewriters(handler, PathRewriters) - handler = k8stracing.WithTracing(handler, c.TracerProvider, "KubernetesAPI") - handler = filters.WithExtractJaegerTrace(handler) - // Configure filters.WithPanicRecovery to not crash on panic - utilruntime.ReallyCrash = false - - return handler - } -} func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus.Registerer) BuildHandlerChainFunc { return func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler { requestHandler, err := GetCustomRoutesHandler( delegateHandler, c.LoopbackClientConfig, builders, - reg, - c.MergedResourceConfig, - ) + reg) if err != nil { panic(fmt.Sprintf("could not build the request handler for specified API builders: %s", err.Error())) } @@ -129,8 +105,6 @@ func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus. } } -// SetupConfig sets up the server config for the API server -// specify isAggregator=true, if the chain is being constructed for kube-aggregator func SetupConfig( scheme *runtime.Scheme, serverConfig *genericapiserver.RecommendedConfig, @@ -140,7 +114,6 @@ func SetupConfig( gvs []schema.GroupVersion, additionalOpenAPIDefGetters []common.GetOpenAPIDefinitions, reg prometheus.Registerer, - apiResourceConfig *serverstorage.ResourceConfig, ) error { serverConfig.AdmissionControl = NewAdmissionFromBuilders(builders) defsGetter := GetOpenAPIDefinitions(builders, additionalOpenAPIDefGetters...) @@ -153,7 +126,7 @@ func SetupConfig( openapinamer.NewDefinitionNamer(scheme, k8sscheme.Scheme)) // Add the custom routes to service discovery - serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs, apiResourceConfig) + serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs) serverConfig.OpenAPIV3Config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) { meta := r.Metadata() kind := "" @@ -314,7 +287,6 @@ func InstallAPIs( features featuremgmt.FeatureToggles, dualWriterMetrics *grafanarest.DualWriterMetrics, builderMetrics *BuilderMetrics, - apiResourceConfig *serverstorage.ResourceConfig, ) error { // dual writing is only enabled when the storage type is not legacy. // this is needed to support setting a default RESTOptionsGetter for new APIs that don't @@ -429,9 +401,34 @@ func InstallAPIs( for group, buildersForGroup := range buildersGroupMap { g := genericapiserver.NewDefaultAPIGroupInfo(group, scheme, metav1.ParameterCodec, codecs) for _, b := range buildersForGroup { - if err := installAPIGroupsForBuilder(&g, group, b, apiResourceConfig, scheme, optsGetter, dualWrite, reg, optsregister, storageOpts, features); err != nil { + if err := b.UpdateAPIGroupInfo(&g, APIGroupOptions{ + Scheme: scheme, + OptsGetter: optsGetter, + DualWriteBuilder: dualWrite, + MetricsRegister: reg, + StorageOptsRegister: optsregister, + StorageOpts: storageOpts, + }); err != nil { return err } + if len(g.PrioritizedVersions) < 1 { + continue + } + + // if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed + //nolint:staticcheck // not yet migrated to OpenFeature + if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { + if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok { + for name := range resources { + if !allowRegisteringResourceByInfo(b.AllowedV0Alpha1Resources(), name) { + delete(resources, name) + } + } + if len(resources) == 0 { + delete(g.VersionedResourcesStorageMap, "v0alpha1") + } + } + } } // skip installing the group if there are no resources left after filtering @@ -448,53 +445,6 @@ func InstallAPIs( return nil } -func installAPIGroupsForBuilder(g *genericapiserver.APIGroupInfo, group string, b APIGroupBuilder, apiResourceConfig *serverstorage.ResourceConfig, scheme *runtime.Scheme, - optsGetter generic.RESTOptionsGetter, dualWrite grafanarest.DualWriteBuilder, reg prometheus.Registerer, optsregister apistore.StorageOptionsRegister, - storageOpts *options.StorageOptions, features featuremgmt.FeatureToggles) error { - if err := b.UpdateAPIGroupInfo(g, APIGroupOptions{ - Scheme: scheme, - OptsGetter: optsGetter, - DualWriteBuilder: dualWrite, - MetricsRegister: reg, - StorageOptsRegister: optsregister, - StorageOpts: storageOpts, - }); err != nil { - return err - } - if len(g.PrioritizedVersions) < 1 { - return nil - } - - // filter out api groups that are disabled in APIEnablementOptions - for version := range g.VersionedResourcesStorageMap { - gvr := schema.GroupVersionResource{ - Group: group, - Version: version, - } - if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) { - klog.InfoS("Skipping storage for disabled resource", "gvr", gvr.String()) - delete(g.VersionedResourcesStorageMap, version) - } - } - - // if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed - //nolint:staticcheck // not yet migrated to OpenFeature - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok { - for name := range resources { - if !allowRegisteringResourceByInfo(b.AllowedV0Alpha1Resources(), name) { - delete(resources, name) - } - } - if len(resources) == 0 { - delete(g.VersionedResourcesStorageMap, "v0alpha1") - } - } - } - - return nil -} - // AddPostStartHooks adds post start hooks to a generic API server config func AddPostStartHooks( config *genericapiserver.RecommendedConfig, diff --git a/pkg/services/apiserver/builder/openapi.go b/pkg/services/apiserver/builder/openapi.go index 6d3b33f4baf..9cb7cc17f7c 100644 --- a/pkg/services/apiserver/builder/openapi.go +++ b/pkg/services/apiserver/builder/openapi.go @@ -9,8 +9,6 @@ import ( apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime/schema" - serverstorage "k8s.io/apiserver/pkg/server/storage" - "k8s.io/klog/v2" openapi "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" spec "k8s.io/kube-openapi/pkg/validation/spec" @@ -78,7 +76,6 @@ func addBuilderRoutes( targetGroupVersion schema.GroupVersion, openAPISpec *spec3.OpenAPI, apiGroupBuilders []APIGroupBuilder, - apiResourceConfig *serverstorage.ResourceConfig, ) (*spec3.OpenAPI, error) { for _, apiGroupBuilder := range apiGroupBuilders { // Optionally include raw http handlers for all builders @@ -110,24 +107,12 @@ func addBuilderRoutes( } } } - - // filter out api groups that are disabled in APIEnablementOptions - for path := range openAPISpec.Paths.Paths { - if strings.HasPrefix(path, "/apis/"+targetGroupVersion.String()+"/") { - gv := targetGroupVersion.WithResource("") - if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gv) { - klog.InfoS("removing openapi routes for disabled resource", "gv", gv.String()) - delete(openAPISpec.Paths.Paths, path) - } - } - } - return openAPISpec, nil } // Modify the OpenAPI spec to include the additional routes. // nolint:gocyclo -func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion, apiResourceConfig *serverstorage.ResourceConfig) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) { +func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) { return func(s *spec3.OpenAPI) (*spec3.OpenAPI, error) { if s.Paths == nil { return s, nil @@ -242,7 +227,7 @@ func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []s } } } - return addBuilderRoutes(gv, ©, builders, apiResourceConfig) + return addBuilderRoutes(gv, ©, builders) } } return s, nil diff --git a/pkg/services/apiserver/builder/request_handler.go b/pkg/services/apiserver/builder/request_handler.go index 50761f1d42c..80fbf6d0eaa 100644 --- a/pkg/services/apiserver/builder/request_handler.go +++ b/pkg/services/apiserver/builder/request_handler.go @@ -6,9 +6,7 @@ import ( "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" - serverstorage "k8s.io/apiserver/pkg/server/storage" restclient "k8s.io/client-go/rest" - klog "k8s.io/klog/v2" "k8s.io/kube-openapi/pkg/spec3" ) @@ -16,7 +14,7 @@ type requestHandler struct { router *mux.Router } -func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer, apiResourceConfig *serverstorage.ResourceConfig) (http.Handler, error) { +func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer) (http.Handler, error) { useful := false // only true if any routes exist anywhere router := mux.NewRouter() @@ -29,12 +27,6 @@ func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient } for _, gv := range GetGroupVersions(builder) { - // filter out api groups that are disabled in APIEnablementOptions - gvr := gv.WithResource("") - if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) { - klog.InfoS("Skipping custom route handler for disabled group version", "gv", gv.String()) - continue - } routes := provider.GetAPIRoutes(gv) if routes == nil { continue diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 605016c112d..28da416536c 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -316,11 +316,7 @@ func (s *service) start(ctx context.Context) error { s.cfg.BuildBranch, ) - apiResourceConfig := appinstaller.NewAPIResourceConfig(s.appInstallers) - // add the builder group versions to the api resource config - apiResourceConfig.EnableVersions(groupVersions...) - - if err := o.APIEnablementOptions.ApplyTo(&serverConfig.Config, apiResourceConfig, s.scheme); err != nil { + if err := o.APIEnablementOptions.ApplyTo(&serverConfig.Config, appinstaller.NewAPIResourceConfig(s.appInstallers), s.scheme); err != nil { return err } @@ -363,7 +359,6 @@ func (s *service) start(ctx context.Context) error { groupVersions, defGetters, s.metrics, - apiResourceConfig, ) if err != nil { return err @@ -405,7 +400,6 @@ func (s *service) start(ctx context.Context) error { s.features, s.dualWriterMetrics, s.builderMetrics, - apiResourceConfig, ) if err != nil { return err From 51d562eb813503bcb4327728fe358d20daa8df45 Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Wed, 26 Nov 2025 09:21:12 -0600 Subject: [PATCH 138/378] Docs: Clarify some language in migration assistant docs (#114440) Co-authored-by: Irene Rodriguez --- .../migration-guide/cloud-migration-assistant.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/administration/migration-guide/cloud-migration-assistant.md b/docs/sources/administration/migration-guide/cloud-migration-assistant.md index 439e04c282d..b4a1e5b89fb 100644 --- a/docs/sources/administration/migration-guide/cloud-migration-assistant.md +++ b/docs/sources/administration/migration-guide/cloud-migration-assistant.md @@ -14,10 +14,10 @@ weight: 400 The Grafana Cloud Migration Assistant, generally available from Grafana v12.0, automatically migrates resources from your Grafana OSS/Enterprise instance to Grafana Cloud. It provides the following functionality: - Securely connect your self-managed instance to a Grafana Cloud instance. -- Seamlessly migrate resources such as dashboards, data sources, and folders to your cloud instance in a few easy steps. +- Migrate resources such as dashboards, data sources, and folders to your cloud instance in a few easy steps. - View the migration status of your resources in real-time. -Some of the benefits of the migration assistant are: +Some benefits of the migration assistant are: Ease of use : Follow the steps provided by the UI to easily migrate all your resources to Grafana Cloud without using Grafana APIs or scripts. @@ -44,7 +44,7 @@ The following resources are supported by the migration assistant: To use the Grafana migration assistant, you need: -- Grafana v11.2 or above with the `onPremToCloudMigrations` feature toggle enabled. In Grafana 11.5, this is enabled by default. For more information on how to enable a feature toggle, refer to [Configure feature toggles](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/#configure-feature-toggles). +- A self-managed Grafana instance version v11.2 or above with the `onPremToCloudMigrations` feature toggle enabled. In Grafana 11.5, this is enabled by default. For more information on how to enable a feature toggle, refer to [Configure feature toggles](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/#configure-feature-toggles). - A [Grafana Cloud Stack](https://grafana.com/docs/grafana-cloud/get-started/) you intend to migrate your resources to. - [`Admin`](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/cloud-roles/) access to the Grafana Cloud Stack. To check your access level, go to `https://grafana.com/orgs//members`. - [Grafana server administrator](https://grafana.com/docs/grafana//administration/roles-and-permissions/#grafana-server-administrators) access to your existing Grafana OSS/Enterprise instance. To check your access level, go to `https:///admin/users`. @@ -64,7 +64,7 @@ In Grafana Enterprise, the server administrator has access to the migration assi ### Grant access in Grafana Enterprise -{{< admonition type="important">}} +{{< admonition type="note" >}} You must [configure RBAC](https://grafana.com/docs/grafana//administration/roles-and-permissions/access-control/configure-rbac/) before you can grant other administrators access to the Grafana Migration Assistant. {{< /admonition >}} From ca8cad68c8764a8f6653900de21dc39024b6d40f Mon Sep 17 00:00:00 2001 From: beejeebus Date: Tue, 25 Nov 2025 16:47:04 +0000 Subject: [PATCH 139/378] Add a metric to track usage of datasource configuration CRUD This PR adds `ds_config_handler_requests_duration_seconds` metric to help us track the release of the new datasource configuration CRUD api. Fixes https://github.com/grafana/grafana-enterprise/issues/10309 --- pkg/api/datasources.go | 21 +++++++++ pkg/api/datasources_test.go | 23 +++++++++ pkg/api/http_server.go | 49 +++++++++++--------- pkg/registry/apis/datasource/legacy_store.go | 28 ++++++++++- pkg/registry/apis/datasource/register.go | 6 +++ 5 files changed, 104 insertions(+), 23 deletions(-) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index cba7f5bc202..d7dc89d248f 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -9,6 +9,7 @@ import ( "sort" "strconv" "strings" + "time" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -18,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/setting" @@ -200,6 +202,11 @@ func (hs *HTTPServer) DeleteDataSourceById(c *contextmodel.ReqContext) response. // 404: notFoundError // 500: internalServerError func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Response { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "GetDataSourceByUID"), time.Since(start).Seconds()) + }() + ds, err := hs.getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.GetOrgID()) if err != nil { @@ -231,6 +238,11 @@ func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Re // 404: notFoundError // 500: internalServerError func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response.Response { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "DeleteDataSourceByUID"), time.Since(start).Seconds()) + }() + uid := web.Params(c.Req)[":uid"] if uid == "" { @@ -361,6 +373,11 @@ func validateJSONData(jsonData *simplejson.Json, cfg *setting.Cfg) error { // 409: conflictError // 500: internalServerError func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Response { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "AddDataSource"), time.Since(start).Seconds()) + }() + cmd := datasources.AddDataSourceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -478,6 +495,10 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response. // 409: conflictError // 500: internalServerError func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response.Response { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "UpdateDataSourceByUID"), time.Since(start).Seconds()) + }() cmd := datasources.UpdateDataSourceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index 517dc6e9048..e2f951b0a37 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db/dbtest" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" @@ -81,6 +83,19 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) { }, mockSQLStore) } +// setupDsConfigMetrics creates and registers the prometheus metrics needed for HTTPServer tests +// that call methods using dsConfigHandlerRequestsDuration. +func setupDsConfigHandlerMetrics() (prometheus.Registerer, *prometheus.HistogramVec) { + promRegister := prometheus.NewRegistry() + dsConfigHandlerRequestsDuration := metricutil.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "grafana", + Name: "ds_config_handler_requests_duration_seconds", + Help: "Duration of requests handled by datasource configuration handlers", + }, []string{"code_path", "handler"}) + promRegister.MustRegister(dsConfigHandlerRequestsDuration) + return promRegister, dsConfigHandlerRequestsDuration +} + // Adding data sources with invalid URLs should lead to an error. func TestAddDataSource_InvalidURL(t *testing.T) { sc := setupScenarioContext(t, "/api/datasources") @@ -88,6 +103,7 @@ func TestAddDataSource_InvalidURL(t *testing.T) { DataSourcesService: &dataSourcesServiceMock{}, Cfg: setting.NewCfg(), } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ @@ -118,6 +134,7 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) { AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), accesscontrolService: actest.FakeService{}, } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc := setupScenarioContext(t, "/api/datasources") @@ -143,6 +160,7 @@ func TestAddDataSource_InvalidJSONData(t *testing.T) { DataSourcesService: &dataSourcesServiceMock{}, Cfg: setting.NewCfg(), } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc := setupScenarioContext(t, "/api/datasources") @@ -175,6 +193,7 @@ func TestUpdateDataSource_InvalidURL(t *testing.T) { DataSourcesService: &dataSourcesServiceMock{}, Cfg: setting.NewCfg(), } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc := setupScenarioContext(t, "/api/datasources/1234") sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { @@ -199,6 +218,7 @@ func TestUpdateDataSource_InvalidJSONData(t *testing.T) { DataSourcesService: &dataSourcesServiceMock{}, Cfg: setting.NewCfg(), } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc := setupScenarioContext(t, "/api/datasources/1234") hs.Cfg.AuthProxy.Enabled = true @@ -236,6 +256,7 @@ func TestAddDataSourceTeamHTTPHeaders(t *testing.T) { ExpectedErr: nil, }, } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc := setupScenarioContext(t, fmt.Sprintf("/api/datasources/%s", tenantID)) hs.Cfg.AuthProxy.Enabled = true @@ -289,6 +310,7 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) { AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), accesscontrolService: actest.FakeService{}, } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc := setupScenarioContext(t, "/api/datasources/1234") @@ -429,6 +451,7 @@ func TestAPI_datasources_AccessControl(t *testing.T) { hs.DataSourcesService = &dataSourcesServiceMock{expectedDatasource: &datasources.DataSource{}} hs.accesscontrolService = actest.FakeService{} hs.Live = newTestLive(t, hs.SQLStore) + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() }) for _, url := range tt.urls { diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index fa0b407aa2b..f2ac32a80c6 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -203,27 +203,28 @@ type HTTPServer struct { pluginsCDNService *pluginscdn.Service managedPluginsService managedplugins.Manager - userService user.Service - tempUserService tempUser.Service - loginAttemptService loginAttempt.Service - orgService org.Service - orgDeletionService org.DeletionService - TeamService team.Service - accesscontrolService accesscontrol.Service - annotationsRepo annotations.Repository - tagService tag.Service - oauthTokenService oauthtoken.OAuthTokenService - statsService stats.Service - authnService authn.Service - starApi *starApi.API - promRegister prometheus.Registerer - promGatherer prometheus.Gatherer - clientConfigProvider grafanaapiserver.DirectRestConfigProvider - namespacer request.NamespaceMapper - anonService anonymous.Service - userVerifier user.Verifier - tlsCerts TLSCerts - htmlHandlerRequestsDuration *prometheus.HistogramVec + userService user.Service + tempUserService tempUser.Service + loginAttemptService loginAttempt.Service + orgService org.Service + orgDeletionService org.DeletionService + TeamService team.Service + accesscontrolService accesscontrol.Service + annotationsRepo annotations.Repository + tagService tag.Service + oauthTokenService oauthtoken.OAuthTokenService + statsService stats.Service + authnService authn.Service + starApi *starApi.API + promRegister prometheus.Registerer + promGatherer prometheus.Gatherer + clientConfigProvider grafanaapiserver.DirectRestConfigProvider + namespacer request.NamespaceMapper + anonService anonymous.Service + userVerifier user.Verifier + tlsCerts TLSCerts + htmlHandlerRequestsDuration *prometheus.HistogramVec + dsConfigHandlerRequestsDuration *prometheus.HistogramVec } type TLSCerts struct { @@ -382,9 +383,15 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi Name: "html_handler_requests_duration_seconds", Help: "Duration of requests handled by the index.go HTML handler", }, []string{"handler"}), + dsConfigHandlerRequestsDuration: metricutil.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "grafana", + Name: "ds_config_handler_requests_duration_seconds", + Help: "Duration of requests handled by datasource configuration handlers", + }, []string{"code_path", "handler"}), } promRegister.MustRegister(hs.htmlHandlerRequestsDuration) + promRegister.MustRegister(hs.dsConfigHandlerRequestsDuration) if hs.Listener != nil { hs.log.Debug("Using provided listener") diff --git a/pkg/registry/apis/datasource/legacy_store.go b/pkg/registry/apis/datasource/legacy_store.go index 8786dc8d86e..aea7ed1daa3 100644 --- a/pkg/registry/apis/datasource/legacy_store.go +++ b/pkg/registry/apis/datasource/legacy_store.go @@ -3,7 +3,9 @@ package datasource import ( "context" "fmt" + "time" + "github.com/prometheus/client_golang/prometheus" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -11,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" ) var ( @@ -26,8 +29,9 @@ var ( ) type legacyStorage struct { - datasources PluginDatasourceProvider - resourceInfo *utils.ResourceInfo + datasources PluginDatasourceProvider + resourceInfo *utils.ResourceInfo + dsConfigHandlerRequestsDuration *prometheus.HistogramVec } func (s *legacyStorage) New() runtime.Object { @@ -57,11 +61,21 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO } func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Get"), time.Since(start).Seconds()) + }() + return s.datasources.GetDataSource(ctx, name) } // Create implements rest.Creater. func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) + }() + ds, ok := obj.(*v0alpha1.DataSource) if !ok { return nil, fmt.Errorf("expected a datasource object") @@ -71,6 +85,11 @@ func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createVa // Update implements rest.Updater. func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) + }() + old, err := s.Get(ctx, name, &metav1.GetOptions{}) if err != nil { return nil, false, err @@ -107,6 +126,11 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up // Delete implements rest.GracefulDeleter. func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) + }() + err := s.datasources.DeleteDataSource(ctx, name) return nil, false, err } diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index 9feb46a33d4..bec8fdaec5b 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -20,6 +20,7 @@ import ( datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/promlib/models" @@ -218,6 +219,11 @@ func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver legacyStore := &legacyStorage{ datasources: b.datasources, resourceInfo: &ds, + dsConfigHandlerRequestsDuration: metricutil.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "grafana", + Name: "ds_config_handler_requests_duration_seconds", + Help: "Duration of requests handled by datasource configuration handlers", + }, []string{"code_path", "handler"}), } unified, err := grafanaregistry.NewRegistryStore(opts.Scheme, ds, opts.OptsGetter) if err != nil { From ebaccc781b62ac664b010ec2f7c3e91c337891c7 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 26 Nov 2025 08:30:38 -0800 Subject: [PATCH 140/378] Suggestions: Update all suggestions suppliers to be functions (#113986) * Suggestions: Convert panels to use function supplier * rework deaggregation * BarGauge * cleanup and make consistent the deaggregation in suggestions * Candlestick * Implement timeseries and clean up some things that can already be deleted * spotted some typos in self-review * restore PanelDataSummary deprecated fields, we wont delete till Grafana 13 * change deprecation message * remove some unused imports * run prettier * update radialbar defaults logic * update tests and logic to DRY up the reduceOptions a bit and more thoroughly test the output * Trend: Improve suggestions * updates from review * add unique DataFrameType list to PanelDataSummary * add histogram suggestions * rework panelDataSummary to be a class, change some things * further boil down PanelDataSummary * Improve FlameGgraph suggestions * geomap and other defaults * reorder the single frame with string and number test --- .../grafana-data/src/panel/PanelPlugin.ts | 5 + .../panel/suggestions/getPanelDataSummary.ts | 162 +++++++--- .../transformers/calculateField.ts | 6 +- .../grafana-data/src/types/suggestions.ts | 2 + public/app/features/geo/utils/location.ts | 10 + .../panel/components/PanelDataErrorView.tsx | 7 +- .../suggestions/getAllSuggestions.test.ts | 255 ++++++++++----- .../panel/suggestions/getAllSuggestions.ts | 33 +- .../app/features/panel/suggestions/utils.ts | 45 ++- public/app/plugins/panel/barchart/module.tsx | 4 +- .../app/plugins/panel/barchart/suggestions.ts | 161 +++++----- public/app/plugins/panel/bargauge/module.tsx | 4 +- .../app/plugins/panel/bargauge/suggestions.ts | 157 +++------ .../app/plugins/panel/candlestick/module.tsx | 4 +- .../plugins/panel/candlestick/suggestions.ts | 71 ++--- .../panel/flamegraph/FlameGraphPanel.tsx | 4 +- .../app/plugins/panel/flamegraph/module.tsx | 23 +- .../plugins/panel/flamegraph/suggestions.ts | 31 -- public/app/plugins/panel/flamegraph/types.ts | 3 + public/app/plugins/panel/gauge/module.tsx | 4 +- public/app/plugins/panel/gauge/suggestions.ts | 136 ++++---- public/app/plugins/panel/geomap/module.tsx | 4 +- .../app/plugins/panel/geomap/suggestions.ts | 46 +++ public/app/plugins/panel/heatmap/module.tsx | 4 +- .../plugins/panel/heatmap/suggestions.test.ts | 226 +++++++++++++ .../app/plugins/panel/heatmap/suggestions.ts | 102 +++--- public/app/plugins/panel/histogram/module.tsx | 15 + public/app/plugins/panel/logs/module.tsx | 8 +- public/app/plugins/panel/logs/suggestions.ts | 33 -- public/app/plugins/panel/nodeGraph/module.tsx | 4 +- .../plugins/panel/nodeGraph/suggestions.ts | 119 ++++--- public/app/plugins/panel/piechart/module.tsx | 4 +- .../app/plugins/panel/piechart/suggestions.ts | 133 ++++---- public/app/plugins/panel/radialbar/module.tsx | 4 +- .../panel/radialbar/suggestions.test.ts | 18 +- .../plugins/panel/radialbar/suggestions.ts | 112 +++---- public/app/plugins/panel/stat/module.tsx | 4 +- public/app/plugins/panel/stat/suggestions.ts | 127 ++++---- .../plugins/panel/state-timeline/module.tsx | 2 +- .../plugins/panel/status-history/module.tsx | 42 ++- .../panel/status-history/suggestions.ts | 56 ---- public/app/plugins/panel/table/module.tsx | 4 +- public/app/plugins/panel/table/suggestions.ts | 61 ++-- .../app/plugins/panel/timeseries/module.tsx | 4 +- .../plugins/panel/timeseries/suggestions.ts | 297 +++++++----------- public/app/plugins/panel/traces/module.tsx | 4 +- .../app/plugins/panel/traces/suggestions.ts | 29 -- public/app/plugins/panel/trend/TrendPanel.tsx | 50 +-- public/app/plugins/panel/trend/module.tsx | 51 ++- public/app/plugins/panel/trend/suggestions.ts | 43 --- public/app/plugins/panel/trend/utils.ts | 48 +++ public/app/types/suggestions.ts | 34 -- public/locales/en-US/grafana.json | 33 ++ 53 files changed, 1573 insertions(+), 1275 deletions(-) delete mode 100644 public/app/plugins/panel/flamegraph/suggestions.ts create mode 100644 public/app/plugins/panel/flamegraph/types.ts create mode 100644 public/app/plugins/panel/geomap/suggestions.ts create mode 100644 public/app/plugins/panel/heatmap/suggestions.test.ts delete mode 100644 public/app/plugins/panel/logs/suggestions.ts delete mode 100644 public/app/plugins/panel/status-history/suggestions.ts delete mode 100644 public/app/plugins/panel/traces/suggestions.ts delete mode 100644 public/app/plugins/panel/trend/suggestions.ts create mode 100644 public/app/plugins/panel/trend/utils.ts delete mode 100644 public/app/types/suggestions.ts diff --git a/packages/grafana-data/src/panel/PanelPlugin.ts b/packages/grafana-data/src/panel/PanelPlugin.ts index fdae270fd48..7b766227ba1 100644 --- a/packages/grafana-data/src/panel/PanelPlugin.ts +++ b/packages/grafana-data/src/panel/PanelPlugin.ts @@ -381,6 +381,11 @@ export class PanelPlugin< const appender = builder.getListAppender({ pluginId: this.meta.id, name: this.meta.name, + options: {}, + fieldConfig: { + defaults: {}, + overrides: [], + }, }); const result = supplier(builder.dataSummary); diff --git a/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.ts b/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.ts index ea97e1705d7..021d1eb48c6 100644 --- a/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.ts +++ b/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.ts @@ -1,19 +1,27 @@ import { PreferredVisualisationType } from '../../types/data'; import { DataFrame, FieldType } from '../../types/dataFrame'; +import { DataFrameType } from '../../types/dataFrameTypes'; -/** - * @alpha - */ export interface PanelDataSummary { hasData?: boolean; rowCountTotal: number; + /** max number of rows in any given dataframe in the panel data */ rowCountMax: number; frameCount: number; fieldCount: number; + /** max number of fields in any given dataframe in the panel data */ + fieldCountMax: number; + /** given a field type, return the number of fields across all dataframes which match this type */ fieldCountByType: (type: FieldType) => number; + /** returns true if any fields in any frames match the field type */ hasFieldType: (type: FieldType) => boolean; - /** The first frame that set's this value */ - preferredVisualisationType?: PreferredVisualisationType; + /* returns true if any of the frames in this panel data summary have the type */ + hasDataFrameType: (type: DataFrameType) => boolean; + /* returns true if any of the frames in this panel data summary have the type */ + hasPreferredVisualisationType: (type: PreferredVisualisationType) => boolean; + + /** pass along a reference to the DataFrame array in case it's needed by the plugin */ + rawFrames?: DataFrame[]; /* --- DEPRECATED FIELDS BELOW --- */ /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */ @@ -23,60 +31,114 @@ export interface PanelDataSummary { /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */ stringFieldCount: number; /** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */ - hasNumberField?: boolean; - /** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */ hasTimeField?: boolean; + /** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */ + hasNumberField?: boolean; /** @deprecated use PanelDataSummary.hasFieldType(FieldType.string) */ hasStringField?: boolean; } +/** + * @alpha + */ +class PanelDataSummaryImpl implements PanelDataSummary { + public rowCountTotal = 0; + /** max number of rows in any single dataframe in the panel data */ + public rowCountMax = 0; + public fieldCount = 0; + /** max number of fields in any single dataframe in the panel data */ + public fieldCountMax = 0; + + private countByType: Partial> = {}; + private preferredVisualisationTypes: Set = new Set(); + private dataFrameTypes: Set = new Set(); + + public get hasData(): boolean { + return this.rowCountTotal > 0; + } + + public get frameCount(): number { + return this.rawFrames?.length ?? 0; + } + + constructor(public rawFrames?: DataFrame[]) { + this._processFrames(); + } + + private _processFrames() { + for (const frame of this.rawFrames ?? []) { + this.rowCountTotal += frame.length; + + if (frame.meta?.preferredVisualisationType) { + this.preferredVisualisationTypes.add(frame.meta.preferredVisualisationType); + } + if (frame.meta?.type) { + this.dataFrameTypes.add(frame.meta.type); + } + + for (const field of frame.fields) { + this.fieldCount++; + this.countByType[field.type] = (this.countByType[field.type] || 0) + 1; + } + + if (frame.length > this.rowCountMax) { + this.rowCountMax = frame.length; + } + if (frame.fields.length > this.fieldCountMax) { + this.fieldCountMax = frame.fields.length; + } + } + } + + public fieldCountByType(type: FieldType): number { + return this.countByType[type] ?? 0; + } + + public hasFieldType(type: FieldType): boolean { + return this.fieldCountByType(type) > 0; + } + + public hasPreferredVisualisationType(type: PreferredVisualisationType): boolean { + return this.preferredVisualisationTypes.has(type); + } + + public hasDataFrameType(type: DataFrameType): boolean { + return this.dataFrameTypes.has(type); + } + + /**** DEPRECATED ****/ + /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */ + public get numberFieldCount(): number { + return this.fieldCountByType(FieldType.number); + } + /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.time) */ + public get timeFieldCount(): number { + return this.fieldCountByType(FieldType.time); + } + /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */ + public get stringFieldCount() { + return this.fieldCountByType(FieldType.string); + } + /** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */ + public get hasTimeField() { + return this.fieldCountByType(FieldType.time) > 0; + } + /** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */ + public get hasNumberField() { + return this.fieldCountByType(FieldType.number) > 0; + } + /** @deprecated use PanelDataSummary.hasFieldType(FieldType.string) */ + public get hasStringField() { + return this.fieldCountByType(FieldType.string) > 0; + } +} + /** * @alpha * given a list of dataframes, summarize attributes of those frames for features like suggestions. * @param frames - dataframes to summarize * @returns summary of the dataframes */ -export function getPanelDataSummary(frames: DataFrame[] = []): PanelDataSummary { - let rowCountTotal = 0; - let rowCountMax = 0; - let fieldCount = 0; - const countByType: Partial> = {}; - let preferredVisualisationType: PreferredVisualisationType | undefined; - - for (const frame of frames) { - rowCountTotal += frame.length; - - if (frame.meta?.preferredVisualisationType) { - preferredVisualisationType = frame.meta.preferredVisualisationType; - } - - for (const field of frame.fields) { - fieldCount++; - countByType[field.type] = (countByType[field.type] || 0) + 1; - } - - if (frame.length > rowCountMax) { - rowCountMax = frame.length; - } - } - - const fieldCountByType = (f: FieldType) => countByType[f] ?? 0; - - return { - rowCountTotal, - rowCountMax, - fieldCount, - preferredVisualisationType, - frameCount: frames.length, - hasData: rowCountTotal > 0, - hasFieldType: (f: FieldType) => fieldCountByType(f) > 0, - fieldCountByType, - // deprecated - numberFieldCount: fieldCountByType(FieldType.number), - timeFieldCount: fieldCountByType(FieldType.time), - stringFieldCount: fieldCountByType(FieldType.string), - hasTimeField: fieldCountByType(FieldType.time) > 0, - hasNumberField: fieldCountByType(FieldType.number) > 0, - hasStringField: fieldCountByType(FieldType.string) > 0, - }; +export function getPanelDataSummary(frames?: DataFrame[]): PanelDataSummary { + return new PanelDataSummaryImpl(frames); } diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 733155a3f10..f1023b49502 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -72,7 +72,7 @@ interface IndexOptions { asPercentile: boolean; } -const defaultReduceOptions: ReduceOptions = { +const defaultNumericVizOptions: ReduceOptions = { reducer: ReducerID.sum, }; @@ -149,10 +149,10 @@ export const calculateFieldTransformer: DataTransformerInfo) => void; + /** @deprecated this will no longer be supported in the new Suggestions UI. */ icon?: string; + /** @deprecated this will no longer be supported in the new Suggestions UI. */ imgSrc?: string; }; } diff --git a/public/app/features/geo/utils/location.ts b/public/app/features/geo/utils/location.ts index 96e92929cdd..90aaa0f9d1e 100644 --- a/public/app/features/geo/utils/location.ts +++ b/public/app/features/geo/utils/location.ts @@ -68,6 +68,16 @@ const defaultMatchers: LocationFieldMatchers = { geo: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.geo), }; +/** + * suggestions needs to run sync, and we just want to use the default matchers in that situation. + */ +export function getDefaultLocationMatchers(): LocationFieldMatchers { + return { + ...defaultMatchers, + mode: FrameGeometrySourceMode.Auto, + }; +} + export async function getLocationMatchers(src?: FrameGeometrySource): Promise { const info: LocationFieldMatchers = { ...defaultMatchers, diff --git a/public/app/features/panel/components/PanelDataErrorView.tsx b/public/app/features/panel/components/PanelDataErrorView.tsx index 4c72a3c879f..93723b3bff1 100644 --- a/public/app/features/panel/components/PanelDataErrorView.tsx +++ b/public/app/features/panel/components/PanelDataErrorView.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import { CoreApp, + FieldType, getPanelDataSummary, GrafanaTheme2, PanelDataSummary, @@ -134,15 +135,15 @@ function getMessageFor( return fieldConfig?.defaults.noValue ?? t('panel.panel-data-error-view.no-value.default', 'No data'); } - if (needsStringField && !dataSummary.hasStringField) { + if (needsStringField && !dataSummary.hasFieldType(FieldType.string)) { return t('panel.panel-data-error-view.missing-value.string', 'Data is missing a string field'); } - if (needsNumberField && !dataSummary.hasNumberField) { + if (needsNumberField && !dataSummary.hasFieldType(FieldType.number)) { return t('panel.panel-data-error-view.missing-value.number', 'Data is missing a number field'); } - if (needsTimeField && !dataSummary.hasTimeField) { + if (needsTimeField && !dataSummary.hasFieldType(FieldType.time)) { return t('panel.panel-data-error-view.missing-value.time', 'Data is missing a time field'); } diff --git a/public/app/features/panel/suggestions/getAllSuggestions.test.ts b/public/app/features/panel/suggestions/getAllSuggestions.test.ts index e4a0c72c5d9..1cb10605ab0 100644 --- a/public/app/features/panel/suggestions/getAllSuggestions.test.ts +++ b/public/app/features/panel/suggestions/getAllSuggestions.test.ts @@ -5,12 +5,18 @@ import { LoadingState, PanelData, PanelPluginMeta, - toDataFrame, PanelPluginVisualizationSuggestion, + toDataFrame, } from '@grafana/data'; -import { GraphFieldConfig, ReduceDataOptions } from '@grafana/schema'; +import { + BarGaugeDisplayMode, + BigValueColorMode, + GraphFieldConfig, + ReduceDataOptions, + StackingMode, + VizOrientation, +} from '@grafana/schema'; import { config } from 'app/core/config'; -import { SuggestionName } from 'app/types/suggestions'; import { getAllSuggestions, panelsToCheckFirst } from './getAllSuggestions'; @@ -21,6 +27,8 @@ for (const pluginId of panelsToCheckFirst) { } as PanelPluginMeta; } +const SCALAR_PLUGINS = ['gauge', 'stat', 'bargauge', 'piechart', 'radialbar']; + config.panels['text'] = { id: 'text', name: 'Text', @@ -69,7 +77,10 @@ scenario('No series', (ctx) => { ctx.setData([]); it('should return correct suggestions', () => { - expect(ctx.names()).toEqual([SuggestionName.Table, SuggestionName.TextPanel]); + expect(ctx.suggestions).toEqual([ + expect.objectContaining({ pluginId: 'table' }), + expect.objectContaining({ pluginId: 'text' }), + ]); }); }); @@ -84,7 +95,7 @@ scenario('No rows', (ctx) => { ]); it('should return correct suggestions', () => { - expect(ctx.names()).toEqual([SuggestionName.Table]); + expect(ctx.suggestions).toEqual([expect.objectContaining({ pluginId: 'table' })]); }); }); @@ -100,33 +111,46 @@ scenario('Single frame with time and number field', (ctx) => { it('should return correct suggestions', () => { expect(ctx.suggestions).toEqual([ - expect.objectContaining({ name: SuggestionName.LineChart }), - expect.objectContaining({ name: SuggestionName.LineChartSmooth }), - expect.objectContaining({ name: SuggestionName.AreaChart }), - expect.objectContaining({ name: SuggestionName.LineChartGradientColorScheme }), - expect.objectContaining({ name: SuggestionName.BarChart }), - expect.objectContaining({ name: SuggestionName.BarChartGradientColorScheme }), - expect.objectContaining({ name: SuggestionName.Gauge }), - expect.objectContaining({ name: SuggestionName.GaugeNoThresholds }), - expect.objectContaining({ name: SuggestionName.Stat }), - expect.objectContaining({ name: SuggestionName.StatColoredBackground }), - expect.objectContaining({ name: SuggestionName.BarGaugeBasic }), - expect.objectContaining({ name: SuggestionName.BarGaugeLCD }), - expect.objectContaining({ name: SuggestionName.Table }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Line chart' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Line chart - smooth' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Area chart' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Bar chart' }), + expect.objectContaining({ pluginId: 'gauge' }), + expect.objectContaining({ pluginId: 'gauge', options: expect.objectContaining({ showThresholdMarkers: false }) }), + expect.objectContaining({ pluginId: 'stat' }), + expect.objectContaining({ + pluginId: 'stat', + options: expect.objectContaining({ colorMode: BigValueColorMode.Background }), + }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }), + }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Lcd }), + }), + expect.objectContaining({ pluginId: 'table' }), expect.objectContaining({ pluginId: 'state-timeline' }), - expect.objectContaining({ name: SuggestionName.StatusHistory }), + expect.objectContaining({ pluginId: 'status-history' }), + expect.objectContaining({ pluginId: 'heatmap' }), + expect.objectContaining({ pluginId: 'histogram' }), ]); }); it('Bar chart suggestion should be using timeseries panel', () => { - expect(ctx.suggestions.find((x) => x.name === SuggestionName.BarChart)?.pluginId).toBe('timeseries'); + expect(ctx.suggestions.find((x) => x.name === 'Bar chart')?.pluginId).toBe('timeseries'); }); - it('Stat panels have reduce values disabled', () => { - for (const suggestion of ctx.suggestions) { - if (suggestion.options?.reduceOptions?.values) { - throw new Error(`Suggestion ${suggestion.name} reduce.values set to true when it should be false`); - } + it('Scalar panels should use calcs', () => { + for (const suggestion of ctx.suggestions.filter((s) => SCALAR_PLUGINS.includes(s.pluginId))) { + expect(suggestion).toEqual( + expect.objectContaining({ + options: expect.objectContaining({ + reduceOptions: expect.objectContaining({ values: false, calcs: ['lastNotNull'] }), + }), + }) + ); } }); }); @@ -144,31 +168,46 @@ scenario('Single frame with time 2 number fields', (ctx) => { it('should return correct suggestions', () => { expect(ctx.suggestions).toEqual([ - expect.objectContaining({ name: SuggestionName.LineChart }), - expect.objectContaining({ name: SuggestionName.LineChartSmooth }), - expect.objectContaining({ name: SuggestionName.AreaChartStacked }), - expect.objectContaining({ name: SuggestionName.AreaChartStackedPercent }), - expect.objectContaining({ name: SuggestionName.BarChartStacked }), - expect.objectContaining({ name: SuggestionName.BarChartStackedPercent }), - expect.objectContaining({ name: SuggestionName.Gauge }), - expect.objectContaining({ name: SuggestionName.GaugeNoThresholds }), - expect.objectContaining({ name: SuggestionName.Stat }), - expect.objectContaining({ name: SuggestionName.StatColoredBackground }), - expect.objectContaining({ name: SuggestionName.PieChart }), - expect.objectContaining({ name: SuggestionName.PieChartDonut }), - expect.objectContaining({ name: SuggestionName.BarGaugeBasic }), - expect.objectContaining({ name: SuggestionName.BarGaugeLCD }), - expect.objectContaining({ name: SuggestionName.Table }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Line chart' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Line chart - smooth' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Area chart - stacked' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Area chart - stacked by percentage' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Bar chart - stacked' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Bar chart - stacked by percentage' }), + expect.objectContaining({ pluginId: 'gauge' }), + expect.objectContaining({ pluginId: 'gauge', options: expect.objectContaining({ showThresholdMarkers: false }) }), + expect.objectContaining({ pluginId: 'stat' }), + expect.objectContaining({ + pluginId: 'stat', + options: expect.objectContaining({ colorMode: BigValueColorMode.Background }), + }), + expect.objectContaining({ pluginId: 'piechart' }), + expect.objectContaining({ pluginId: 'piechart', options: expect.objectContaining({ pieType: 'donut' }) }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }), + }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Lcd }), + }), + expect.objectContaining({ pluginId: 'table' }), expect.objectContaining({ pluginId: 'state-timeline' }), - expect.objectContaining({ name: SuggestionName.StatusHistory }), + expect.objectContaining({ pluginId: 'status-history' }), + expect.objectContaining({ pluginId: 'heatmap' }), + expect.objectContaining({ pluginId: 'histogram' }), ]); }); - it('Stat panels have reduceOptions.values disabled', () => { - for (const suggestion of ctx.suggestions) { - if (suggestion.options?.reduceOptions?.values) { - throw new Error(`Suggestion ${suggestion.name} reduce.values set to true when it should be false`); - } + it('Scalar panels should use calcs', () => { + for (const suggestion of ctx.suggestions.filter((s) => SCALAR_PLUGINS.includes(s.pluginId))) { + expect(suggestion).toEqual( + expect.objectContaining({ + options: expect.objectContaining({ + reduceOptions: expect.objectContaining({ values: false, calcs: ['lastNotNull'] }), + }), + }) + ); } }); }); @@ -184,7 +223,7 @@ scenario('Single time series with 100 data points', (ctx) => { ]); it('should not suggest bar chart', () => { - expect(ctx.suggestions.find((x) => x.name === SuggestionName.BarChart)).toBe(undefined); + expect(ctx.suggestions.find((x) => x.name === 'Bar chart')).toBe(undefined); }); }); @@ -235,26 +274,42 @@ scenario('Single frame with string and number field', (ctx) => { ]); it('should return correct suggestions', () => { - expect(ctx.names()).toEqual([ - SuggestionName.BarChart, - SuggestionName.BarChartHorizontal, - SuggestionName.Gauge, - SuggestionName.GaugeNoThresholds, - SuggestionName.Stat, - SuggestionName.StatColoredBackground, - SuggestionName.PieChart, - SuggestionName.PieChartDonut, - SuggestionName.BarGaugeBasic, - SuggestionName.BarGaugeLCD, - SuggestionName.Table, + expect(ctx.suggestions).toEqual([ + expect.objectContaining({ pluginId: 'piechart' }), + expect.objectContaining({ pluginId: 'piechart', options: expect.objectContaining({ pieType: 'donut' }) }), + expect.objectContaining({ pluginId: 'barchart' }), + expect.objectContaining({ + pluginId: 'barchart', + options: expect.objectContaining({ orientation: VizOrientation.Horizontal }), + }), + expect.objectContaining({ pluginId: 'gauge' }), + expect.objectContaining({ pluginId: 'gauge', options: expect.objectContaining({ showThresholdMarkers: false }) }), + expect.objectContaining({ pluginId: 'stat' }), + expect.objectContaining({ + pluginId: 'stat', + options: expect.objectContaining({ colorMode: BigValueColorMode.Background }), + }), + + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }), + }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Lcd }), + }), + expect.objectContaining({ pluginId: 'table' }), + expect.objectContaining({ pluginId: 'histogram' }), ]); }); - it('Stat/Gauge/BarGauge/PieChart panels to have reduceOptions.values enabled', () => { - for (const suggestion of ctx.suggestions) { - if (suggestion.options?.reduceOptions && !suggestion.options?.reduceOptions?.values) { - throw new Error(`Suggestion ${suggestion.name} reduce.values set to false when it should be true`); - } + it('Scalar panels should contain raw values', () => { + for (const suggestion of ctx.suggestions.filter((s) => SCALAR_PLUGINS.includes(s.pluginId))) { + expect(suggestion).toEqual( + expect.objectContaining({ + options: expect.objectContaining({ reduceOptions: expect.objectContaining({ values: true, calcs: [] }) }), + }) + ); } }); }); @@ -271,22 +326,48 @@ scenario('Single frame with string and 2 number field', (ctx) => { ]); it('should return correct suggestions', () => { - expect(ctx.names()).toEqual([ - SuggestionName.BarChart, - SuggestionName.BarChartStacked, - SuggestionName.BarChartStackedPercent, - SuggestionName.BarChartHorizontal, - SuggestionName.BarChartHorizontalStacked, - SuggestionName.BarChartHorizontalStackedPercent, - SuggestionName.Gauge, - SuggestionName.GaugeNoThresholds, - SuggestionName.Stat, - SuggestionName.StatColoredBackground, - SuggestionName.PieChart, - SuggestionName.PieChartDonut, - SuggestionName.BarGaugeBasic, - SuggestionName.BarGaugeLCD, - SuggestionName.Table, + expect(ctx.suggestions).toEqual([ + expect.objectContaining({ pluginId: 'barchart' }), + expect.objectContaining({ + pluginId: 'barchart', + options: expect.objectContaining({ stacking: StackingMode.Normal }), + }), + expect.objectContaining({ + pluginId: 'barchart', + options: expect.objectContaining({ stacking: StackingMode.Percent }), + }), + + expect.objectContaining({ + pluginId: 'barchart', + options: expect.objectContaining({ orientation: VizOrientation.Horizontal }), + }), + expect.objectContaining({ + pluginId: 'barchart', + options: expect.objectContaining({ orientation: VizOrientation.Horizontal, stacking: StackingMode.Normal }), + }), + expect.objectContaining({ + pluginId: 'barchart', + options: expect.objectContaining({ orientation: VizOrientation.Horizontal, stacking: StackingMode.Percent }), + }), + expect.objectContaining({ pluginId: 'gauge' }), + expect.objectContaining({ pluginId: 'gauge', options: expect.objectContaining({ showThresholdMarkers: false }) }), + expect.objectContaining({ pluginId: 'stat' }), + expect.objectContaining({ + pluginId: 'stat', + options: expect.objectContaining({ colorMode: BigValueColorMode.Background }), + }), + expect.objectContaining({ pluginId: 'piechart' }), + expect.objectContaining({ pluginId: 'piechart', options: expect.objectContaining({ pieType: 'donut' }) }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }), + }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Lcd }), + }), + expect.objectContaining({ pluginId: 'table' }), + expect.objectContaining({ pluginId: 'histogram' }), ]); }); }); @@ -299,11 +380,14 @@ scenario('Single frame with only string field', (ctx) => { ]); it('should return correct suggestions', () => { - expect(ctx.names()).toEqual([SuggestionName.Stat, SuggestionName.Table]); + expect(ctx.suggestions).toEqual([ + expect.objectContaining({ pluginId: 'stat' }), + expect.objectContaining({ pluginId: 'table' }), + ]); }); it('Stat panels have reduceOptions.fields set to show all fields', () => { - for (const suggestion of ctx.suggestions) { + for (const suggestion of ctx.suggestions.filter((s) => s.pluginId === 'stat')) { if (suggestion.options?.reduceOptions) { expect(suggestion.options.reduceOptions.fields).toBe('/.*/'); } @@ -333,7 +417,10 @@ scenario('Given default loki logs data', (ctx) => { ]); it('should return correct suggestions', () => { - expect(ctx.names()).toEqual([SuggestionName.Logs, SuggestionName.Table]); + expect(ctx.suggestions).toEqual([ + expect.objectContaining({ pluginId: 'logs' }), + expect.objectContaining({ pluginId: 'table' }), + ]); }); }); @@ -356,7 +443,7 @@ scenario('Given a preferredVisualisationType', (ctx) => { ]); it('should return the preferred visualization first', () => { - expect(ctx.names()[0]).toEqual(SuggestionName.Table); + expect(ctx.suggestions[0]).toEqual(expect.objectContaining({ pluginId: 'table' })); }); }); diff --git a/public/app/features/panel/suggestions/getAllSuggestions.ts b/public/app/features/panel/suggestions/getAllSuggestions.ts index ff6fbef50e8..a7c9764d2a0 100644 --- a/public/app/features/panel/suggestions/getAllSuggestions.ts +++ b/public/app/features/panel/suggestions/getAllSuggestions.ts @@ -4,6 +4,7 @@ import { VisualizationSuggestionsBuilder, PanelModel, VisualizationSuggestionScore, + PreferredVisualisationType, } from '@grafana/data'; import { config } from '@grafana/runtime'; import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin'; @@ -23,8 +24,26 @@ export const panelsToCheckFirst = [ 'flamegraph', 'traces', 'nodeGraph', + 'heatmap', + 'histogram', + 'geomap', ]; +/** + * some of the PreferredVisualisationTypes do not match the panel plugin ids, so we have to map them. d'oh. + */ +const PLUGIN_ID_TO_PREFERRED_VIZ_TYPE: Record = { + traces: 'trace', + timeseries: 'graph', + table: 'table', + logs: 'logs', + nodeGraph: 'nodeGraph', + flamegraph: 'flamegraph', +}; +const mapPreferredVisualisationTypeToPlugin = (type: string): PreferredVisualisationType | undefined => { + return PLUGIN_ID_TO_PREFERRED_VIZ_TYPE[type]; +}; + export async function getAllSuggestions( data?: PanelData, panel?: PanelModel @@ -61,13 +80,13 @@ export async function getAllSuggestions( } return list.sort((a, b) => { - if (builder.dataSummary.preferredVisualisationType) { - if (a.pluginId === builder.dataSummary.preferredVisualisationType) { - return -1; - } - if (b.pluginId === builder.dataSummary.preferredVisualisationType) { - return 1; - } + const mappedA = mapPreferredVisualisationTypeToPlugin(a.pluginId); + if (mappedA && builder.dataSummary.hasPreferredVisualisationType(mappedA)) { + return -1; + } + const mappedB = mapPreferredVisualisationTypeToPlugin(a.pluginId); + if (mappedB && builder.dataSummary.hasPreferredVisualisationType(mappedB)) { + return 1; } return (b.score ?? VisualizationSuggestionScore.OK) - (a.score ?? VisualizationSuggestionScore.OK); }); diff --git a/public/app/features/panel/suggestions/utils.ts b/public/app/features/panel/suggestions/utils.ts index ca5cc74bd77..07820587be5 100644 --- a/public/app/features/panel/suggestions/utils.ts +++ b/public/app/features/panel/suggestions/utils.ts @@ -1,4 +1,11 @@ -import { PanelData, PanelDataSummary } from '@grafana/data'; +import { + DataFrameType, + PanelData, + PanelDataSummary, + VisualizationSuggestion, + VisualizationSuggestionScore, +} from '@grafana/data'; +import { ReduceDataOptions } from '@grafana/schema'; /** * @internal @@ -10,6 +17,42 @@ export function showDefaultSuggestion(fn: (panelDataSummary: PanelDataSummary) = return (panelDataSummary: PanelDataSummary) => (fn(panelDataSummary) ? [{}] : undefined); } +/** + * @internal + * for panel plugins which render "scalar" data (stat, gauge, etc), this helper provides default reduce options + * depending on whether deaggregation is likely needed. + * @param suggestion the suggestion to modify + * @param panelDataSummary the panel data summary to use for scoring + * @param shouldUseRawValues if true, reduceOptions will be set to use raw values, + * otherwise a calcs will be used with the default value of `lastNotNull`. + */ +export function defaultNumericVizOptions( + suggestion: VisualizationSuggestion<{ reduceOptions?: ReduceDataOptions }>, + panelDataSummary: PanelDataSummary, + shouldUseRawValues: boolean +): VisualizationSuggestion { + suggestion.score = + (suggestion.score ?? + (panelDataSummary.hasDataFrameType(DataFrameType.NumericLong) || + panelDataSummary.hasDataFrameType(DataFrameType.NumericWide) || + panelDataSummary.hasDataFrameType(DataFrameType.NumericMulti))) + ? VisualizationSuggestionScore.Good + : VisualizationSuggestionScore.OK; + suggestion.options = suggestion.options ?? {}; + suggestion.options.reduceOptions = + suggestion.options.reduceOptions ?? + (shouldUseRawValues + ? { + values: true, + calcs: [], + } + : { + values: false, + calcs: ['lastNotNull'], + }); + return suggestion; +} + /** * @internal * Checks if the panel has data diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index d4284656595..28ba77d5476 100644 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -18,7 +18,7 @@ import { BarChartPanel } from './BarChartPanel'; import { TickSpacingEditor } from './TickSpacingEditor'; import { changeToBarChartPanelMigrationHandler } from './migrations'; import { FieldConfig, Options, defaultFieldConfig, defaultOptions } from './panelcfg.gen'; -import { BarChartSuggestionsSupplier } from './suggestions'; +import { barchartSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(BarChartPanel) .setPanelChangeHandler(changeToBarChartPanelMigrationHandler) @@ -257,7 +257,7 @@ export const plugin = new PanelPlugin(BarChartPanel) commonOptionsBuilder.addLegendOptions(builder); commonOptionsBuilder.addTextSizeOptions(builder, { withValue: true }); }) - .setSuggestionsSupplier(new BarChartSuggestionsSupplier()); + .setSuggestionsSupplier(barchartSuggestionsSupplier); function countNumberFields(data?: DataFrame[]): number { let count = 0; diff --git a/public/app/plugins/panel/barchart/suggestions.ts b/public/app/plugins/panel/barchart/suggestions.ts index f9428d6e709..56918e4b346 100644 --- a/public/app/plugins/panel/barchart/suggestions.ts +++ b/public/app/plugins/panel/barchart/suggestions.ts @@ -1,99 +1,112 @@ -import { VisualizationSuggestionsBuilder, VizOrientation } from '@grafana/data'; +import { defaultsDeep } from 'lodash'; + +import { FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplierFn, VizOrientation } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { LegendDisplayMode, StackingMode, VisibilityMode } from '@grafana/schema'; -import { SuggestionName } from 'app/types/suggestions'; import { FieldConfig, Options } from './panelcfg.gen'; -export class BarChartSuggestionsSupplier { - getListWithDefaults(builder: VisualizationSuggestionsBuilder) { - return builder.getListAppender({ - name: SuggestionName.BarChart, - pluginId: 'barchart', - options: { - showValue: VisibilityMode.Never, - legend: { - calcs: [], - displayMode: LegendDisplayMode.List, - showLegend: true, - placement: 'right', - }, +const withDefaults = (suggestion: VisualizationSuggestion) => + defaultsDeep(suggestion, { + options: { + showValue: VisibilityMode.Never, + legend: { + calcs: [], + displayMode: LegendDisplayMode.List, + showLegend: true, + placement: 'right', }, - fieldConfig: { - defaults: { - unit: 'short', - custom: {}, - }, - overrides: [], + }, + fieldConfig: { + defaults: { + unit: 'short', + custom: {}, }, - cardOptions: { - previewModifier: (s) => { - s.options!.barWidth = 0.8; - }, + overrides: [], + }, + cardOptions: { + previewModifier: (s) => { + s.options!.barWidth = 0.8; + s.fieldConfig!.defaults!.custom!.hideFrom = { tooltip: false, legend: true, viz: false }; // hide legend in preview }, - }); + }, + } satisfies VisualizationSuggestion); + +export const barchartSuggestionsSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => { + if (dataSummary.frameCount !== 1) { + return; } - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const list = this.getListWithDefaults(builder); - const { dataSummary } = builder; + if (!dataSummary.hasFieldType(FieldType.number) || !dataSummary.hasFieldType(FieldType.string)) { + return; + } - if (dataSummary.frameCount !== 1) { - return; - } + // if you have this many rows barchart might not be a good fit + if (dataSummary.rowCountTotal > 50) { + return; + } - if (!dataSummary.hasNumberField || !dataSummary.hasStringField) { - return; - } + const result: Array> = [ + { + name: t('barchart.suggestions.vertical', 'Bar chart'), + }, + ]; - // if you have this many rows barchart might not be a good fit - if (dataSummary.rowCountTotal > 50) { - return; - } - - // Vertical bars - list.append({ - name: SuggestionName.BarChart, - }); - - if (dataSummary.numberFieldCount > 1) { - list.append({ - name: SuggestionName.BarChartStacked, + if (dataSummary.fieldCountByType(FieldType.number) > 1) { + result.push( + { + name: t('barchart.suggestions.vert-stacked', 'Bar chart - stacked'), options: { stacking: StackingMode.Normal, }, - }); - list.append({ - name: SuggestionName.BarChartStackedPercent, + }, + { + name: t('barchart.suggestions.vert-stacked-percent', 'Bar chart - stacked by percentage'), options: { stacking: StackingMode.Percent, }, - }); - } - - // horizontal bars - list.append({ - name: SuggestionName.BarChartHorizontal, - options: { - orientation: VizOrientation.Horizontal, - }, - }); - - if (dataSummary.numberFieldCount > 1) { - list.append({ - name: SuggestionName.BarChartHorizontalStacked, - options: { - stacking: StackingMode.Normal, - orientation: VizOrientation.Horizontal, + fieldConfig: { + overrides: [], + defaults: { + unit: 'percentunit', + }, }, - }); + } + ); + } - list.append({ - name: SuggestionName.BarChartHorizontalStackedPercent, + // horizontal bars + result.push({ + name: t('barchart.suggestions.horizontal', 'Horizontal bar chart'), + options: { + orientation: VizOrientation.Horizontal, + }, + }); + + if (dataSummary.fieldCountByType(FieldType.number) > 1) { + result.push( + { + name: t('barchart.suggestions.hz-stacked', 'Horizontal bar chart - stacked'), + options: { + orientation: VizOrientation.Horizontal, + stacking: StackingMode.Normal, + }, + }, + { + name: t('barchart.suggestions.hz-stacked-percent', 'Horizontal bar chart - stacked by percentage'), options: { orientation: VizOrientation.Horizontal, stacking: StackingMode.Percent, }, - }); - } + fieldConfig: { + overrides: [], + defaults: { + unit: 'percentunit', + }, + }, + } + ); } -} + + return result.map(withDefaults); +}; diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx index 3e0262fb237..8e7fd5ccae6 100644 --- a/public/app/plugins/panel/bargauge/module.tsx +++ b/public/app/plugins/panel/bargauge/module.tsx @@ -8,7 +8,7 @@ import { addOrientationOption, addStandardDataReduceOptions } from '../stat/comm import { barGaugePanelMigrationHandler } from './BarGaugeMigrations'; import { BarGaugePanel } from './BarGaugePanel'; import { Options, defaultOptions } from './panelcfg.gen'; -import { BarGaugeSuggestionsSupplier } from './suggestions'; +import { barGaugeSugggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(BarGaugePanel) .useFieldConfig() @@ -151,4 +151,4 @@ export const plugin = new PanelPlugin(BarGaugePanel) }) .setPanelChangeHandler(sharedSingleStatPanelChangedHandler) .setMigrationHandler(barGaugePanelMigrationHandler) - .setSuggestionsSupplier(new BarGaugeSuggestionsSupplier()); + .setSuggestionsSupplier(barGaugeSugggestionsSupplier); diff --git a/public/app/plugins/panel/bargauge/suggestions.ts b/public/app/plugins/panel/bargauge/suggestions.ts index 61d48ce104a..dc8a451cac0 100644 --- a/public/app/plugins/panel/bargauge/suggestions.ts +++ b/public/app/plugins/panel/bargauge/suggestions.ts @@ -1,115 +1,60 @@ -import { FieldColorModeId, VisualizationSuggestionsBuilder, VizOrientation } from '@grafana/data'; +import { defaultsDeep } from 'lodash'; + +import { + FieldColorModeId, + FieldType, + VisualizationSuggestion, + VisualizationSuggestionsSupplierFn, + VizOrientation, +} from '@grafana/data'; +import { t } from '@grafana/i18n'; import { BarGaugeDisplayMode } from '@grafana/ui'; -import { SuggestionName } from 'app/types/suggestions'; +import { defaultNumericVizOptions } from 'app/features/panel/suggestions/utils'; import { Options } from './panelcfg.gen'; -export class BarGaugeSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary } = builder; - - if (!dataSummary.hasData || !dataSummary.hasNumberField) { - return; - } - - const list = builder.getListAppender({ - name: '', - pluginId: 'bargauge', - options: {}, - fieldConfig: { - defaults: { - custom: {}, +const withDefaults = (suggestion: VisualizationSuggestion): VisualizationSuggestion => + defaultsDeep(suggestion, { + options: { + displayMode: BarGaugeDisplayMode.Basic, + orientation: VizOrientation.Horizontal, + }, + fieldConfig: { + defaults: { + color: { + mode: FieldColorModeId.ContinuousGrYlRd, }, - overrides: [], }, - }); + overrides: [], + }, + }); - // This is probably not a good option for many numeric fields - if (dataSummary.numberFieldCount > 50) { - return; - } +const BAR_LIMIT = 30; - // To use show individual row values we also need a string field to give each value a name - if (dataSummary.hasStringField && dataSummary.frameCount === 1 && dataSummary.rowCountTotal < 30) { - list.append({ - name: SuggestionName.BarGaugeBasic, - options: { - reduceOptions: { - values: true, - calcs: [], - }, - displayMode: BarGaugeDisplayMode.Basic, - orientation: VizOrientation.Horizontal, - }, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - }, - overrides: [], - }, - }); - - list.append({ - name: SuggestionName.BarGaugeLCD, - options: { - reduceOptions: { - values: true, - calcs: [], - }, - displayMode: BarGaugeDisplayMode.Lcd, - orientation: VizOrientation.Horizontal, - }, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - }, - overrides: [], - }, - }); - } else { - list.append({ - name: SuggestionName.BarGaugeBasic, - options: { - displayMode: BarGaugeDisplayMode.Basic, - orientation: VizOrientation.Horizontal, - reduceOptions: { - values: false, - calcs: ['lastNotNull'], - }, - }, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - }, - overrides: [], - }, - }); - - list.append({ - name: SuggestionName.BarGaugeLCD, - options: { - displayMode: BarGaugeDisplayMode.Lcd, - orientation: VizOrientation.Horizontal, - reduceOptions: { - values: false, - calcs: ['lastNotNull'], - }, - }, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - }, - overrides: [], - }, - }); - } +export const barGaugeSugggestionsSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => { + if (!dataSummary.hasData || !dataSummary.hasFieldType(FieldType.number)) { + return; } -} + + // This is probably not a good option for many numeric fields + if (dataSummary.fieldCountByType(FieldType.number) > BAR_LIMIT) { + return; + } + + const suggestions: Array> = [ + { name: t('bargauge.suggestions.basic', 'Bar gauge') }, + { + name: t('bargauge.suggestions.lcd', 'Bar gauge - LCD'), + options: { + displayMode: BarGaugeDisplayMode.Lcd, + }, + }, + ]; + + const shouldUseRawValues = + dataSummary.hasFieldType(FieldType.string) && + dataSummary.frameCount === 1 && + dataSummary.rowCountTotal <= BAR_LIMIT; + + return suggestions.map((s) => defaultNumericVizOptions(withDefaults(s), dataSummary, shouldUseRawValues)); +}; diff --git a/public/app/plugins/panel/candlestick/module.tsx b/public/app/plugins/panel/candlestick/module.tsx index 8a57e4801c1..ae43037e1b1 100644 --- a/public/app/plugins/panel/candlestick/module.tsx +++ b/public/app/plugins/panel/candlestick/module.tsx @@ -8,7 +8,7 @@ import { defaultGraphConfig, getGraphFieldConfig } from '../timeseries/config'; import { CandlestickPanel } from './CandlestickPanel'; import { CandlestickData, getCandlestickFieldsInfo, FieldPickerInfo, prepareCandlestickFields } from './fields'; -import { CandlestickSuggestionsSupplier } from './suggestions'; +import { candlestickSuggestionSupplier } from './suggestions'; import { defaultCandlestickColors, defaultOptions, Options, VizDisplayMode, ColorStrategy, CandleStyle } from './types'; const numericFieldFilter = (f: Field) => f.type === FieldType.number; @@ -147,4 +147,4 @@ export const plugin = new PanelPlugin(CandlestickPane commonOptionsBuilder.addLegendOptions(builder); }) .setDataSupport({ annotations: true, alertStates: true }) - .setSuggestionsSupplier(new CandlestickSuggestionsSupplier()); + .setSuggestionsSupplier(candlestickSuggestionSupplier); diff --git a/public/app/plugins/panel/candlestick/suggestions.ts b/public/app/plugins/panel/candlestick/suggestions.ts index ce22cc276c5..81834af9db5 100644 --- a/public/app/plugins/panel/candlestick/suggestions.ts +++ b/public/app/plugins/panel/candlestick/suggestions.ts @@ -1,54 +1,29 @@ -import { VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data'; +import { FieldType, VisualizationSuggestionScore, VisualizationSuggestionsSupplierFn } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { SuggestionName } from 'app/types/suggestions'; import { prepareCandlestickFields } from './fields'; import { defaultOptions, Options } from './types'; -export class CandlestickSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary } = builder; - - if ( - !builder.data?.series || - !dataSummary.hasData || - dataSummary.timeFieldCount < 1 || - dataSummary.numberFieldCount < 2 || - dataSummary.numberFieldCount > 10 - ) { - return; - } - - const info = prepareCandlestickFields(builder.data.series, defaultOptions, config.theme2); - if (!info) { - return; - } - - // Regular timeseries - if (info.open === info.high && info.open === info.low) { - return; - } - - const list = builder.getListAppender({ - name: '', - pluginId: 'candlestick', - options: {}, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], - }, - }); - - list.append({ - name: SuggestionName.Candlestick, - options: defaultOptions, - fieldConfig: { - defaults: {}, - overrides: [], - }, - score: info.autoOpenClose ? VisualizationSuggestionScore.Good : VisualizationSuggestionScore.Best, - }); +export const candlestickSuggestionSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => { + if ( + !dataSummary.rawFrames || + !dataSummary.hasData || + dataSummary.fieldCountByType(FieldType.time) < 1 || + dataSummary.fieldCountByType(FieldType.number) < 2 || + dataSummary.fieldCountByType(FieldType.number) > 10 + ) { + return; } -} + + const info = prepareCandlestickFields(dataSummary.rawFrames, defaultOptions, config.theme2); + if (!info) { + return; + } + + // Regular timeseries + if (info.open === info.high && info.open === info.low) { + return; + } + + return [{ score: info.autoOpenClose ? VisualizationSuggestionScore.Good : VisualizationSuggestionScore.Best }]; +}; diff --git a/public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx b/public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx index e9e8d9c8368..54a68a78af0 100644 --- a/public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx +++ b/public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx @@ -2,6 +2,8 @@ import { CoreApp, PanelProps } from '@grafana/data'; import { FlameGraph, checkFields, getMessageCheckFieldsResult } from '@grafana/flamegraph'; import { PanelDataErrorView, reportInteraction, config } from '@grafana/runtime'; +import { Options } from './types'; + function interaction(name: string, context: Record = {}) { reportInteraction(`grafana_flamegraph_${name}`, { app: CoreApp.Unknown, @@ -10,7 +12,7 @@ function interaction(name: string, context: Record = {} }); } -export const FlameGraphPanel = (props: PanelProps) => { +export const FlameGraphPanel = (props: PanelProps) => { const wrongFields = checkFields(props.data.series[0]); if (wrongFields) { return ( diff --git a/public/app/plugins/panel/flamegraph/module.tsx b/public/app/plugins/panel/flamegraph/module.tsx index 19fdd0afcfc..a80fcea3eee 100644 --- a/public/app/plugins/panel/flamegraph/module.tsx +++ b/public/app/plugins/panel/flamegraph/module.tsx @@ -1,12 +1,29 @@ import { FieldConfigProperty, PanelPlugin } from '@grafana/data'; +import { checkFields } from '@grafana/flamegraph'; import { FlameGraphPanel } from './FlameGraphPanel'; -import { FlameGraphSuggestionsSupplier } from './suggestions'; +import { Options } from './types'; const flamegraphConfigOptions = [FieldConfigProperty.Unit, FieldConfigProperty.Decimals]; -export const plugin = new PanelPlugin(FlameGraphPanel) - .setSuggestionsSupplier(new FlameGraphSuggestionsSupplier()) +export const plugin = new PanelPlugin(FlameGraphPanel) + // check that the first frame of the data has the required fields for a flamegraph + .setSuggestionsSupplier((ds) => { + if (!ds.rawFrames?.some((frame) => checkFields(frame) === undefined)) { + return; + } + + return [ + { + cardOptions: { + previewModifier: (s) => { + s.options = s.options || {}; + s.options.showFlameGraphOnly = true; + }, + }, + }, + ]; + }) .useFieldConfig({ disableStandardOptions: Object.values(FieldConfigProperty).filter((v) => !flamegraphConfigOptions.includes(v)), }); diff --git a/public/app/plugins/panel/flamegraph/suggestions.ts b/public/app/plugins/panel/flamegraph/suggestions.ts deleted file mode 100644 index eb997ee8078..00000000000 --- a/public/app/plugins/panel/flamegraph/suggestions.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { VisualizationSuggestionsBuilder } from '@grafana/data'; -import { checkFields } from '@grafana/flamegraph'; -import { SuggestionName } from 'app/types/suggestions'; - -export class FlameGraphSuggestionsSupplier { - getListWithDefaults(builder: VisualizationSuggestionsBuilder) { - return builder.getListAppender<{}, {}>({ - name: SuggestionName.FlameGraph, - pluginId: 'flamegraph', - }); - } - - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - if (!builder.data) { - return; - } - - const dataFrame = builder.data.series[0]; - if (!dataFrame) { - return; - } - const wrongFields = checkFields(dataFrame); - if (wrongFields) { - return; - } - - this.getListWithDefaults(builder).append({ - name: SuggestionName.FlameGraph, - }); - } -} diff --git a/public/app/plugins/panel/flamegraph/types.ts b/public/app/plugins/panel/flamegraph/types.ts new file mode 100644 index 00000000000..8e5544c54a2 --- /dev/null +++ b/public/app/plugins/panel/flamegraph/types.ts @@ -0,0 +1,3 @@ +export interface Options { + showFlameGraphOnly?: boolean; +} diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 690698534a1..61ea853e943 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/public/app/plugins/panel/gauge/module.tsx @@ -8,7 +8,7 @@ import { addOrientationOption, addStandardDataReduceOptions } from '../stat/comm import { gaugePanelMigrationHandler, gaugePanelChangedHandler } from './GaugeMigrations'; import { GaugePanel } from './GaugePanel'; import { Options, defaultOptions } from './panelcfg.gen'; -import { GaugeSuggestionsSupplier } from './suggestions'; +import { gaugeSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(GaugePanel) .useFieldConfig({ @@ -88,5 +88,5 @@ export const plugin = new PanelPlugin(GaugePanel) commonOptionsBuilder.addTextSizeOptions(builder, { withTitle: true, withValue: true }); }) .setPanelChangeHandler(gaugePanelChangedHandler) - .setSuggestionsSupplier(new GaugeSuggestionsSupplier()) + .setSuggestionsSupplier(gaugeSuggestionsSupplier) .setMigrationHandler(gaugePanelMigrationHandler); diff --git a/public/app/plugins/panel/gauge/suggestions.ts b/public/app/plugins/panel/gauge/suggestions.ts index 5f5ebc8f40e..6695741b093 100644 --- a/public/app/plugins/panel/gauge/suggestions.ts +++ b/public/app/plugins/panel/gauge/suggestions.ts @@ -1,89 +1,63 @@ -import { ThresholdsMode, VisualizationSuggestionsBuilder } from '@grafana/data'; -import { GraphFieldConfig } from '@grafana/ui'; -import { SuggestionName } from 'app/types/suggestions'; +import { defaultsDeep } from 'lodash'; + +import { ThresholdsMode, FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplierFn } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { defaultNumericVizOptions } from 'app/features/panel/suggestions/utils'; import { Options } from './panelcfg.gen'; -export class GaugeSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary } = builder; - - if (!dataSummary.hasData || !dataSummary.hasNumberField) { - return; - } - - // for many fields / series this is probably not a good fit - if (dataSummary.numberFieldCount >= 50) { - return; - } - - const list = builder.getListAppender({ - name: SuggestionName.Gauge, - pluginId: 'gauge', - options: {}, - fieldConfig: { - defaults: { - thresholds: { - steps: [ - { value: -Infinity, color: 'green' }, - { value: 70, color: 'orange' }, - { value: 85, color: 'red' }, - ], - mode: ThresholdsMode.Percentage, - }, - custom: {}, +const withDefaults = (suggestion: VisualizationSuggestion): VisualizationSuggestion => + defaultsDeep(suggestion, { + fieldConfig: { + defaults: { + thresholds: { + steps: [ + { value: -Infinity, color: 'green' }, + { value: 70, color: 'orange' }, + { value: 85, color: 'red' }, + ], + mode: ThresholdsMode.Percentage, }, - overrides: [], + custom: {}, }, - cardOptions: { - previewModifier: (s) => { - if (s.options?.reduceOptions?.values) { - s.options.reduceOptions.limit = 2; - } - }, + overrides: [], + }, + cardOptions: { + previewModifier: (s) => { + if (s.options?.reduceOptions?.values) { + s.options.reduceOptions.limit = 2; + } }, - }); + }, + } satisfies VisualizationSuggestion); - if (dataSummary.hasStringField && dataSummary.frameCount === 1 && dataSummary.rowCountTotal < 10) { - list.append({ - name: SuggestionName.Gauge, - options: { - reduceOptions: { - values: true, - calcs: [], - }, - }, - }); - list.append({ - name: SuggestionName.GaugeNoThresholds, - options: { - reduceOptions: { - values: true, - calcs: [], - }, - showThresholdMarkers: false, - }, - }); - } else { - list.append({ - name: SuggestionName.Gauge, - options: { - reduceOptions: { - values: false, - calcs: ['lastNotNull'], - }, - }, - }); - list.append({ - name: SuggestionName.GaugeNoThresholds, - options: { - reduceOptions: { - values: false, - calcs: ['lastNotNull'], - }, - showThresholdMarkers: false, - }, - }); - } +const GAUGE_LIMIT = 10; + +export const gaugeSuggestionsSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => { + if (!dataSummary.hasData || !dataSummary.hasFieldType(FieldType.number)) { + return; } -} + + // for many fields / series this is probably not a good fit + if (dataSummary.fieldCountByType(FieldType.number) > GAUGE_LIMIT) { + return; + } + + const suggestions: Array> = [ + { name: t('gauge.suggestions.arc', 'Gauge') }, + { + name: t('gauge.suggestions.no-thresholds', 'Gauge - no thresholds'), + options: { + showThresholdMarkers: false, + }, + }, + ]; + + // sometimes, we want to de-aggregate the data for the gauge suggestion + const shouldUseRawValues = + dataSummary.hasFieldType(FieldType.string) && + dataSummary.frameCount === 1 && + dataSummary.rowCountTotal <= GAUGE_LIMIT; + + return suggestions.map((s) => defaultNumericVizOptions(withDefaults(s), dataSummary, shouldUseRawValues)); +}; diff --git a/public/app/plugins/panel/geomap/module.tsx b/public/app/plugins/panel/geomap/module.tsx index 7573f9317aa..a88a3bfbe02 100644 --- a/public/app/plugins/panel/geomap/module.tsx +++ b/public/app/plugins/panel/geomap/module.tsx @@ -8,6 +8,7 @@ import { LayersEditor } from './editor/LayersEditor'; import { MapViewEditor } from './editor/MapViewEditor'; import { getLayerEditor } from './editor/layerEditor'; import { mapPanelChangedHandler, mapMigrationHandler } from './migrations'; +import { geomapSuggestionsSupplier } from './suggestions'; import { defaultMapViewConfig, Options, TooltipMode, GeomapInstanceState } from './types'; export const plugin = new PanelPlugin(GeomapPanel) @@ -171,4 +172,5 @@ export const plugin = new PanelPlugin(GeomapPanel) ], }, }); - }); + }) + .setSuggestionsSupplier(geomapSuggestionsSupplier); diff --git a/public/app/plugins/panel/geomap/suggestions.ts b/public/app/plugins/panel/geomap/suggestions.ts new file mode 100644 index 00000000000..97fe03a7739 --- /dev/null +++ b/public/app/plugins/panel/geomap/suggestions.ts @@ -0,0 +1,46 @@ +import { VisualizationSuggestionScore, VisualizationSuggestionsSupplierFn } from '@grafana/data'; +import { GraphFieldConfig } from '@grafana/ui'; +import { getGeometryField, getDefaultLocationMatchers } from 'app/features/geo/utils/location'; + +import { Options } from './panelcfg.gen'; + +export const geomapSuggestionsSupplier: VisualizationSuggestionsSupplierFn = ( + dataSummary +) => { + if (!dataSummary.hasData || !dataSummary.rawFrames) { + return; + } + + // use getGeometryField to see if any frames have geolocation info + const location = getDefaultLocationMatchers(); + if (!dataSummary.rawFrames.some((frame) => !getGeometryField(frame, location).warning)) { + return; + } + + return [ + { + score: VisualizationSuggestionScore.Best, + fieldConfig: { + defaults: { + custom: {}, + }, + overrides: [], + }, + cardOptions: { + previewModifier: (s) => { + s.options!.controls = { + showZoom: false, + showScale: false, + showAttribution: false, + showMeasure: false, + }; + // FIXME: this doesn't work. I want to disable legends in the preview. + s.options?.layers?.forEach((layer) => { + layer.config = layer.config || {}; + layer.config.showLegend = false; + }); + }, + }, + }, + ]; +}; diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index 897fdbda461..a7d44726886 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -18,7 +18,7 @@ import { HeatmapPanel } from './HeatmapPanel'; import { prepareHeatmapData } from './fields'; import { heatmapChangedHandler, heatmapMigrationHandler } from './migrations'; import { colorSchemes, quantizeScheme } from './palettes'; -import { HeatmapSuggestionsSupplier } from './suggestions'; +import { heatmapSuggestionsSupplier } from './suggestions'; import { Options, defaultOptions, HeatmapColorMode, HeatmapColorScale } from './types'; export const plugin = new PanelPlugin(HeatmapPanel) @@ -472,5 +472,5 @@ export const plugin = new PanelPlugin(HeatmapPanel) annotations?.some((df) => df.meta?.custom?.resultType === 'exemplar'), }); }) - .setSuggestionsSupplier(new HeatmapSuggestionsSupplier()) + .setSuggestionsSupplier(heatmapSuggestionsSupplier) .setDataSupport({ annotations: true }); diff --git a/public/app/plugins/panel/heatmap/suggestions.test.ts b/public/app/plugins/panel/heatmap/suggestions.test.ts new file mode 100644 index 00000000000..ef3e73bc0aa --- /dev/null +++ b/public/app/plugins/panel/heatmap/suggestions.test.ts @@ -0,0 +1,226 @@ +import { + createDataFrame, + DataFrameType, + FieldType, + getPanelDataSummary, + VisualizationSuggestionScore, +} from '@grafana/data'; + +import { heatmapSuggestionsSupplier } from './suggestions'; + +describe('heatmap suggestions', () => { + describe('applicability', () => { + it('should not suggest for data without time field', () => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + fields: [ + { name: 'value1', type: FieldType.number, values: [1, 2, 3] }, + { name: 'value2', type: FieldType.number, values: [4, 5, 6] }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toBeUndefined(); + }); + + it('should suggest for data with time and number fields', () => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [1609459200000, 1609462800000, 1609466400000] }, + { name: 'value1', type: FieldType.number, values: [1, 2, 3] }, + { name: 'value2', type: FieldType.number, values: [4, 5, 6] }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toHaveLength(1); + }); + }); + + describe('scoring', () => { + it('should score this as "OK" if the data is not particularly heatmap-y', () => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [1609459200000, 1609462800000, 1609466400000] }, + { name: 'value1', type: FieldType.number, values: [1, 2, 3] }, + { name: 'value2', type: FieldType.number, values: [4, 5, 6] }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toEqual([expect.objectContaining({ score: VisualizationSuggestionScore.OK })]); + }); + + it.each([DataFrameType.HeatmapRows, DataFrameType.HeatmapCells])( + 'should score this as "Best" if the data explicitly has %s frame type', + (frameType: DataFrameType) => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + meta: { type: frameType }, + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1609459200000, 1609462800000, 1609466400000], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value1', + type: FieldType.number, + values: [1, 2, 3], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value2', + type: FieldType.number, + values: [4, 5, 6], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value3', + type: FieldType.number, + values: [7, 8, 9], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toEqual([expect.objectContaining({ score: VisualizationSuggestionScore.Best })]); + } + ); + + it('should score this as "Best" if the data has "ge" labels', () => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1609459200000, 1609462800000, 1609466400000], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value1', + type: FieldType.number, + values: [1, 2, 3], + labels: { ge: '-Inf' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value2', + type: FieldType.number, + values: [4, 5, 6], + labels: { ge: '0' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value3', + type: FieldType.number, + values: [7, 8, 9], + labels: { ge: '10' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toEqual([expect.objectContaining({ score: VisualizationSuggestionScore.Best })]); + }); + + it('should score this as "Best" if the data has "le" labels', () => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1609459200000, 1609462800000, 1609466400000], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value1', + type: FieldType.number, + values: [1, 2, 3], + labels: { le: '1' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value2', + type: FieldType.number, + values: [4, 5, 6], + labels: { le: '2' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value2', + type: FieldType.number, + values: [6, 2, 6], + labels: { le: '4' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value3', + type: FieldType.number, + values: [7, 8, 9], + labels: { le: 'Inf' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toEqual([expect.objectContaining({ score: VisualizationSuggestionScore.Best })]); + }); + + it('should score this as "Best" if the field names are numeric in a way that makes sense', () => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1609459200000, 1609462800000, 1609466400000], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: '0', + type: FieldType.number, + values: [1, 2, 3], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: '10', + type: FieldType.number, + values: [4, 5, 6], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: '20', + type: FieldType.number, + values: [7, 8, 9], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'Inf', + type: FieldType.number, + values: [10, 9, 8], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toEqual([expect.objectContaining({ score: VisualizationSuggestionScore.Best })]); + }); + }); +}); diff --git a/public/app/plugins/panel/heatmap/suggestions.ts b/public/app/plugins/panel/heatmap/suggestions.ts index f49de761e85..670f76cda63 100644 --- a/public/app/plugins/panel/heatmap/suggestions.ts +++ b/public/app/plugins/panel/heatmap/suggestions.ts @@ -1,45 +1,73 @@ -import { VisualizationSuggestionsBuilder } from '@grafana/data'; +import { + DataFrameType, + FieldType, + PanelDataSummary, + VisualizationSuggestionScore, + VisualizationSuggestionsSupplierFn, +} from '@grafana/data'; import { config } from '@grafana/runtime'; +import { GraphFieldConfig } from '@grafana/schema'; import { prepareHeatmapData } from './fields'; import { quantizeScheme } from './palettes'; import { Options, defaultOptions } from './types'; -export class HeatmapSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary } = builder; - - if ( - !builder.data?.series || - !dataSummary.hasData || - dataSummary.timeFieldCount < 1 || - dataSummary.numberFieldCount < 2 || - dataSummary.numberFieldCount > 10 - ) { - return; - } - - const palette = quantizeScheme(defaultOptions.color, config.theme2); - const info = prepareHeatmapData({ - frames: builder.data.series, - options: defaultOptions, - palette, - theme: config.theme2, - }); - if (!info || info.warning) { - return; - } - - builder.getListAppender({ - name: '', - pluginId: 'heatmap', - options: {}, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], - }, - }); +function determineScore(dataSummary: PanelDataSummary): VisualizationSuggestionScore { + // look to see if the data has an explicity marker for heatmap data on it. + if ([DataFrameType.HeatmapRows, DataFrameType.HeatmapCells].some((t) => dataSummary.hasDataFrameType(t))) { + return VisualizationSuggestionScore.Best; } + + // we'll also look more closely at frames which return between 3 and 10 numeric fields. + if (dataSummary.fieldCountByType(FieldType.number) > 2 || dataSummary.fieldCountByType(FieldType.number) <= 10) { + // look through the names of the panels + const hasPotentialHeatmapSeries = dataSummary.rawFrames!.some((frame) => { + for (const field of frame.fields) { + if (field.type === FieldType.number) { + // if the field name, or "ge" or "le" label on the field, are numeric, then it's very possibly part of a heatmap. + if ([field.name, field.labels?.ge, field.labels?.le].some((v) => !isNaN(Number(v)))) { + return true; + } + } + } + return false; + }); + + // if at least all but 1 of the numeric fields in the frame have numeric names, then this is probably a heatmap. + // (the out-of-place one would be "Inf" or "-Inf") + if (hasPotentialHeatmapSeries) { + return VisualizationSuggestionScore.Best; + } + } + + return VisualizationSuggestionScore.OK; } + +export const heatmapSuggestionsSupplier: VisualizationSuggestionsSupplierFn = ( + dataSummary: PanelDataSummary +) => { + if ( + !dataSummary.rawFrames || + !dataSummary.hasData || + !dataSummary.hasFieldType(FieldType.time) || + !dataSummary.hasFieldType(FieldType.number) + ) { + return; + } + + // parse the frame into a heatmap structure to see if it's possible. + const palette = quantizeScheme(defaultOptions.color, config.theme2); + const info = prepareHeatmapData({ + frames: dataSummary.rawFrames, + options: defaultOptions, + palette, + theme: config.theme2, + }); + + // if we can't parse the data into a heatmap, then bail out and prevent showing suggestions. + if (!info || info.warning) { + return; + } + + return [{ score: determineScore(dataSummary) }]; +}; diff --git a/public/app/plugins/panel/histogram/module.tsx b/public/app/plugins/panel/histogram/module.tsx index 4ee2fa97a09..8855287cc93 100644 --- a/public/app/plugins/panel/histogram/module.tsx +++ b/public/app/plugins/panel/histogram/module.tsx @@ -5,6 +5,9 @@ import { identityOverrideProcessor, PanelPlugin, histogramFieldInfo, + buildHistogram, + VisualizationSuggestionScore, + DataFrameType, } from '@grafana/data'; import { t } from '@grafana/i18n'; import { commonOptionsBuilder, getGraphFieldOptions } from '@grafana/ui'; @@ -149,4 +152,16 @@ export const plugin = new PanelPlugin(HistogramPanel) commonOptionsBuilder.addHideFrom(builder); }, + }) + .setSuggestionsSupplier((ds) => { + if (ds.rawFrames && ds.hasData && buildHistogram(ds.rawFrames)) { + return [ + { + score: ds.hasDataFrameType(DataFrameType.Histogram) + ? VisualizationSuggestionScore.Best + : VisualizationSuggestionScore.OK, + }, + ]; + } + return; }); diff --git a/public/app/plugins/panel/logs/module.tsx b/public/app/plugins/panel/logs/module.tsx index 04ed5e92183..e51ff946902 100644 --- a/public/app/plugins/panel/logs/module.tsx +++ b/public/app/plugins/panel/logs/module.tsx @@ -1,10 +1,10 @@ -import { PanelPlugin, LogsSortOrder, LogsDedupStrategy, LogsDedupDescription } from '@grafana/data'; +import { PanelPlugin, LogsSortOrder, LogsDedupStrategy, LogsDedupDescription, FieldType } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; +import { showDefaultSuggestion } from 'app/features/panel/suggestions/utils'; import { LogsPanel } from './LogsPanel'; import { Options } from './panelcfg.gen'; -import { LogsPanelSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(LogsPanel) .setPanelOptions((builder, context) => { @@ -207,4 +207,6 @@ export const plugin = new PanelPlugin(LogsPanel) defaultValue: LogsSortOrder.Descending, }); }) - .setSuggestionsSupplier(new LogsPanelSuggestionsSupplier()); + .setSuggestionsSupplier( + showDefaultSuggestion((ds) => ds.hasData && ds.hasFieldType(FieldType.time) && ds.hasFieldType(FieldType.string)) + ); diff --git a/public/app/plugins/panel/logs/suggestions.ts b/public/app/plugins/panel/logs/suggestions.ts deleted file mode 100644 index 79b804cb8d7..00000000000 --- a/public/app/plugins/panel/logs/suggestions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data'; -import { SuggestionName } from 'app/types/suggestions'; - -import { Options } from './panelcfg.gen'; - -export class LogsPanelSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const list = builder.getListAppender({ - name: '', - pluginId: 'logs', - options: {}, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], - }, - }); - - const { dataSummary: ds } = builder; - - // Require a string & time field - if (!ds.hasData || !ds.hasTimeField || !ds.hasStringField) { - return; - } - - if (ds.preferredVisualisationType === 'logs') { - list.append({ name: SuggestionName.Logs, score: VisualizationSuggestionScore.Best }); - } else { - list.append({ name: SuggestionName.Logs }); - } - } -} diff --git a/public/app/plugins/panel/nodeGraph/module.tsx b/public/app/plugins/panel/nodeGraph/module.tsx index 9bfa3456fc4..4230b30a9a1 100644 --- a/public/app/plugins/panel/nodeGraph/module.tsx +++ b/public/app/plugins/panel/nodeGraph/module.tsx @@ -4,7 +4,7 @@ import { t } from '@grafana/i18n'; import { NodeGraphPanel } from './NodeGraphPanel'; import { ArcOptionsEditor } from './editor/ArcOptionsEditor'; import { LayoutAlgorithm, Options as NodeGraphOptions } from './panelcfg.gen'; -import { NodeGraphSuggestionsSupplier } from './suggestions'; +import { nodeGraphSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(NodeGraphPanel) .useFieldConfig({ @@ -92,4 +92,4 @@ export const plugin = new PanelPlugin(NodeGraphPanel) }, }); }) - .setSuggestionsSupplier(new NodeGraphSuggestionsSupplier()); + .setSuggestionsSupplier(nodeGraphSuggestionsSupplier); diff --git a/public/app/plugins/panel/nodeGraph/suggestions.ts b/public/app/plugins/panel/nodeGraph/suggestions.ts index eb4a4773ef4..d1e5e1e29b4 100644 --- a/public/app/plugins/panel/nodeGraph/suggestions.ts +++ b/public/app/plugins/panel/nodeGraph/suggestions.ts @@ -1,71 +1,60 @@ -import { DataFrame, FieldType, VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data'; -import { SuggestionName } from 'app/types/suggestions'; +import { DataFrame, FieldType, VisualizationSuggestionScore, VisualizationSuggestionsSupplierFn } from '@grafana/data'; -export class NodeGraphSuggestionsSupplier { - getListWithDefaults(builder: VisualizationSuggestionsBuilder) { - return builder.getListAppender<{}, {}>({ - name: SuggestionName.NodeGraph, - pluginId: 'nodeGraph', - }); - } +import { Options } from './panelcfg.gen'; - hasCorrectFields(frames: DataFrame[]): boolean { - let hasNodesFrame = false; - let hasEdgesFrame = false; +function checkFields(fields: Array<[string, FieldType]>, frame: DataFrame): boolean { + let hasCorrectFields = true; - const nodeFields: Array<[string, FieldType]> = [ - ['id', FieldType.string], - ['title', FieldType.string], - ['mainstat', FieldType.number], - ]; - const edgeFields: Array<[string, FieldType]> = [ - ['id', FieldType.string], - ['source', FieldType.string], - ['target', FieldType.string], - ]; - - for (const frame of frames) { - if (this.checkFields(nodeFields, frame)) { - hasNodesFrame = true; - } - if (this.checkFields(edgeFields, frame)) { - hasEdgesFrame = true; - } - } - - return hasNodesFrame && hasEdgesFrame; - } - - checkFields(fields: Array<[string, FieldType]>, frame: DataFrame): boolean { - let hasCorrectFields = true; - - for (const field of fields) { - const [name, type] = field; - const frameField = frame.fields.find((f) => f.name === name); - if (!frameField || type !== frameField.type) { - hasCorrectFields = false; - break; - } - } - - return hasCorrectFields; - } - - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - if (!builder.data) { - return; - } - - const hasCorrectFields = this.hasCorrectFields(builder.data.series); - const nodeGraphFrames = builder.data.series.filter( - (df) => df.meta && df.meta.preferredVisualisationType === 'nodeGraph' - ); - - if (hasCorrectFields || nodeGraphFrames.length === 2) { - this.getListWithDefaults(builder).append({ - name: SuggestionName.NodeGraph, - score: VisualizationSuggestionScore.Best, - }); + for (const field of fields) { + const [name, type] = field; + const frameField = frame.fields.find((f) => f.name === name); + if (!frameField || type !== frameField.type) { + hasCorrectFields = false; + break; } } + + return hasCorrectFields; } + +function frameHasCorrectFields(frames: DataFrame[]): boolean { + let hasNodesFrame = false; + let hasEdgesFrame = false; + + const nodeFields: Array<[string, FieldType]> = [ + ['id', FieldType.string], + ['title', FieldType.string], + ['mainstat', FieldType.number], + ]; + const edgeFields: Array<[string, FieldType]> = [ + ['id', FieldType.string], + ['source', FieldType.string], + ['target', FieldType.string], + ]; + + for (const frame of frames) { + if (checkFields(nodeFields, frame)) { + hasNodesFrame = true; + } + if (checkFields(edgeFields, frame)) { + hasEdgesFrame = true; + } + } + + return hasNodesFrame && hasEdgesFrame; +} + +export const nodeGraphSuggestionsSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => { + if (!dataSummary.rawFrames) { + return; + } + + const hasCorrectFields = frameHasCorrectFields(dataSummary.rawFrames); + const nodeGraphFrames = dataSummary.hasPreferredVisualisationType('nodeGraph'); + + if (!hasCorrectFields && !nodeGraphFrames) { + return; + } + + return [{ score: VisualizationSuggestionScore.Best }]; +}; diff --git a/public/app/plugins/panel/piechart/module.tsx b/public/app/plugins/panel/piechart/module.tsx index 33726bf3a0f..683a0cc0b6a 100644 --- a/public/app/plugins/panel/piechart/module.tsx +++ b/public/app/plugins/panel/piechart/module.tsx @@ -9,7 +9,7 @@ import { addStandardDataReduceOptions } from '../stat/common'; import { PieChartPanel } from './PieChartPanel'; import { PieChartPanelChangedHandler } from './migrations'; import { Options, FieldConfig, PieChartType, PieChartLabels, PieChartLegendValues } from './panelcfg.gen'; -import { PieChartSuggestionsSupplier } from './suggestions'; +import { piechartSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(PieChartPanel) .setPanelChangeHandler(PieChartPanelChangedHandler) @@ -92,4 +92,4 @@ export const plugin = new PanelPlugin(PieChartPanel) showIf: (c) => c.legend.showLegend !== false, }); }) - .setSuggestionsSupplier(new PieChartSuggestionsSupplier()); + .setSuggestionsSupplier(piechartSuggestionsSupplier); diff --git a/public/app/plugins/panel/piechart/suggestions.ts b/public/app/plugins/panel/piechart/suggestions.ts index fd4beed5e6a..405d5722796 100644 --- a/public/app/plugins/panel/piechart/suggestions.ts +++ b/public/app/plugins/panel/piechart/suggestions.ts @@ -1,79 +1,78 @@ -import { VisualizationSuggestionsBuilder } from '@grafana/data'; +import { defaultsDeep } from 'lodash'; + +import { + FieldType, + VisualizationSuggestion, + VisualizationSuggestionScore, + VisualizationSuggestionsSupplierFn, +} from '@grafana/data'; +import { t } from '@grafana/i18n'; import { LegendDisplayMode } from '@grafana/schema'; -import { SuggestionName } from 'app/types/suggestions'; +import { defaultNumericVizOptions } from 'app/features/panel/suggestions/utils'; import { PieChartLabels, Options, PieChartType } from './panelcfg.gen'; -export class PieChartSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const list = builder.getListAppender({ - name: SuggestionName.PieChart, - pluginId: 'piechart', - options: { - reduceOptions: { - values: false, - calcs: ['lastNotNull'], - }, - displayLabels: [PieChartLabels.Percent], - legend: { - calcs: [], - displayMode: LegendDisplayMode.Hidden, - placement: 'right', - values: [], - showLegend: false, - }, +const withDefaults = (suggestion: VisualizationSuggestion): VisualizationSuggestion => + defaultsDeep(suggestion, { + options: { + displayLabels: [PieChartLabels.Percent], + legend: { + calcs: [], + displayMode: LegendDisplayMode.Hidden, + placement: 'right', + values: [], + showLegend: false, }, - }); + }, + } satisfies VisualizationSuggestion); - const { dataSummary } = builder; +const SLICE_MAX = 30; +const SLICE_MIN = 2; - if (!dataSummary.hasNumberField) { - return; - } +export const piechartSuggestionsSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => { + if (!dataSummary.hasFieldType(FieldType.number)) { + return; + } - if (dataSummary.hasStringField && dataSummary.frameCount === 1) { - // if many values this or single value PieChart is not a good option - if (dataSummary.rowCountTotal > 30 || dataSummary.rowCountTotal < 2) { - return; - } - - list.append({ - name: SuggestionName.PieChart, - options: { - reduceOptions: { - values: true, - calcs: [], - }, - }, - }); - - list.append({ - name: SuggestionName.PieChartDonut, - options: { - reduceOptions: { - values: true, - calcs: [], - }, - pieType: PieChartType.Donut, - }, - }); - - return; - } - - if (dataSummary.numberFieldCount > 30 || dataSummary.numberFieldCount < 2) { - return; - } - - list.append({ - name: SuggestionName.PieChart, - }); - - list.append({ - name: SuggestionName.PieChartDonut, + const suggestions: Array> = [ + { + name: t('piechart.suggestions.pie', 'Pie chart'), + }, + { + name: t('piechart.suggestions.donut', 'Donut chart'), options: { pieType: PieChartType.Donut, }, - }); + }, + ]; + + let shouldUseRawValues = false; + + // we're filtering out data which has more than 30 slices or less than 2, and we're also + // determining whether the reduce options should be set based on the data summary. + if (dataSummary.hasFieldType(FieldType.string) && dataSummary.frameCount === 1) { + if (dataSummary.rowCountTotal > SLICE_MAX && dataSummary.rowCountTotal < SLICE_MIN) { + return; + } + + shouldUseRawValues = true; + } else if ( + dataSummary.fieldCountByType(FieldType.number) > SLICE_MAX || + dataSummary.fieldCountByType(FieldType.number) < SLICE_MIN + ) { + return; } -} + + return suggestions.map((s) => { + const result = defaultNumericVizOptions(withDefaults(s), dataSummary, shouldUseRawValues); + // bump the score up to best if we have exactly one numeric and one string field + if ( + dataSummary.fieldCount === 2 && + dataSummary.fieldCountByType(FieldType.string) === 1 && + dataSummary.fieldCountByType(FieldType.number) === 1 + ) { + result.score = VisualizationSuggestionScore.Best; + } + return result; + }); +}; diff --git a/public/app/plugins/panel/radialbar/module.tsx b/public/app/plugins/panel/radialbar/module.tsx index dfbdb875865..33212d3ce1c 100644 --- a/public/app/plugins/panel/radialbar/module.tsx +++ b/public/app/plugins/panel/radialbar/module.tsx @@ -8,7 +8,7 @@ import { EffectsEditor } from './EffectsEditor'; import { gaugePanelChangedHandler, gaugePanelMigrationHandler, shouldMigrateGauge } from './GaugeMigrations'; import { RadialBarPanel } from './RadialBarPanel'; import { defaultGaugePanelEffects, defaultOptions, Options } from './panelcfg.gen'; -import { radialBarSuggestionsHandler } from './suggestions'; +import { radialBarSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(RadialBarPanel) .useFieldConfig({}) @@ -112,6 +112,6 @@ export const plugin = new PanelPlugin(RadialBarPanel) defaultValue: defaultGaugePanelEffects, }); }) - .setSuggestionsSupplier(radialBarSuggestionsHandler) + .setSuggestionsSupplier(radialBarSuggestionsSupplier) .setMigrationHandler(gaugePanelMigrationHandler, shouldMigrateGauge) .setPanelChangeHandler(gaugePanelChangedHandler); diff --git a/public/app/plugins/panel/radialbar/suggestions.test.ts b/public/app/plugins/panel/radialbar/suggestions.test.ts index 21a59267bbb..0d8ead08c87 100644 --- a/public/app/plugins/panel/radialbar/suggestions.test.ts +++ b/public/app/plugins/panel/radialbar/suggestions.test.ts @@ -1,13 +1,13 @@ import { createDataFrame, Field, FieldType, getPanelDataSummary } from '@grafana/data'; -import { radialBarSuggestionsHandler } from './suggestions'; +import { radialBarSuggestionsSupplier } from './suggestions'; describe('RadialBarPanel Suggestions', () => { it('does not suggest gauge if no data is present', () => { - expect(radialBarSuggestionsHandler(getPanelDataSummary([]))).toBeFalsy(); - expect(radialBarSuggestionsHandler(getPanelDataSummary(undefined))).toBeFalsy(); + expect(radialBarSuggestionsSupplier(getPanelDataSummary([]))).toBeFalsy(); + expect(radialBarSuggestionsSupplier(getPanelDataSummary(undefined))).toBeFalsy(); expect( - radialBarSuggestionsHandler( + radialBarSuggestionsSupplier( getPanelDataSummary([ createDataFrame({ fields: [ @@ -27,7 +27,7 @@ describe('RadialBarPanel Suggestions', () => { { name: 'status', type: FieldType.string }, ], }); - expect(radialBarSuggestionsHandler(getPanelDataSummary([df]))).toBeFalsy(); + expect(radialBarSuggestionsSupplier(getPanelDataSummary([df]))).toBeFalsy(); }); it('does not suggest gauge if there are too many numeric fields', () => { @@ -35,12 +35,12 @@ describe('RadialBarPanel Suggestions', () => { for (let i = 0; i < 20; i++) { fields.push({ name: `numeric-${i}`, type: FieldType.number, values: [0, 100, 200, 300, 400, 500], config: {} }); } - expect(radialBarSuggestionsHandler(getPanelDataSummary([createDataFrame({ fields })]))).toBeFalsy(); + expect(radialBarSuggestionsSupplier(getPanelDataSummary([createDataFrame({ fields })]))).toBeFalsy(); }); it('suggests gauge for a single numeric field', () => { expect( - radialBarSuggestionsHandler( + radialBarSuggestionsSupplier( getPanelDataSummary([ createDataFrame({ fields: [ @@ -58,7 +58,7 @@ describe('RadialBarPanel Suggestions', () => { it('suggests gauge for a few numeric fields, with other fields mixed in', () => { expect( - radialBarSuggestionsHandler( + radialBarSuggestionsSupplier( getPanelDataSummary([ createDataFrame({ fields: [ @@ -136,7 +136,7 @@ describe('RadialBarPanel Suggestions', () => { ], }, ])('$description suggests aggregated=$aggregated', ({ dataframes, aggregated }) => { - const suggestions = radialBarSuggestionsHandler(getPanelDataSummary(dataframes)); + const suggestions = radialBarSuggestionsSupplier(getPanelDataSummary(dataframes)); const expected = aggregated ? { values: false, calcs: ['lastNotNull'] } : { values: true, calcs: [] }; if (Array.isArray(suggestions)) { for (const suggestion of suggestions) { diff --git a/public/app/plugins/panel/radialbar/suggestions.ts b/public/app/plugins/panel/radialbar/suggestions.ts index 35cde48ec0f..f8d13a096b3 100644 --- a/public/app/plugins/panel/radialbar/suggestions.ts +++ b/public/app/plugins/panel/radialbar/suggestions.ts @@ -8,10 +8,39 @@ import { } from '@grafana/data'; import { t } from '@grafana/i18n'; import { GraphFieldConfig } from '@grafana/ui'; +import { defaultNumericVizOptions } from 'app/features/panel/suggestions/utils'; import { Options } from './panelcfg.gen'; -export const radialBarSuggestionsHandler: VisualizationSuggestionsSupplierFn = ( +const withDefaults = ( + suggestion: VisualizationSuggestion +): VisualizationSuggestion => + defaultsDeep(suggestion, { + cardOptions: { + previewModifier: (s) => { + if (s.options?.reduceOptions) { + s.options.reduceOptions.limit = 4; + } + }, + }, + // styles: [{ + // name: t('gauge.suggestions.style.circular', 'Glowing'), + // options: { + // effects: { + // rounded: true, + // barGlow: true, + // centerGlow: true, + // spotlight: true, + // }, + // }, + // }, { + // name: t('gauge.suggestions.style.simple', 'Simple'), + // }] + } satisfies VisualizationSuggestion); + +const MAX_GAUGES = 10; + +export const radialBarSuggestionsSupplier: VisualizationSuggestionsSupplierFn = ( dataSummary ) => { if (!dataSummary.hasData || !dataSummary.hasFieldType(FieldType.number)) { @@ -19,69 +48,40 @@ export const radialBarSuggestionsHandler: VisualizationSuggestionsSupplierFn= 10) { + if (dataSummary.fieldCountByType(FieldType.number) > MAX_GAUGES) { return; } - const withDefaults = ( - suggestion: VisualizationSuggestion - ): VisualizationSuggestion => { - // if there is a string field and there are few enough rows, we assume it's tabular data and not numeric series data, - // and the de-aggregated version of the viz probably makes more sense - const isTabularData = - dataSummary.hasFieldType(FieldType.string) && dataSummary.frameCount === 1 && dataSummary.rowCountTotal < 10; - return defaultsDeep(suggestion, { - options: { - reduceOptions: isTabularData - ? { - values: true, - calcs: [], - } - : { - values: false, - calcs: ['lastNotNull'], - }, - }, - fieldConfig: { - defaults: isTabularData - ? { - color: { mode: FieldColorModeId.PaletteClassic }, - } - : {}, - overrides: [], - }, - cardOptions: { - previewModifier: (s) => { - if (s.options?.reduceOptions) { - s.options.reduceOptions.limit = 4; - } - }, - }, - // styles: [{ - // name: t('gauge.suggestions.style.circular', 'Glowing'), - // options: { - // effects: { - // rounded: true, - // barGlow: true, - // centerGlow: true, - // spotlight: true, - // }, - // }, - // }, { - // name: t('gauge.suggestions.style.simple', 'Simple'), - // }] - } satisfies VisualizationSuggestion); - }; - - return [ - withDefaults({ name: t('gauge.suggestions.arc', 'Gauge') }), - withDefaults({ + const suggestions: Array> = [ + { name: t('gauge.suggestions.arc', 'Gauge') }, + { name: t('gauge.suggestions.circular', 'Circular gauge'), options: { shape: 'circle', showThresholdMarkers: false, barWidthFactor: 0.3, }, - }), + }, ]; + + const shouldUseRawValues = + dataSummary.hasFieldType(FieldType.string) && + dataSummary.frameCount === 1 && + dataSummary.rowCountTotal <= MAX_GAUGES; + + return suggestions.map((s) => { + const suggestion = defaultNumericVizOptions(withDefaults(s), dataSummary, shouldUseRawValues); + + if (shouldUseRawValues) { + suggestion.fieldConfig = suggestion.fieldConfig ?? { + defaults: {}, + overrides: [], + }; + suggestion.fieldConfig.defaults.color = suggestion.fieldConfig.defaults.color ?? { + mode: FieldColorModeId.PaletteClassic, + }; + } + + return suggestion; + }); }; diff --git a/public/app/plugins/panel/stat/module.tsx b/public/app/plugins/panel/stat/module.tsx index 969e010352c..ecb146a851b 100644 --- a/public/app/plugins/panel/stat/module.tsx +++ b/public/app/plugins/panel/stat/module.tsx @@ -13,7 +13,7 @@ import { statPanelChangedHandler } from './StatMigrations'; import { StatPanel } from './StatPanel'; import { addStandardDataReduceOptions, addOrientationOption } from './common'; import { defaultOptions, Options } from './panelcfg.gen'; -import { StatSuggestionsSupplier } from './suggestions'; +import { statSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(StatPanel) .useFieldConfig() @@ -137,5 +137,5 @@ export const plugin = new PanelPlugin(StatPanel) }) .setNoPadding() .setPanelChangeHandler(statPanelChangedHandler) - .setSuggestionsSupplier(new StatSuggestionsSupplier()) + .setSuggestionsSupplier(statSuggestionsSupplier) .setMigrationHandler(sharedSingleStatMigrationHandler); diff --git a/public/app/plugins/panel/stat/suggestions.ts b/public/app/plugins/panel/stat/suggestions.ts index 00af3220135..14a45d4b52d 100644 --- a/public/app/plugins/panel/stat/suggestions.ts +++ b/public/app/plugins/panel/stat/suggestions.ts @@ -1,41 +1,46 @@ -import { VisualizationSuggestionsBuilder } from '@grafana/data'; -import { BigValueColorMode, BigValueGraphMode, GraphFieldConfig } from '@grafana/schema'; -import { SuggestionName } from 'app/types/suggestions'; +import { defaultsDeep } from 'lodash'; + +import { FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplierFn } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { BigValueColorMode, BigValueGraphMode } from '@grafana/schema'; import { Options } from './panelcfg.gen'; -export class StatSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary: ds } = builder; - - if (!ds.hasData) { - return; - } - - const list = builder.getListAppender({ - name: SuggestionName.Stat, - pluginId: 'stat', - options: {}, - fieldConfig: { - defaults: { - unit: 'short', - custom: {}, - }, - overrides: [], +const withDefaults = (s: VisualizationSuggestion): VisualizationSuggestion => + defaultsDeep(s, { + fieldConfig: { + defaults: { + unit: 'short', + custom: {}, }, - cardOptions: { - previewModifier: (s) => { - if (s.options?.reduceOptions?.values) { - s.options.reduceOptions.limit = 1; - } - }, + overrides: [], + }, + cardOptions: { + previewModifier: (s) => { + if (s.options?.reduceOptions?.values) { + s.options.reduceOptions.limit = 1; + } }, - }); + }, + } satisfies VisualizationSuggestion); - // String and number field with low row count show individual rows - if (ds.hasStringField && ds.hasNumberField && ds.frameCount === 1 && ds.rowCountTotal < 10) { - list.append({ - name: SuggestionName.Stat, +export const statSuggestionsSupplier: VisualizationSuggestionsSupplierFn = (ds) => { + if (!ds.hasData) { + return; + } + + const suggestions: Array> = []; + + // String and number field with low row count show individual rows + if ( + ds.hasFieldType(FieldType.string) && + ds.hasFieldType(FieldType.number) && + ds.frameCount === 1 && + ds.rowCountTotal < 10 + ) { + suggestions.push( + { + name: t('stat.suggestions.stat-discrete-values', 'Stat - discrete values'), options: { reduceOptions: { values: true, @@ -43,9 +48,9 @@ export class StatSuggestionsSupplier { fields: '/.*/', }, }, - }); - list.append({ - name: SuggestionName.StatColoredBackground, + }, + { + name: t('stat.suggestions.stat-discrete-values-color-background', 'Stat - discrete values - color background'), options: { reduceOptions: { values: true, @@ -54,36 +59,38 @@ export class StatSuggestionsSupplier { }, colorMode: BigValueColorMode.Background, }, - }); - } + } + ); + } - // Just a single string field - if (ds.stringFieldCount === 1 && ds.frameCount === 1 && ds.rowCountTotal < 10 && ds.fieldCount === 1) { - list.append({ - name: SuggestionName.Stat, - options: { - reduceOptions: { - values: true, - calcs: [], - fields: '/.*/', - }, - colorMode: BigValueColorMode.None, + // just a single string field + if (ds.fieldCount === 1 && ds.hasFieldType(FieldType.string)) { + suggestions.push({ + name: t('stat.suggestions.stat-single-string', 'Stat - single string'), + options: { + reduceOptions: { + values: true, + calcs: [], + fields: '/.*/', }, - }); - } + colorMode: BigValueColorMode.None, + }, + }); + } - if (ds.hasNumberField && ds.hasTimeField) { - list.append({ + // aggregated suggestions for number fields + if (ds.hasFieldType(FieldType.number) && ds.hasFieldType(FieldType.time)) { + suggestions.push( + { options: { reduceOptions: { values: false, calcs: ['lastNotNull'], }, }, - }); - - list.append({ - name: SuggestionName.StatColoredBackground, + }, + { + name: t('stat.suggestions.stat-color-background', 'Stat - color background'), options: { reduceOptions: { values: false, @@ -92,7 +99,9 @@ export class StatSuggestionsSupplier { graphMode: BigValueGraphMode.None, colorMode: BigValueColorMode.Background, }, - }); - } + } + ); } -} + + return suggestions.map(withDefaults); +}; diff --git a/public/app/plugins/panel/state-timeline/module.tsx b/public/app/plugins/panel/state-timeline/module.tsx index 55fd557eea0..89acb338825 100644 --- a/public/app/plugins/panel/state-timeline/module.tsx +++ b/public/app/plugins/panel/state-timeline/module.tsx @@ -177,7 +177,7 @@ export const plugin = new PanelPlugin(StateTimelinePanel) } // Probably better ways to filter out this by inspecting the types of string values so view this as temporary - if (ds.preferredVisualisationType === 'logs') { + if (ds.hasPreferredVisualisationType('logs')) { return; } diff --git a/public/app/plugins/panel/status-history/module.tsx b/public/app/plugins/panel/status-history/module.tsx index 1e07588d664..eed5e3546a8 100644 --- a/public/app/plugins/panel/status-history/module.tsx +++ b/public/app/plugins/panel/status-history/module.tsx @@ -1,11 +1,10 @@ -import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data'; +import { FieldColorModeId, FieldConfigProperty, FieldType, PanelPlugin } from '@grafana/data'; import { t } from '@grafana/i18n'; import { AxisPlacement, VisibilityMode } from '@grafana/schema'; import { commonOptionsBuilder } from '@grafana/ui'; import { StatusHistoryPanel } from './StatusHistoryPanel'; import { Options, FieldConfig, defaultFieldConfig } from './panelcfg.gen'; -import { StatusHistorySuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(StatusHistoryPanel) .useFieldConfig({ @@ -113,5 +112,42 @@ export const plugin = new PanelPlugin(StatusHistoryPanel) commonOptionsBuilder.addLegendOptions(builder, false); commonOptionsBuilder.addTooltipOptions(builder); }) - .setSuggestionsSupplier(new StatusHistorySuggestionsSupplier()) + .setSuggestionsSupplier((ds) => { + if (!ds.hasData) { + return; + } + + // This panel needs a time field and a string or number field + if ( + !ds.hasFieldType(FieldType.time) || + (!ds.hasFieldType(FieldType.string) && !ds.hasFieldType(FieldType.number)) + ) { + return; + } + + // If there are many series then they won't fit on y-axis so this panel is not good fit + if (ds.fieldCountByType(FieldType.number) >= 30) { + return; + } + + // if there a lot of data points for each series then this is not a good match + if (ds.rowCountMax > 100) { + return; + } + + // Probably better ways to filter out this by inspecting the types of string values so view this as temporary + if (ds.hasPreferredVisualisationType('logs')) { + return; + } + + return [ + { + cardOptions: { + previewModifier: (s) => { + s.options!.colWidth = 0.7; + }, + }, + }, + ]; + }) .setDataSupport({ annotations: true }); diff --git a/public/app/plugins/panel/status-history/suggestions.ts b/public/app/plugins/panel/status-history/suggestions.ts deleted file mode 100644 index 48add53c83d..00000000000 --- a/public/app/plugins/panel/status-history/suggestions.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { FieldColorModeId, VisualizationSuggestionsBuilder } from '@grafana/data'; -import { SuggestionName } from 'app/types/suggestions'; - -import { Options, FieldConfig } from './panelcfg.gen'; - -export class StatusHistorySuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary: ds } = builder; - - if (!ds.hasData) { - return; - } - - // This panel needs a time field and a string or number field - if (!ds.hasTimeField || (!ds.hasStringField && !ds.hasNumberField)) { - return; - } - - // If there are many series then they won't fit on y-axis so this panel is not good fit - if (ds.numberFieldCount >= 30) { - return; - } - - // if there a lot of data points for each series then this is not a good match - if (ds.rowCountMax > 100) { - return; - } - - // Probably better ways to filter out this by inspecting the types of string values so view this as temporary - if (ds.preferredVisualisationType === 'logs') { - return; - } - - const list = builder.getListAppender({ - name: '', - pluginId: 'status-history', - options: {}, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - custom: {}, - }, - overrides: [], - }, - cardOptions: { - previewModifier: (s) => { - s.options!.colWidth = 0.7; - }, - }, - }); - - list.append({ name: SuggestionName.StatusHistory }); - } -} diff --git a/public/app/plugins/panel/table/module.tsx b/public/app/plugins/panel/table/module.tsx index e2ee5b8e97f..5a3a1d21fac 100644 --- a/public/app/plugins/panel/table/module.tsx +++ b/public/app/plugins/panel/table/module.tsx @@ -13,7 +13,7 @@ import { TableCellOptionEditor } from './TableCellOptionEditor'; import { TablePanel } from './TablePanel'; import { tableMigrationHandler, tablePanelChangedHandler } from './migrations'; import { Options, defaultOptions, FieldConfig } from './panelcfg.gen'; -import { TableSuggestionsSupplier } from './suggestions'; +import { tableSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(TablePanel) .setPanelChangeHandler(tablePanelChangedHandler) @@ -227,4 +227,4 @@ export const plugin = new PanelPlugin(TablePanel) defaultValue: defaultOptions?.enablePagination, }); }) - .setSuggestionsSupplier(new TableSuggestionsSupplier()); + .setSuggestionsSupplier(tableSuggestionsSupplier); diff --git a/public/app/plugins/panel/table/suggestions.ts b/public/app/plugins/panel/table/suggestions.ts index bcfead0fe31..98fc085e1cf 100644 --- a/public/app/plugins/panel/table/suggestions.ts +++ b/public/app/plugins/panel/table/suggestions.ts @@ -1,38 +1,33 @@ -import { VisualizationSuggestionsBuilder } from '@grafana/data'; -import { TableFieldOptions } from '@grafana/schema'; +import { PanelDataSummary, VisualizationSuggestionScore, VisualizationSuggestionsSupplierFn } from '@grafana/data'; import icnTablePanelSvg from 'app/plugins/panel/table/img/icn-table-panel.svg'; -import { SuggestionName } from 'app/types/suggestions'; -import { Options } from './panelcfg.gen'; +import { Options, FieldConfig } from './panelcfg.gen'; -export class TableSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const list = builder.getListAppender({ - name: SuggestionName.Table, - pluginId: 'table', - options: {}, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], - }, - cardOptions: { - previewModifier: (s) => { - s.fieldConfig!.defaults.custom!.minWidth = 50; - }, - }, - }); - - // If there are not data suggest table anyway but use icon instead of real preview - if (builder.dataSummary.fieldCount === 0) { - list.append({ - cardOptions: { - imgSrc: icnTablePanelSvg, - }, - }); - } else { - list.append({}); - } +function getTableSuggestionScore(dataSummary: PanelDataSummary): VisualizationSuggestionScore { + if (dataSummary.hasPreferredVisualisationType('table')) { + return VisualizationSuggestionScore.Best; } + + // table is best suited to showing many fields with many rows. + if (dataSummary.fieldCountMax > 5 && dataSummary.rowCountMax > 50) { + return VisualizationSuggestionScore.Good; + } + + return VisualizationSuggestionScore.OK; } + +export const tableSuggestionsSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => [ + { + score: getTableSuggestionScore(dataSummary), + cardOptions: { + previewModifier: (s) => { + if (s.fieldConfig && s.fieldConfig.defaults.custom) { + s.fieldConfig.defaults.custom.minWidth = 50; + } + }, + // If there is no data, suggest table anyway, but use icon instead of real preview + // TODO: delete this in once "new" suggestions are fully rolled out + imgSrc: dataSummary.fieldCount === 0 ? icnTablePanelSvg : undefined, + }, + }, +]; diff --git a/public/app/plugins/panel/timeseries/module.tsx b/public/app/plugins/panel/timeseries/module.tsx index beccdf480d6..22b0e3a715a 100644 --- a/public/app/plugins/panel/timeseries/module.tsx +++ b/public/app/plugins/panel/timeseries/module.tsx @@ -8,7 +8,7 @@ import { TimezonesEditor } from './TimezonesEditor'; import { defaultGraphConfig, getGraphFieldConfig } from './config'; import { graphPanelChangedHandler } from './migrations'; import { FieldConfig, Options } from './panelcfg.gen'; -import { TimeSeriesSuggestionsSupplier } from './suggestions'; +import { timeseriesSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(TimeSeriesPanel) .setPanelChangeHandler(graphPanelChangedHandler) @@ -26,5 +26,5 @@ export const plugin = new PanelPlugin(TimeSeriesPanel) defaultValue: undefined, }); }) - .setSuggestionsSupplier(new TimeSeriesSuggestionsSupplier()) + .setSuggestionsSupplier(timeseriesSuggestionsSupplier) .setDataSupport({ annotations: true, alertStates: true }); diff --git a/public/app/plugins/panel/timeseries/suggestions.ts b/public/app/plugins/panel/timeseries/suggestions.ts index d41179d3f36..05a73acdcc7 100644 --- a/public/app/plugins/panel/timeseries/suggestions.ts +++ b/public/app/plugins/panel/timeseries/suggestions.ts @@ -1,9 +1,15 @@ +import { defaultsDeep } from 'lodash'; + import { - FieldColorModeId, - VisualizationSuggestionsBuilder, + DataFrameType, DataTransformerID, + FieldType, PanelPluginVisualizationSuggestion, + VisualizationSuggestion, + VisualizationSuggestionScore, + VisualizationSuggestionsSupplierFn, } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { GraphDrawStyle, GraphFieldConfig, @@ -13,209 +19,142 @@ import { StackingMode, } from '@grafana/schema'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { SuggestionName } from 'app/types/suggestions'; import { Options } from './panelcfg.gen'; -export class TimeSeriesSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary } = builder; +const MAX_BARS = 100; +const MAX_ROWS_SMOOTH_CHART = 200; - if (!dataSummary.hasTimeField || !dataSummary.hasNumberField || dataSummary.rowCountTotal < 2) { - return; - } - - const list = builder.getListAppender({ - name: SuggestionName.LineChart, - pluginId: 'timeseries', - options: { - legend: { - calcs: [], - displayMode: LegendDisplayMode.Hidden, - placement: 'right', - showLegend: false, - }, +const withDefaults = ( + suggestion: VisualizationSuggestion +): VisualizationSuggestion => + defaultsDeep(suggestion, { + options: { + legend: { + calcs: [], + displayMode: LegendDisplayMode.Hidden, + placement: 'right', + showLegend: false, }, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], + }, + fieldConfig: { + defaults: { + custom: {}, }, - cardOptions: { - previewModifier: (s) => { - if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) { - s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2); - } - }, + overrides: [], + }, + cardOptions: { + previewModifier: (s) => { + if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) { + s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2); + } }, - }); + }, + } satisfies VisualizationSuggestion); - const maxBarsCount = 100; +const areaChart = (name: string, stacking?: StackingMode) => ({ + name, + fieldConfig: { + defaults: { + custom: { + fillOpacity: 25, + ...(stacking ? { stacking: { mode: stacking, group: 'A' } } : {}), + }, + }, + overrides: [], + }, +}); - list.append({ - name: SuggestionName.LineChart, - }); +const barChart = (name: string, stacking?: StackingMode) => ({ + name, + fieldConfig: { + defaults: { + custom: { + drawStyle: GraphDrawStyle.Bars, + fillOpacity: 100, + lineWidth: 1, + gradientMode: GraphGradientMode.Hue, + ...(stacking ? { stacking: { mode: stacking, group: 'A' } } : {}), + }, + }, + overrides: [], + }, +}); - if (dataSummary.rowCountMax < 200) { - list.append({ - name: SuggestionName.LineChartSmooth, - fieldConfig: { - defaults: { - custom: { - lineInterpolation: LineInterpolation.Smooth, - }, - }, - overrides: [], - }, - }); - } +// TODO: all "gradient color scheme" suggestions have been removed. they will be re-added as part of the "styles" feature. - // Single series suggestions - if (dataSummary.numberFieldCount === 1) { - list.append({ - name: SuggestionName.AreaChart, - fieldConfig: { - defaults: { - custom: { - fillOpacity: 25, - }, - }, - overrides: [], - }, - }); +export const timeseriesSuggestionsSupplier: VisualizationSuggestionsSupplierFn = ( + dataSummary +) => { + if ( + !dataSummary.hasFieldType(FieldType.time) || + !dataSummary.hasFieldType(FieldType.number) || + dataSummary.rowCountTotal < 2 + ) { + return; + } - list.append({ - name: SuggestionName.LineChartGradientColorScheme, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - custom: { - gradientMode: GraphGradientMode.Scheme, - lineInterpolation: LineInterpolation.Smooth, - lineWidth: 3, - fillOpacity: 20, - }, - }, - overrides: [], - }, - }); + const score: VisualizationSuggestionScore = + dataSummary.hasDataFrameType(DataFrameType.TimeSeriesLong) || + dataSummary.hasDataFrameType(DataFrameType.TimeSeriesWide) || + dataSummary.hasDataFrameType(DataFrameType.TimeSeriesMulti) + ? VisualizationSuggestionScore.Good + : VisualizationSuggestionScore.OK; - if (dataSummary.rowCountMax < maxBarsCount) { - list.append({ - name: SuggestionName.BarChart, - fieldConfig: { - defaults: { - custom: { - drawStyle: GraphDrawStyle.Bars, - fillOpacity: 100, - lineWidth: 1, - gradientMode: GraphGradientMode.Hue, - }, - }, - overrides: [], - }, - }); + const suggestions: Array> = [ + { + name: t('timeseries.suggestions.line', 'Line chart'), + }, + ]; - list.append({ - name: SuggestionName.BarChartGradientColorScheme, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - custom: { - drawStyle: GraphDrawStyle.Bars, - fillOpacity: 90, - lineWidth: 1, - gradientMode: GraphGradientMode.Scheme, - }, - }, - overrides: [], - }, - }); - } - - return; - } - - // Multiple series suggestions - - list.append({ - name: SuggestionName.AreaChartStacked, + if (dataSummary.rowCountMax < MAX_ROWS_SMOOTH_CHART) { + suggestions.push({ + name: t('timeseries.suggestions.line-smooth', 'Line chart - smooth'), fieldConfig: { defaults: { custom: { - fillOpacity: 25, - stacking: { - mode: StackingMode.Normal, - group: 'A', - }, + lineInterpolation: LineInterpolation.Smooth, }, }, overrides: [], }, }); + } - list.append({ - name: SuggestionName.AreaChartStackedPercent, - fieldConfig: { - defaults: { - custom: { - fillOpacity: 25, - stacking: { - mode: StackingMode.Percent, - group: 'A', - }, - }, - }, - overrides: [], - }, - }); + // Single-series suggestions + if (dataSummary.fieldCountByType(FieldType.number) === 1) { + suggestions.push(areaChart(t('timeseries.suggestions.area', 'Area chart'))); - if (dataSummary.rowCountTotal / dataSummary.numberFieldCount < maxBarsCount) { - list.append({ - name: SuggestionName.BarChartStacked, - fieldConfig: { - defaults: { - custom: { - drawStyle: GraphDrawStyle.Bars, - fillOpacity: 100, - lineWidth: 1, - gradientMode: GraphGradientMode.Hue, - stacking: { - mode: StackingMode.Normal, - group: 'A', - }, - }, - }, - overrides: [], - }, - }); - - list.append({ - name: SuggestionName.BarChartStackedPercent, - fieldConfig: { - defaults: { - custom: { - drawStyle: GraphDrawStyle.Bars, - fillOpacity: 100, - lineWidth: 1, - gradientMode: GraphGradientMode.Hue, - stacking: { - mode: StackingMode.Percent, - group: 'A', - }, - }, - }, - overrides: [], - }, - }); + if (dataSummary.rowCountMax < MAX_BARS) { + suggestions.push(barChart(t('timeseries.suggestions.bar', 'Bar chart'))); } } -} + // Multiple series suggestions + else { + suggestions.push( + areaChart(t('timeseries.suggestions.area-stacked', 'Area chart - stacked'), StackingMode.Normal), + areaChart( + t('timeseries.suggestions.area-stacked-percentage', 'Area chart - stacked by percentage'), + StackingMode.Percent + ) + ); + + if (dataSummary.rowCountTotal / dataSummary.fieldCountByType(FieldType.number) < MAX_BARS) { + suggestions.push( + barChart(t('timeseries.suggestions.bar-stacked', 'Bar chart - stacked'), StackingMode.Normal), + barChart( + t('timeseries.suggestions.bar-stacked-percent', 'Bar chart - stacked by percentage'), + StackingMode.Percent + ) + ); + } + } + + return suggestions.map((s) => { + s.score = score; + return withDefaults(s); + }); +}; // This will try to get a suggestion that will add a long to wide conversion export function getPrepareTimeseriesSuggestion(panelId: number): PanelPluginVisualizationSuggestion | undefined { diff --git a/public/app/plugins/panel/traces/module.tsx b/public/app/plugins/panel/traces/module.tsx index fdbe35c6c91..07b72f00d53 100644 --- a/public/app/plugins/panel/traces/module.tsx +++ b/public/app/plugins/panel/traces/module.tsx @@ -1,11 +1,11 @@ import { PanelPlugin } from '@grafana/data'; import { t } from '@grafana/i18n'; +import { showDefaultSuggestion } from 'app/features/panel/suggestions/utils'; import { migrateToAdhocFilters } from '../../../features/explore/TraceView/useSearch'; import { FiltersEditor } from './FiltersEditor'; import { TracesPanel } from './TracesPanel'; -import { TracesSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(TracesPanel) .setMigrationHandler((panel) => { @@ -44,4 +44,4 @@ export const plugin = new PanelPlugin(TracesPanel) category, }); }) - .setSuggestionsSupplier(new TracesSuggestionsSupplier()); + .setSuggestionsSupplier(showDefaultSuggestion((ds) => ds.hasPreferredVisualisationType('trace'))); diff --git a/public/app/plugins/panel/traces/suggestions.ts b/public/app/plugins/panel/traces/suggestions.ts deleted file mode 100644 index bda2e463927..00000000000 --- a/public/app/plugins/panel/traces/suggestions.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data'; -import { SuggestionName } from 'app/types/suggestions'; - -export class TracesSuggestionsSupplier { - getListWithDefaults(builder: VisualizationSuggestionsBuilder) { - return builder.getListAppender<{}, {}>({ - name: SuggestionName.Trace, - pluginId: 'traces', - }); - } - - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - if (!builder.data) { - return; - } - - const dataFrame = builder.data.series[0]; - if (!dataFrame) { - return; - } - - if (builder.data.series[0].meta?.preferredVisualisationType === 'trace') { - this.getListWithDefaults(builder).append({ - name: SuggestionName.Trace, - score: VisualizationSuggestionScore.Best, - }); - } - } -} diff --git a/public/app/plugins/panel/trend/TrendPanel.tsx b/public/app/plugins/panel/trend/TrendPanel.tsx index 95ce2330283..b446c651733 100644 --- a/public/app/plugins/panel/trend/TrendPanel.tsx +++ b/public/app/plugins/panel/trend/TrendPanel.tsx @@ -1,7 +1,6 @@ import { useMemo } from 'react'; import { - isLikelyAscendingVector, DataFrame, FieldMatcherID, fieldMatchers, @@ -10,18 +9,17 @@ import { TimeRange, useDataLinksContext, } from '@grafana/data'; -import { config, PanelDataErrorView } from '@grafana/runtime'; +import { PanelDataErrorView } from '@grafana/runtime'; import { KeyboardPlugin, TooltipDisplayMode, TooltipPlugin2, usePanelContext } from '@grafana/ui'; import { TooltipHoverMode } from '@grafana/ui/internal'; import { XYFieldMatchers } from 'app/core/components/GraphNG/types'; import { preparePlotFrame } from 'app/core/components/GraphNG/utils'; import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; -import { findFieldIndex } from 'app/features/dimensions/utils'; import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip'; -import { prepareGraphableFields } from '../timeseries/utils'; import { Options } from './panelcfg.gen'; +import { prepSeries } from './utils'; export const TrendPanel = ({ data, @@ -51,49 +49,7 @@ export const TrendPanel = ({ return preparePlotFrame(frames, dimFields); }; - const info = useMemo(() => { - if (data.series.length > 1) { - return { - warning: 'Only one frame is supported, consider adding a join transformation', - frames: data.series, - }; - } - - let frames = data.series; - let xFieldIdx: number | undefined; - if (options.xField) { - xFieldIdx = findFieldIndex(options.xField, frames[0]); - if (xFieldIdx == null) { - return { - warning: 'Unable to find field: ' + options.xField, - frames: data.series, - }; - } - } else { - // first number field - // Perhaps we can/should support any ordinal rather than an error here - xFieldIdx = frames[0] ? frames[0].fields.findIndex((f) => f.type === FieldType.number) : -1; - if (xFieldIdx === -1) { - return { - warning: 'No numeric fields found for X axis', - frames, - }; - } - } - - // Make sure values are ascending - if (xFieldIdx != null) { - const field = frames[0].fields[xFieldIdx]; - if (field.type === FieldType.number && !isLikelyAscendingVector(field.values)) { - return { - warning: `Values must be in ascending order`, - frames, - }; - } - } - - return { frames: prepareGraphableFields(frames, config.theme2, undefined, xFieldIdx) }; - }, [data.series, options.xField]); + const info = useMemo(() => prepSeries(data.series, options.xField), [data.series, options.xField]); if (info.warning || !info.frames) { return ( diff --git a/public/app/plugins/panel/trend/module.tsx b/public/app/plugins/panel/trend/module.tsx index ec823e00644..67434a8c542 100644 --- a/public/app/plugins/panel/trend/module.tsx +++ b/public/app/plugins/panel/trend/module.tsx @@ -1,13 +1,14 @@ -import { Field, FieldType, PanelPlugin } from '@grafana/data'; +import { Field, FieldType, PanelPlugin, VisualizationSuggestionScore } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { commonOptionsBuilder } from '@grafana/ui'; +import { GraphDrawStyle } from '@grafana/schema'; +import { commonOptionsBuilder, LegendDisplayMode } from '@grafana/ui'; import { optsWithHideZeros } from '@grafana/ui/internal'; import { defaultGraphConfig, getGraphFieldConfig } from '../timeseries/config'; import { TrendPanel } from './TrendPanel'; import { FieldConfig, Options } from './panelcfg.gen'; -import { TrendSuggestionsSupplier } from './suggestions'; +import { prepSeries } from './utils'; export const plugin = new PanelPlugin(TrendPanel) .useFieldConfig(getGraphFieldConfig(defaultGraphConfig, false)) @@ -29,5 +30,47 @@ export const plugin = new PanelPlugin(TrendPanel) commonOptionsBuilder.addTooltipOptions(builder, false, true, optsWithHideZeros); commonOptionsBuilder.addLegendOptions(builder); }) - .setSuggestionsSupplier(new TrendSuggestionsSupplier()); + .setSuggestionsSupplier((ds) => { + if ( + !ds.rawFrames || + ds.fieldCountByType(FieldType.number) < 2 || + ds.rowCountTotal < 2 || + ds.rowCountTotal < 2 || + ds.frameCount > 1 + ) { + return; + } + + const info = prepSeries(ds.rawFrames); + if (info.warning || !info.frames) { + return; + } + + return [ + { + score: VisualizationSuggestionScore.Good, + options: { + legend: { + calcs: [], + displayMode: LegendDisplayMode.Hidden, + placement: 'right', + showLegend: false, + }, + }, + fieldConfig: { + defaults: { + custom: {}, + }, + overrides: [], + }, + cardOptions: { + previewModifier: (s) => { + if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) { + s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2); + } + }, + }, + }, + ]; + }); //.setDataSupport({ annotations: true, alertStates: true }); diff --git a/public/app/plugins/panel/trend/suggestions.ts b/public/app/plugins/panel/trend/suggestions.ts deleted file mode 100644 index c135c5df30e..00000000000 --- a/public/app/plugins/panel/trend/suggestions.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { VisualizationSuggestionsBuilder } from '@grafana/data'; -import { GraphDrawStyle, GraphFieldConfig, LegendDisplayMode } from '@grafana/schema'; -import { SuggestionName } from 'app/types/suggestions'; - -import { Options } from './panelcfg.gen'; - -export class TrendSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary } = builder; - - if (dataSummary.numberFieldCount < 2 || dataSummary.rowCountTotal < 2 || dataSummary.rowCountTotal < 2) { - return; - } - - // Super basic - const list = builder.getListAppender({ - name: SuggestionName.LineChart, - pluginId: 'trend', - options: { - legend: { - calcs: [], - displayMode: LegendDisplayMode.Hidden, - placement: 'right', - showLegend: false, - }, - }, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], - }, - cardOptions: { - previewModifier: (s) => { - if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) { - s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2); - } - }, - }, - }); - return list; - } -} diff --git a/public/app/plugins/panel/trend/utils.ts b/public/app/plugins/panel/trend/utils.ts new file mode 100644 index 00000000000..17a02fd5049 --- /dev/null +++ b/public/app/plugins/panel/trend/utils.ts @@ -0,0 +1,48 @@ +import { DataFrame, FieldType, isLikelyAscendingVector } from '@grafana/data'; +import config from 'app/core/config'; +import { findFieldIndex } from 'app/features/dimensions/utils'; + +import { prepareGraphableFields } from '../timeseries/utils'; + +export function prepSeries(frames: DataFrame[], xField?: string): { warning?: string; frames: DataFrame[] | null } { + if (frames.length > 1) { + return { + warning: 'Only one frame is supported, consider adding a join transformation', + frames: frames, + }; + } + + let xFieldIdx: number | undefined; + if (xField) { + xFieldIdx = findFieldIndex(xField, frames[0]); + if (xFieldIdx == null) { + return { + warning: 'Unable to find field: ' + xField, + frames: frames, + }; + } + } else { + // first number field + // Perhaps we can/should support any ordinal rather than an error here + xFieldIdx = frames[0] ? frames[0].fields.findIndex((f) => f.type === FieldType.number) : -1; + if (xFieldIdx === -1) { + return { + warning: 'No numeric fields found for X axis', + frames, + }; + } + } + + // Make sure values are ascending + if (xFieldIdx != null) { + const field = frames[0].fields[xFieldIdx]; + if (field.type === FieldType.number && !isLikelyAscendingVector(field.values)) { + return { + warning: `Values must be in ascending order`, + frames, + }; + } + } + + return { frames: prepareGraphableFields(frames, config.theme2, undefined, xFieldIdx) }; +} diff --git a/public/app/types/suggestions.ts b/public/app/types/suggestions.ts deleted file mode 100644 index aac0dc1bd3c..00000000000 --- a/public/app/types/suggestions.ts +++ /dev/null @@ -1,34 +0,0 @@ -export enum SuggestionName { - LineChart = 'Line chart', - LineChartSmooth = 'Line chart smooth', - LineChartGradientColorScheme = 'Line chart with gradient color scheme', - AreaChart = 'Area chart', - AreaChartStacked = 'Area chart stacked', - AreaChartStackedPercent = 'Area chart 100% stacked', - BarChart = 'Bar chart', - BarChartGradientColorScheme = 'Bar chart with gradient color scheme', - BarChartStacked = 'Bar chart stacked', - BarChartStackedPercent = 'Bar chart 100% stacked', - BarChartHorizontal = 'Bar chart horizontal', - BarChartHorizontalStacked = 'Bar chart horizontal stacked', - BarChartHorizontalStackedPercent = 'Bar chart horizontal 100% stacked', - Candlestick = 'Candlestick', - PieChart = 'Pie chart', - PieChartDonut = 'Pie chart donut', - Stat = 'Stat', - StatColoredBackground = 'Stat colored background', - Gauge = 'Gauge', - GaugeCircular = 'Circular gauge', - GaugeNoThresholds = 'Gauge no thresholds', - BarGaugeBasic = 'Bar gauge basic', - BarGaugeLCD = 'Bar gauge LCD', - Table = 'Table', - StateTimeline = 'State timeline', - StatusHistory = 'Status history', - TextPanel = 'Text', - DashboardList = 'Dashboard list', - Logs = 'Logs', - FlameGraph = 'Flame graph', - Trace = 'Trace', - NodeGraph = 'Node graph', -} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 9f8b19218de..aa73bf3cd22 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3477,6 +3477,14 @@ "label-negative-y": "Negative Y" } }, + "suggestions": { + "horizontal": "Horizontal bar chart", + "hz-stacked": "Horizontal bar chart - stacked", + "hz-stacked-percent": "Horizontal bar chart - stacked by percentage", + "vert-stacked": "Bar chart - stacked", + "vert-stacked-percent": "Bar chart - stacked by percentage", + "vertical": "Bar chart" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Require space from the right side", "gaps-options": { @@ -3521,6 +3529,10 @@ }, "name-show-unfilled-area": "Show unfilled area", "name-value-display": "Value display", + "suggestions": { + "basic": "Bar gauge", + "lcd": "Bar gauge - LCD" + }, "value-display-options": { "label-hidden": "Hidden", "label-text-color": "Text color", @@ -7849,6 +7861,7 @@ "suggestions": { "arc": "Gauge", "circular": "Circular gauge", + "no-thresholds": "Gauge - no thresholds", "style": { "circular": "Glowing", "simple": "Simple" @@ -11103,6 +11116,10 @@ "pie-chart-type-options": { "label-donut": "Donut", "label-pie": "Pie" + }, + "suggestions": { + "donut": "Donut chart", + "pie": "Pie chart" } }, "playlist": { @@ -13051,6 +13068,12 @@ "label-same-as-value": "Same as Value", "label-standard": "Standard" }, + "suggestions": { + "stat-color-background": "Stat - color background", + "stat-discrete-values": "Stat - discrete values", + "stat-discrete-values-color-background": "Stat - discrete values - color background", + "stat-single-string": "Stat - single string" + }, "text-alignment-options": { "label-auto": "Auto", "label-center": "Center" @@ -13490,6 +13513,16 @@ "label-threshold": "Threshold" } }, + "suggestions": { + "area": "Area chart", + "area-stacked": "Area chart - stacked", + "area-stacked-percentage": "Area chart - stacked by percentage", + "bar": "Bar chart", + "bar-stacked": "Bar chart - stacked", + "bar-stacked-percent": "Bar chart - stacked by percentage", + "line": "Line chart", + "line-smooth": "Line chart - smooth" + }, "timezones-editor": { "tooltip-add-timezone": "Add timezone", "tooltip-remove-timezone": "Remove timezone" From c29ed31c7ab977833642fcfe3aebd987dd09675f Mon Sep 17 00:00:00 2001 From: Tito Lins Date: Wed, 26 Nov 2025 17:59:22 +0100 Subject: [PATCH 141/378] alerting: set model refID if missing/mismatch (#114441) --- pkg/services/ngalert/models/alert_query.go | 20 +++++++++++++++++++ pkg/tests/api/alerting/api_prometheus_test.go | 14 ++++++------- pkg/tests/api/alerting/api_ruler_test.go | 14 ++++++++++++- .../test-data/rulegroup-1-export.json | 2 ++ .../test-data/rulegroup-2-export.json | 1 + .../test-data/rulegroup-3-export.json | 2 ++ 6 files changed, 45 insertions(+), 8 deletions(-) diff --git a/pkg/services/ngalert/models/alert_query.go b/pkg/services/ngalert/models/alert_query.go index 9d25d8cfa71..f86925eaa9b 100644 --- a/pkg/services/ngalert/models/alert_query.go +++ b/pkg/services/ngalert/models/alert_query.go @@ -165,6 +165,21 @@ func (aq *AlertQuery) setMaxDatapoints() error { return nil } +// setRefID sets the model refId if it's missing or invalid +func (aq *AlertQuery) setRefID() error { + if aq.modelProps == nil { + err := aq.setModelProps() + if err != nil { + return err + } + } + + if refID, ok := aq.modelProps["refId"].(string); !ok || refID != aq.RefID { + aq.modelProps["refId"] = aq.RefID + } + return nil +} + func (aq *AlertQuery) GetMaxDatapoints() (int64, error) { err := aq.setMaxDatapoints() if err != nil { @@ -256,6 +271,11 @@ func (aq *AlertQuery) GetModel() ([]byte, error) { return nil, err } + err = aq.setRefID() + if err != nil { + return nil, err + } + err = aq.setIntervalMS() if err != nil { return nil, err diff --git a/pkg/tests/api/alerting/api_prometheus_test.go b/pkg/tests/api/alerting/api_prometheus_test.go index 23681381fc9..61bc3195857 100644 --- a/pkg/tests/api/alerting/api_prometheus_test.go +++ b/pkg/tests/api/alerting/api_prometheus_test.go @@ -252,7 +252,7 @@ func TestIntegrationPrometheusRules(t *testing.T) { "rules": [{ "state": "inactive", "name": "AlwaysFiring", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "duration": 10, "folderUid": "default", "uid": "%s", @@ -270,7 +270,7 @@ func TestIntegrationPrometheusRules(t *testing.T) { }, { "state": "inactive", "name": "AlwaysFiringButSilenced", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "folderUid": "default", "uid": "%s", "health": "ok", @@ -317,7 +317,7 @@ func TestIntegrationPrometheusRules(t *testing.T) { "rules": [{ "state": "inactive", "name": "AlwaysFiring", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "duration": 10, "folderUid": "default", "uid": "%s", @@ -335,7 +335,7 @@ func TestIntegrationPrometheusRules(t *testing.T) { }, { "state": "inactive", "name": "AlwaysFiringButSilenced", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "folderUid": "default", "uid": "%s", "health": "ok", @@ -639,7 +639,7 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) { "name": "AlwaysFiring", "uid": "%s", "folderUid": "default", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "duration": 10, "keepFiringFor": 15, "annotations": { @@ -656,7 +656,7 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) { "name": "AlwaysFiringButSilenced", "uid": "%s", "folderUid": "default", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "health": "ok", "isPaused": false, "type": "alerting", @@ -688,7 +688,7 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) { "name": "AlwaysFiring", "uid": "%s", "folderUid": "default", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "duration": 10, "keepFiringFor": 15, "annotations": { diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 18eb018b336..9e8855d8f19 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -1166,6 +1166,7 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { "expression": "2 + 3 \u003e 1", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } }], @@ -1209,6 +1210,7 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { "expression": "2 + 3 \u003e 1", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } }], @@ -1264,6 +1266,7 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { "expression": "2 + 3 \u003e 1", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } }], @@ -1610,7 +1613,7 @@ func TestIntegrationRuleCreate(t *testing.T) { To: apimodels.Duration(15 * time.Minute), }, DatasourceUID: expr.DatasourceUID, - Model: json.RawMessage(`{"expression":"1","intervalMs":1000,"maxDataPoints":43200,"type":"math"}`), + Model: json.RawMessage(`{"expression":"1","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}`), }, }, UpdatedBy: &apimodels.UserInfo{ @@ -2681,6 +2684,7 @@ func TestIntegrationQuota(t *testing.T) { "expression":"2 + 4 \u003E 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -2798,6 +2802,7 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) { "expression": "2 + 3 > 1", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } } @@ -3285,6 +3290,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003e 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -3331,6 +3337,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003e 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -3683,6 +3690,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003e 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -3729,6 +3737,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003e 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -3872,6 +3881,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003C 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -3995,6 +4005,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003C 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -4093,6 +4104,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003C 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } diff --git a/pkg/tests/api/alerting/test-data/rulegroup-1-export.json b/pkg/tests/api/alerting/test-data/rulegroup-1-export.json index dbf2ff417b0..18d8b8cea40 100644 --- a/pkg/tests/api/alerting/test-data/rulegroup-1-export.json +++ b/pkg/tests/api/alerting/test-data/rulegroup-1-export.json @@ -23,6 +23,7 @@ "expression": "0 \u003e 0", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } } @@ -55,6 +56,7 @@ "expression": "0 == 0", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } } diff --git a/pkg/tests/api/alerting/test-data/rulegroup-2-export.json b/pkg/tests/api/alerting/test-data/rulegroup-2-export.json index 5f85d830260..43428689eb1 100644 --- a/pkg/tests/api/alerting/test-data/rulegroup-2-export.json +++ b/pkg/tests/api/alerting/test-data/rulegroup-2-export.json @@ -23,6 +23,7 @@ "expression": "0/0", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } } diff --git a/pkg/tests/api/alerting/test-data/rulegroup-3-export.json b/pkg/tests/api/alerting/test-data/rulegroup-3-export.json index 5a1ba9e0eac..76eda60f08d 100644 --- a/pkg/tests/api/alerting/test-data/rulegroup-3-export.json +++ b/pkg/tests/api/alerting/test-data/rulegroup-3-export.json @@ -23,6 +23,7 @@ "expression": "0 \u003e 0", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } } @@ -54,6 +55,7 @@ "expression": "0 == 0", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } } From d732bb2751175b3fd2c1288294dd98d80316dc59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 26 Nov 2025 19:26:53 +0100 Subject: [PATCH 142/378] PanelChrome: Enable new panel padding by default (#114492) --- .../configure-grafana/feature-toggles/index.md | 1 + .../grafana-data/src/types/featureToggles.gen.ts | 2 +- pkg/services/featuremgmt/registry.go | 6 +++--- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.go | 4 ---- pkg/services/featuremgmt/toggles_gen.json | 14 ++++++++------ 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index c05ecd45bd9..7ca66464680 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -98,6 +98,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker | | `newVizSuggestions` | Enable new visualization suggestions | | `preventPanelChromeOverflow` | Restrict PanelChrome contents with overflow: hidden; | +| `newPanelPadding` | Increases panel padding globally | | `transformationsEmptyPlaceholder` | Show transformation quick-start cards in empty transformations state | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 0d20e44b19e..40f2abe4d73 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1150,7 +1150,7 @@ export interface FeatureToggles { pluginStoreServiceLoading?: boolean; /** * Increases panel padding globally - * @default false + * @default true */ newPanelPadding?: boolean; /** diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 65f94c24eac..91b59bf711d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1895,10 +1895,10 @@ var ( { Name: "newPanelPadding", Description: "Increases panel padding globally", - Stage: FeatureStageExperimental, - FrontendOnly: false, + Stage: FeatureStagePublicPreview, + FrontendOnly: true, Owner: grafanaDashboardsSquad, - Expression: "false", + Expression: "true", }, { Name: "onlyStoreActionSets", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 17fdf27f33d..f161087ecb1 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -257,7 +257,7 @@ newVizSuggestions,preview,@grafana/dataviz-squad,false,false,true preventPanelChromeOverflow,preview,@grafana/grafana-frontend-platform,false,false,true jaegerEnableGrpcEndpoint,experimental,@grafana/oss-big-tent,false,false,false pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,false,false -newPanelPadding,experimental,@grafana/dashboards-squad,false,false,false +newPanelPadding,preview,@grafana/dashboards-squad,false,false,true onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b88204db708..1217d1f5a28 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -742,10 +742,6 @@ const ( // Load plugins on store service startup instead of wire provider, and call RegisterFixedRoles after all plugins are loaded FlagPluginStoreServiceLoading = "pluginStoreServiceLoading" - // FlagNewPanelPadding - // Increases panel padding globally - FlagNewPanelPadding = "newPanelPadding" - // FlagOnlyStoreActionSets // When storing dashboard and folder resource permissions, only store action sets and not the full list of underlying permission FlagOnlyStoreActionSets = "onlyStoreActionSets" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 1093b643232..8dee7f8206b 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -551,7 +551,6 @@ "description": "Enables the UI to use rules backend-side filters 100% compatible with the frontend filters", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -565,7 +564,6 @@ "description": "Enables the UI to use rules backend-side filters 100% compatible with the frontend filters", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2361,14 +2359,18 @@ { "metadata": { "name": "newPanelPadding", - "resourceVersion": "1763734583253", - "creationTimestamp": "2025-11-12T15:40:46Z" + "resourceVersion": "1764168915089", + "creationTimestamp": "2025-11-12T15:40:46Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-26 14:55:15.089551 +0000 UTC" + } }, "spec": { "description": "Increases panel padding globally", - "stage": "experimental", + "stage": "preview", "codeowner": "@grafana/dashboards-squad", - "expression": "false" + "frontend": true, + "expression": "true" } }, { From 7eb467e561ef7b01fb259ba866f5a1e9c956163c Mon Sep 17 00:00:00 2001 From: maicon Date: Wed, 26 Nov 2025 16:24:34 -0300 Subject: [PATCH 143/378] provisioning: acquire server lock before provisioning dashboards+folders (#114488) * provisioning: acquire server lock before provisioning dashboards+folders Signed-off-by: Maicon Costa --------- Signed-off-by: Maicon Costa --- pkg/server/wire_gen.go | 4 +- .../provisioning/dashboards/dashboard.go | 67 ++++++++++++++----- pkg/services/provisioning/provisioning.go | 6 +- .../provisioning/provisioning_test.go | 4 +- pkg/setting/setting.go | 13 ++++ 5 files changed, 75 insertions(+), 19 deletions(-) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 64ffd48ca9f..75341e77f78 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -668,7 +668,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api azurePromMigrationService := promtypemigration.ProvideAzurePromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg) amazonPromMigrationService := promtypemigration.ProvideAmazonPromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg) promTypeMigrationProviderImpl := promtypemigration.ProvidePromTypeMigrationProvider(serverLockService, featureToggles, azurePromMigrationService, amazonPromMigrationService) - provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl) + provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl, serverLockService) if err != nil { return nil, err } @@ -1312,7 +1312,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac azurePromMigrationService := promtypemigration.ProvideAzurePromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg) amazonPromMigrationService := promtypemigration.ProvideAmazonPromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg) promTypeMigrationProviderImpl := promtypemigration.ProvidePromTypeMigrationProvider(serverLockService, featureToggles, azurePromMigrationService, amazonPromMigrationService) - provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl) + provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl, serverLockService) if err != nil { return nil, err } diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index e0c24c6ab07..72b980e4198 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -2,6 +2,7 @@ package dashboards import ( "context" + "errors" "fmt" "os" "time" @@ -9,10 +10,12 @@ import ( dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" folderV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/provisioning/utils" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) @@ -28,7 +31,7 @@ type DashboardProvisioner interface { } // DashboardProvisionerFactory creates DashboardProvisioners based on input -type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service) (DashboardProvisioner, error) +type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, *setting.Cfg, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service, *serverlock.ServerLockService) (DashboardProvisioner, error) // Provisioner is responsible for syncing dashboard from disk to Grafana's database. type Provisioner struct { @@ -38,6 +41,8 @@ type Provisioner struct { duplicateValidator duplicateValidator provisioner dashboards.DashboardProvisioningService dual dualwrite.Service + serverLock *serverlock.ServerLockService + cfg *setting.Cfg } func (provider *Provisioner) HasDashboardSources() bool { @@ -45,7 +50,7 @@ func (provider *Provisioner) HasDashboardSources() bool { } // New returns a new DashboardProvisioner -func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgService org.Service, dashboardStore utils.DashboardStore, folderService folder.Service, dual dualwrite.Service) (DashboardProvisioner, error) { +func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, cfg *setting.Cfg, orgService org.Service, dashboardStore utils.DashboardStore, folderService folder.Service, dual dualwrite.Service, serverLockService *serverlock.ServerLockService) (DashboardProvisioner, error) { logger := log.New("provisioning.dashboard") cfgReader := &configReader{path: configDirectory, log: logger, orgExists: utils.NewOrgExistsChecker(orgService)} configs, err := cfgReader.readConfig(ctx) @@ -78,6 +83,8 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das duplicateValidator: newDuplicateValidator(logger, fileReaders), provisioner: provisioner, dual: dual, + serverLock: serverLockService, + cfg: cfg, } return d, nil @@ -95,23 +102,53 @@ func (provider *Provisioner) Provision(ctx context.Context) error { } } - provider.log.Info("starting to provision dashboards") + var errProvisioning error - for _, reader := range provider.fileReaders { - if err := reader.walkDisk(ctx); err != nil { - if os.IsNotExist(err) { - // don't stop the provisioning service in case the folder is missing. The folder can appear after the startup - provider.log.Warn("Failed to provision config", "name", reader.Cfg.Name, "error", err) - return nil - } - - return fmt.Errorf("failed to provision config %v: %w", reader.Cfg.Name, err) + // retry obtaining the lock for 20 attempts + retryOpt := func(attempts int) error { + if attempts < 20 { + return nil } + return errors.New("retries exhausted") } - provider.duplicateValidator.validate() - provider.log.Info("finished to provision dashboards") - return nil + lockTimeConfig := serverlock.LockTimeConfig{ + // if a replica crashes while holding the lock, other replicas can obtain the + // lock after this duration (15s default value, might be configured via config file) + MaxInterval: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMaxIntervalSeconds) * time.Second, + + // wait beetween 100ms and 1s before retrying to obtain the lock (default values, might be configured via config file) + MinWait: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMinWaitMs) * time.Millisecond, + MaxWait: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMaxWaitMs) * time.Millisecond, + } + + // this means that if we fail to obtain the lock after ~10 seconds, we return an error + lockErr := provider.serverLock.LockExecuteAndReleaseWithRetries(ctx, "provisioning_dashboards", lockTimeConfig, func(ctx context.Context) { + provider.log.Info("starting to provision dashboards") + + for _, reader := range provider.fileReaders { + if err := reader.walkDisk(ctx); err != nil { + if os.IsNotExist(err) { + // don't stop the provisioning service in case the folder is missing. The folder can appear after the startup + provider.log.Warn("Failed to provision config", "name", reader.Cfg.Name, "error", err) + return + } + + errProvisioning = fmt.Errorf("failed to provision config %v: %w", reader.Cfg.Name, err) + return + } + } + + provider.duplicateValidator.validate() + provider.log.Info("finished to provision dashboards") + }, retryOpt) + + if lockErr != nil { + provider.log.Error("Failed to obtain dashboard provisioning lock", "error", lockErr) + return lockErr + } + + return errProvisioning } // CleanUpOrphanedDashboards deletes provisioned dashboards missing a linked reader. diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index c79a9625d50..bc7c53e0cb2 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/dskit/services" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -64,6 +65,7 @@ func ProvideService( tracer tracing.Tracer, dual dualwrite.Service, promTypeMigrationProvider promtypemigration.PromTypeMigrationProvider, + serverLockService *serverlock.ServerLockService, ) (*ProvisioningServiceImpl, error) { s := &ProvisioningServiceImpl{ Cfg: cfg, @@ -92,6 +94,7 @@ func ProvideService( tracer: tracer, migratePrometheusType: promTypeMigrationProvider.Run, dual: dual, + serverLock: serverLockService, } s.NamedService = services.NewBasicService(s.starting, s.running, nil).WithName(ServiceName) @@ -166,7 +169,7 @@ func (ps *ProvisioningServiceImpl) running(ctx context.Context) error { func (ps *ProvisioningServiceImpl) setDashboardProvisioner() error { dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards") - dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.orgService, ps.dashboardService, ps.folderService, ps.dual) + dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.Cfg, ps.orgService, ps.dashboardService, ps.folderService, ps.dual, ps.serverLock) if err != nil { return fmt.Errorf("%v: %w", "Failed to create provisioner", err) } @@ -242,6 +245,7 @@ type ProvisioningServiceImpl struct { resourcePermissions accesscontrol.ReceiverPermissionsService tracer tracing.Tracer dual dualwrite.Service + serverLock *serverlock.ServerLockService migratePrometheusType func(context.Context) error } diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go index 8b513ba321e..b94d7beac37 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/serverlock" dashboardstore "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" @@ -20,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/services/provisioning/datasources" "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/services/searchV2" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) @@ -160,7 +162,7 @@ func setup(t *testing.T) *serviceTestStruct { searchStub := searchV2.NewStubSearchService() service, err := newProvisioningServiceImpl( - func(context.Context, string, dashboardstore.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service) (dashboards.DashboardProvisioner, error) { + func(context.Context, string, dashboardstore.DashboardProvisioningService, *setting.Cfg, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service, *serverlock.ServerLockService) (dashboards.DashboardProvisioner, error) { serviceTest.dashboardProvisionerInstantiations++ return serviceTest.mock, nil }, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index a8b48c67f29..470b6910751 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -150,6 +150,11 @@ type Cfg struct { PluginsPath string EnterpriseLicensePath string + // Classic Provisioning settings + ClassicProvisioningDashboardsServerLockMaxIntervalSeconds int64 + ClassicProvisioningDashboardsServerLockMinWaitMs int64 + ClassicProvisioningDashboardsServerLockMaxWaitMs int64 + // SMTP email settings Smtp SmtpSettings @@ -1221,6 +1226,8 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { return err } + cfg.readClassicProvisioningSettings(iniFile) + // read dashboard settings dashboards := iniFile.Section("dashboards") cfg.DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20) @@ -2107,6 +2114,12 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error { return nil } +func (cfg *Cfg) readClassicProvisioningSettings(iniFile *ini.File) { + cfg.ClassicProvisioningDashboardsServerLockMinWaitMs = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_min_wait_ms").MustInt64(100) + cfg.ClassicProvisioningDashboardsServerLockMaxWaitMs = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_max_wait_ms").MustInt64(1000) + cfg.ClassicProvisioningDashboardsServerLockMaxIntervalSeconds = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_max_interval_seconds").MustInt64(15) +} + func (cfg *Cfg) readProvisioningSettings(iniFile *ini.File) error { provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "") cfg.ProvisioningPath = makeAbsolute(provisioning, cfg.HomePath) From b08e7bc3736d9204d1f4d48d49b3315754a69380 Mon Sep 17 00:00:00 2001 From: Jesse David Peterson Date: Wed, 26 Nov 2025 16:32:37 -0400 Subject: [PATCH 144/378] TimePicker: Show new shortcut for zoom out when experimental flag toggled on (#114506) * fix(time-picker): show new shortcut for zoom out when flag toggled on * chore(i18n): extract translations --- .../DateTimePickers/TimeRangePicker.test.tsx | 54 ++++++++++++++++++- .../DateTimePickers/TimeRangePicker.tsx | 24 ++++++--- public/locales/en-US/grafana.json | 3 +- 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx index c7399e65a4f..51d24c5bc8e 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { dateTime, makeTimeRange, TimeRange } from '@grafana/data'; +import { dateTime, makeTimeRange, TimeRange, BootData } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { TimeRangeProvider } from './TimeRangeContext'; @@ -152,6 +152,58 @@ it('does not submit wrapping forms', async () => { expect(onSubmit).not.toHaveBeenCalled(); }); +it('shows CTRL+Z in zoom out tooltip when feature flag is disabled', async () => { + window.grafanaBootData = { + settings: { + featureToggles: { + newTimeRangeZoomShortcuts: false, + }, + }, + } as BootData; + + render( + {}} + onChange={(value) => {}} + value={value} + onMoveBackward={() => {}} + onMoveForward={() => {}} + onZoom={() => {}} + /> + ); + + const zoomButton = screen.getByLabelText('Zoom out time range'); + await userEvent.hover(zoomButton); + + expect(await screen.findByText(/CTRL\+Z/)).toBeInTheDocument(); +}); + +it('shows t - in zoom out tooltip when feature flag is enabled', async () => { + window.grafanaBootData = { + settings: { + featureToggles: { + newTimeRangeZoomShortcuts: true, + }, + }, + } as BootData; + + render( + {}} + onChange={(value) => {}} + value={value} + onMoveBackward={() => {}} + onMoveForward={() => {}} + onZoom={() => {}} + /> + ); + + const zoomButton = screen.getByLabelText('Zoom out time range'); + await userEvent.hover(zoomButton); + + expect(await screen.findByText(/t -/)).toBeInTheDocument(); +}); + describe('TimePickerTooltip', () => { beforeAll(() => { const mockIntl = { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx index b26a6e4c36d..0719693e625 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx @@ -19,6 +19,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { t, Trans } from '@grafana/i18n'; import { useStyles2 } from '../../themes/ThemeContext'; +import { getFeatureToggle } from '../../utils/featureToggle'; import { ButtonGroup } from '../Button/ButtonGroup'; import { getModalStyles } from '../Modal/getModalStyles'; import { getPortalContainer } from '../Portal/Portal'; @@ -243,13 +244,22 @@ export function TimeRangePicker(props: TimeRangePickerProps) { TimeRangePicker.displayName = 'TimeRangePicker'; -const ZoomOutTooltip = () => ( - <> - - Time range zoom out
CTRL+Z -
- -); +const ZoomOutTooltip = () => { + const newShortcuts = getFeatureToggle('newTimeRangeZoomShortcuts'); + return ( + <> + {newShortcuts ? ( + + Time range zoom out
t - +
+ ) : ( + + Time range zoom out
CTRL+Z +
+ )} + + ); +}; export const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; timeZone?: TimeZone }) => { const styles = useStyles2(getLabelStyles); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index aa73bf3cd22..c525ce2e768 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -13411,7 +13411,8 @@ "forwards-time-aria-label": "Move time range forwards", "to": "to", "zoom-out-button": "Zoom out time range", - "zoom-out-tooltip": "Time range zoom out
CTRL+Z" + "zoom-out-tooltip": "Time range zoom out
CTRL+Z", + "zoom-out-tooltip-new": "Time range zoom out
t -" }, "time-range": { "apply": "Apply time range", From 399370bc2c7607a60074b898bd01865f2064f735 Mon Sep 17 00:00:00 2001 From: Jesse David Peterson Date: Wed, 26 Nov 2025 16:41:02 -0400 Subject: [PATCH 145/378] TimeRange: Avoid x-axis pan jump caused by data loading latency (#114496) * fix(time-range): avoid x-axis pan jump caused by data loading latency * refactor(time-range): use a more semantically meaningful names --- .../uPlot/config/UPlotConfigBuilder.ts | 2 +- .../XAxisInteractionAreaPlugin.test.tsx | 16 ++++++++++++++- .../plugins/XAxisInteractionAreaPlugin.tsx | 7 +++++-- .../app/core/components/TimeSeries/utils.ts | 20 +++++++++++++++++-- .../core/components/TimelineChart/utils.ts | 18 +++++++++++++++++ 5 files changed, 57 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index 27322714477..6bc91793236 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -29,7 +29,7 @@ const cursorDefaults: Cursor = { type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData; type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData; -type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number }; +type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number; isTimeRangePending?: boolean }; export class UPlotConfigBuilder { readonly uid = Math.random().toString(36).slice(2); diff --git a/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.test.tsx b/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.test.tsx index d9a6e787251..e9e471b82af 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.test.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.test.tsx @@ -137,7 +137,7 @@ describe('XAxisInteractionAreaPlugin', () => { expect(mockQueryZoom).not.toHaveBeenCalled(); }); - it('should set isPanning state during drag and clear on mouseup', () => { + it('should set isPanning state during drag and mark isTimeRangePending on mouseup', () => { setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom); xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true })); @@ -153,6 +153,20 @@ describe('XAxisInteractionAreaPlugin', () => { document.dispatchEvent(new MouseEvent('mouseup', { clientX: 350, bubbles: true })); + expect(mockConfigBuilder.setState).toHaveBeenCalledWith({ + isPanning: true, + min: expectedRange.from, + max: expectedRange.to, + isTimeRangePending: true, + }); + }); + + it('should clear isPanning state immediately for small drags below threshold', () => { + setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom); + + xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true })); + document.dispatchEvent(new MouseEvent('mouseup', { clientX: 402, bubbles: true })); + expect(mockConfigBuilder.setState).toHaveBeenCalledWith({ isPanning: false }); }); }); diff --git a/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.tsx index 694f372307d..5b70546b55a 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.tsx @@ -96,11 +96,14 @@ export const setupXAxisPan = ( xAxisEl.style.cursor = 'grab'; - config.setState({ isPanning: false }); + const isSignificantDrag = Math.abs(dragPixels) >= MIN_PAN_DIST; - if (Math.abs(dragPixels) >= MIN_PAN_DIST) { + if (isSignificantDrag) { const newRange = calculatePanRange(startMin, startMax, dragPixels, u.bbox.width); + config.setState({ isPanning: true, min: newRange.from, max: newRange.to, isTimeRangePending: true }); queryZoom(newRange); + } else { + config.setState({ isPanning: false }); } document.removeEventListener('mousemove', onMove); diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts index f81de163b7f..88a82ec299c 100644 --- a/public/app/core/components/TimeSeries/utils.ts +++ b/public/app/core/components/TimeSeries/utils.ts @@ -138,10 +138,26 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ range: () => { const state = builder.getState(); if (state.isPanning) { + if (state.isTimeRangePending) { + const timeRange = getTimeRange(); + const propsFrom = timeRange.from.valueOf(); + const propsTo = timeRange.to.valueOf(); + + const MIN_TIMESPAN_MS = 1; + const fromMatches = Math.abs(propsFrom - state.min) <= MIN_TIMESPAN_MS; + const toMatches = Math.abs(propsTo - state.max) <= MIN_TIMESPAN_MS; + const timeRangeHasUpdated = fromMatches && toMatches; + + if (timeRangeHasUpdated) { + builder.setState({ isPanning: false }); + return [propsFrom, propsTo]; + } + } + return [state.min, state.max]; } - const r = getTimeRange(); - return [r.from.valueOf(), r.to.valueOf()]; + const timeRange = getTimeRange(); + return [timeRange.from.valueOf(), timeRange.to.valueOf()]; }, }); diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 955f1ea11b2..264c5498be9 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -159,6 +159,24 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ( range: (u) => { const state = builder.getState(); if (state.isPanning) { + if (state.isTimeRangePending) { + const propsRange = coreConfig.xRange(u); + const propsFrom = propsRange[0]; + const propsTo = propsRange[1]; + + if (propsFrom != null && propsTo != null) { + const MIN_TIMESPAN_MS = 1; + const fromMatches = Math.abs(propsFrom - state.min) <= MIN_TIMESPAN_MS; + const toMatches = Math.abs(propsTo - state.max) <= MIN_TIMESPAN_MS; + const timeRangeHasUpdated = fromMatches && toMatches; + + if (timeRangeHasUpdated) { + builder.setState({ isPanning: false }); + return propsRange; + } + } + } + return [state.min, state.max]; } return coreConfig.xRange(u); From a455f9700d6b780133635f7d8af520669b2b0983 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Wed, 26 Nov 2025 22:07:24 +0100 Subject: [PATCH 146/378] Azure: Enable resource picker updates (#114089) * Default resource picker updates to true * Update toggle docs --- .../configure-grafana/feature-toggles/index.md | 2 +- packages/grafana-data/src/types/featureToggles.gen.ts | 2 +- pkg/services/featuremgmt/registry.go | 4 ++-- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.json | 11 +++++++---- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 7ca66464680..f0f12cce1b6 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -66,6 +66,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `grafanaAssistantInProfilesDrilldown` | Enables integration with Grafana Assistant in Profiles Drilldown | Yes | | `sharingDashboardImage` | Enables image sharing functionality for dashboards | Yes | | `tabularNumbers` | Use fixed-width numbers globally in the UI | | +| `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker | Yes | | `tempoSearchBackendMigration` | Run search queries through the tempo backend | | ## Public preview feature toggles @@ -95,7 +96,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `localeFormatPreference` | Specifies the locale so the correct format for numbers and dates can be shown | | `logsPanelControls` | Enables a control component for the logs panel in Explore | | `interactiveLearning` | Enables the interactive learning app | -| `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker | | `newVizSuggestions` | Enable new visualization suggestions | | `preventPanelChromeOverflow` | Restrict PanelChrome contents with overflow: hidden; | | `newPanelPadding` | Increases panel padding globally | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 40f2abe4d73..33750126afa 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1091,7 +1091,7 @@ export interface FeatureToggles { graphiteBackendMode?: boolean; /** * Enables the updated Azure Monitor resource picker - * @default false + * @default true */ azureResourcePickerUpdates?: boolean; /** diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 91b59bf711d..37ee3001e99 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1801,10 +1801,10 @@ var ( { Name: "azureResourcePickerUpdates", Description: "Enables the updated Azure Monitor resource picker", - Stage: FeatureStagePublicPreview, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: grafanaPartnerPluginsSquad, - Expression: "false", + Expression: "true", }, { Name: "prometheusTypeMigration", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index f161087ecb1..f66bc50bbc7 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -245,7 +245,7 @@ teamFolders,experimental,@grafana/grafana-search-navigate-organise,false,false,f interactiveLearning,preview,@grafana/pathfinder,false,false,false alertingTriage,experimental,@grafana/alerting-squad,false,false,false graphiteBackendMode,privatePreview,@grafana/partner-datasources,false,false,false -azureResourcePickerUpdates,preview,@grafana/partner-datasources,false,false,true +azureResourcePickerUpdates,GA,@grafana/partner-datasources,false,false,true prometheusTypeMigration,experimental,@grafana/partner-datasources,false,true,false pluginContainers,privatePreview,@grafana/plugins-platform-backend,false,true,false tempoSearchBackendMigration,GA,@grafana/oss-big-tent,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 8dee7f8206b..46e5eda90a7 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -766,16 +766,19 @@ { "metadata": { "name": "azureResourcePickerUpdates", - "resourceVersion": "1763734583253", + "resourceVersion": "1764153435365", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z" + "deletionTimestamp": "2025-08-01T11:30:17Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-26 10:37:15.365919 +0000 UTC" + } }, "spec": { "description": "Enables the updated Azure Monitor resource picker", - "stage": "preview", + "stage": "GA", "codeowner": "@grafana/partner-datasources", "frontend": true, - "expression": "false" + "expression": "true" } }, { From f116539541d7f9cd56140f546becc5a150ef4bde Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Wed, 26 Nov 2025 15:53:00 -0600 Subject: [PATCH 147/378] Unified Storage: Update readme (#114415) * update readme * Adds message about creating database * update sample storage config --- pkg/storage/unified/README.md | 52 +++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/pkg/storage/unified/README.md b/pkg/storage/unified/README.md index 034147d84d9..7131178e8a1 100644 --- a/pkg/storage/unified/README.md +++ b/pkg/storage/unified/README.md @@ -202,30 +202,58 @@ then run: kubectl --kubeconfig=./grafana.kubeconfig create -f folder-generate.yaml ``` -### Run as a GRPC service +### Run as a separate GRPC service -#### Start GRPC storage-server +It is recommended to use a separate config file for the storage-server. Create a file `conf/storage-server.ini` with the following content: -Make sure you have the gRPC address in the `[grafana-apiserver]` section of your config file: ```ini +app_mode = development + +target = storage-server + +[database] +type = mysql +host = 127.0.0.1:3306 +name = unified-storage +user = root +password = rootpass +skip_migrations = true +ensure_default_org_and_user = false + +[grpc_server] +network = "tcp" +address = "127.0.0.1:10000" + [grafana-apiserver] -; your gRPC server address -address = localhost:10000 -``` +storage_type = unified -You also need the `[grpc_server_authentication]` section to authenticate incoming requests: -```ini [grpc_server_authentication] -; http url to Grafana's signing keys to validate incoming id tokens -signing_keys_url = http://localhost:3000/api/signing-keys/keys +signing_keys_url = http://localhost:3011/api/signing-keys/keys mode = "on-prem" + +[feature_toggles] +kubernetesDashboards = true +kubernetesFolders = true +unifiedStorage = true +unifiedStorageHistoryPruner = true +unifiedStorageSearch = true +unifiedStorageSearchPermissionFiltering = false +unifiedStorageSearchSprinkles = false + +[unified_storage] +enable_search = true +https_skip_verify = true ``` -This currently only works with a separate database configuration (see previous section). +You should also have a MySQL database running. You can create one with our docker blocks by running: +```bash +make devenv sources=mysql +``` +The database credentials in the example above will work with the default mysql docker block. You'll also need to create a database named `unified-storage`. Start the storage-server with: ```sh -GF_DEFAULT_TARGET=storage-server ./bin/grafana server target +./bin/grafana server target --config conf/storage-server.ini ``` The GRPC service will listen on port 10000 From a8aef11926176e442e6fa1cefbdf4b1ada06c828 Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:59:21 -0800 Subject: [PATCH 148/378] Geomap: Fix data filter for layers (#114515) * Geomap: Fix data filter for layers * Simplify comments --- .../plugins/panel/geomap/utils/layers.test.ts | 123 ++++++++++++++++++ .../app/plugins/panel/geomap/utils/layers.ts | 8 +- 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/panel/geomap/utils/layers.test.ts diff --git a/public/app/plugins/panel/geomap/utils/layers.test.ts b/public/app/plugins/panel/geomap/utils/layers.test.ts new file mode 100644 index 00000000000..1310d892481 --- /dev/null +++ b/public/app/plugins/panel/geomap/utils/layers.test.ts @@ -0,0 +1,123 @@ +jest.mock('ol-mapbox-style', () => ({})); +jest.mock('geotiff', () => ({})); + +import BaseLayer from 'ol/layer/Base'; + +import { + DataFrame, + DataQueryRequest, + FieldType, + LoadingState, + MapLayerHandler, + MapLayerOptions, + PanelData, + TimeRange, +} from '@grafana/data'; + +import { applyLayerFilter } from './layers'; + +describe('applyLayerFilter', () => { + const createDataFrame = (refId: string): DataFrame => ({ + refId, + fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3], config: {} }], + length: 3, + }); + + it('should apply filter when query exists and is visible', () => { + const update = jest.fn(); + const handler: MapLayerHandler = { + init: () => ({}) as BaseLayer, + update, + }; + const options: MapLayerOptions = { + name: 'Test', + type: 'markers', + filterData: { id: 'byRefId', options: 'A' }, + }; + const panelData: PanelData = { + series: [createDataFrame('A'), createDataFrame('B')], + state: LoadingState.Done, + timeRange: {} as TimeRange, + request: { targets: [{ refId: 'A' }, { refId: 'B' }] } as DataQueryRequest, + }; + + applyLayerFilter(handler, options, panelData); + + expect(update).toHaveBeenCalledWith( + expect.objectContaining({ + series: [createDataFrame('A')], + }) + ); + }); + + it('should return empty series when query exists but is hidden', () => { + const update = jest.fn(); + const handler: MapLayerHandler = { + init: () => ({}) as BaseLayer, + update, + }; + const options: MapLayerOptions = { + name: 'Test', + type: 'markers', + filterData: { id: 'byRefId', options: 'A' }, + }; + const panelData: PanelData = { + series: [createDataFrame('B')], + state: LoadingState.Done, + timeRange: {} as TimeRange, + request: { targets: [{ refId: 'A', hide: true }, { refId: 'B' }] } as DataQueryRequest, + }; + + applyLayerFilter(handler, options, panelData); + + expect(update).toHaveBeenCalledWith( + expect.objectContaining({ + series: [], + }) + ); + }); + + it('should not apply filter when query does not exist', () => { + const update = jest.fn(); + const handler: MapLayerHandler = { + init: () => ({}) as BaseLayer, + update, + }; + const options: MapLayerOptions = { + name: 'Test', + type: 'markers', + filterData: { id: 'byRefId', options: 'C' }, + }; + const panelData: PanelData = { + series: [createDataFrame('A'), createDataFrame('B')], + state: LoadingState.Done, + timeRange: {} as TimeRange, + request: { targets: [{ refId: 'A' }, { refId: 'B' }] } as DataQueryRequest, + }; + + applyLayerFilter(handler, options, panelData); + + expect(update).toHaveBeenCalledWith(panelData); + }); + + it('should pass through all data when no filter is configured', () => { + const update = jest.fn(); + const handler: MapLayerHandler = { + init: () => ({}) as BaseLayer, + update, + }; + const options: MapLayerOptions = { + name: 'Test', + type: 'markers', + }; + const panelData: PanelData = { + series: [createDataFrame('A'), createDataFrame('B')], + state: LoadingState.Done, + timeRange: {} as TimeRange, + }; + + applyLayerFilter(handler, options, panelData); + + expect(update).toHaveBeenCalledWith(panelData); + }); +}); diff --git a/public/app/plugins/panel/geomap/utils/layers.ts b/public/app/plugins/panel/geomap/utils/layers.ts index f20d8891ade..925ec1a82c4 100644 --- a/public/app/plugins/panel/geomap/utils/layers.ts +++ b/public/app/plugins/panel/geomap/utils/layers.ts @@ -26,7 +26,13 @@ export const applyLayerFilter = ( let panelData = panelDataProps; if (options.filterData) { const matcherFunc = getFrameMatchers(options.filterData); - if (panelData.series.some(matcherFunc)) { + + const queryExists = panelData.request?.targets.some((target) => { + return target.refId === options.filterData?.options; + }); + + // Only apply filter if the target query exists + if (queryExists) { panelData = { ...panelData, series: panelData.series.filter(matcherFunc), From 84a07be6e4e1acd8f064c3b390c30188d5703afc Mon Sep 17 00:00:00 2001 From: Eric Shields Date: Wed, 26 Nov 2025 15:47:32 -0800 Subject: [PATCH 149/378] Chore: Finalize removal of updateNode & expandOrFilter (#114202) - Remove references to, and related private functions for, `updateNode` and `expandOrFilter` - Remove obsolete tests - Update all usages of `updateNode` to `filterNode` - Integrate `expandOrFilter` functionality into `filterNode` - Add profiler to `filterNode` - Add `.claude` to `.gitignore` IDE junk section - Unit tests for `toggleExpandedNode` and `filterNode` - Add profiler to `toggleExpandedNode` Fixes: https://github.com/grafana/grafana-operator-experience-squad/issues/1566 --- .gitignore | 1 + .../actions/scopeActions.test.tsx | 28 +- .../commandPalette/actions/scopeActions.tsx | 10 +- .../commandPalette/actions/scopesUtils.ts | 5 +- .../selector/ScopesSelectorService.test.ts | 536 ++++++++++++------ .../scopes/selector/ScopesSelectorService.ts | 134 ++--- 6 files changed, 425 insertions(+), 289 deletions(-) diff --git a/.gitignore b/.gitignore index f1a90d05693..5302a698c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ public/css/*.min.css .vs/ .cursor/ .devcontainer/ +.claude/ .eslintcache .stylelintcache diff --git a/public/app/features/commandPalette/actions/scopeActions.test.tsx b/public/app/features/commandPalette/actions/scopeActions.test.tsx index 0d828d66a81..126877224de 100644 --- a/public/app/features/commandPalette/actions/scopeActions.test.tsx +++ b/public/app/features/commandPalette/actions/scopeActions.test.tsx @@ -22,7 +22,7 @@ jest.mock('./scopesUtils', () => { }); const mockScopeServicesState = { - updateNode: jest.fn(), + filterNode: jest.fn(), selectScope: jest.fn(), resetSelection: jest.fn(), nodes: {}, @@ -99,12 +99,12 @@ describe('useRegisterScopesActions', () => { }); it('should register scope tree actions and return scopesRow when scopes are selected', () => { - const mockUpdateNode = jest.fn(); + const mockFilterNode = jest.fn(); // First run with empty scopes in the scopes service (useScopeServicesState as jest.Mock).mockReturnValue({ ...mockScopeServicesState, - updateNode: mockUpdateNode, + filterNode: mockFilterNode, selectedScopes: [{ scopeId: 'scope1', name: 'Scope 1' }], }); @@ -112,14 +112,14 @@ describe('useRegisterScopesActions', () => { return useRegisterScopesActions('', jest.fn()); }); - expect(mockUpdateNode).toHaveBeenCalledWith('', true, ''); + expect(mockFilterNode).toHaveBeenCalledWith('', ''); expect(useRegisterActions).toHaveBeenLastCalledWith([rootScopeAction], [[rootScopeAction]]); expect(result.current.scopesRow).toBeDefined(); // Simulate loading of scopes in the service (useScopeServicesState as jest.Mock).mockReturnValue({ ...mockScopeServicesState, - updateNode: mockUpdateNode, + filterNode: mockFilterNode, selectedScopes: [{ scopeId: 'scope1', name: 'Scope 1' }], nodes, tree, @@ -151,12 +151,12 @@ describe('useRegisterScopesActions', () => { }); it('should load next level of scopes', () => { - const mockUpdateNode = jest.fn(); + const mockFilterNode = jest.fn(); // First run with empty scopes in the scopes service (useScopeServicesState as jest.Mock).mockReturnValue({ ...mockScopeServicesState, - updateNode: mockUpdateNode, + filterNode: mockFilterNode, nodes, tree, }); @@ -165,7 +165,7 @@ describe('useRegisterScopesActions', () => { return useRegisterScopesActions('', jest.fn(), 'scopes/scope1'); }); - expect(mockUpdateNode).toHaveBeenCalledWith('scope1', true, ''); + expect(mockFilterNode).toHaveBeenCalledWith('scope1', ''); }); it('does not return component if no scopes are selected', () => { @@ -259,12 +259,12 @@ describe('useRegisterScopesActions', () => { }); it('should not use global scope search when searching in some deeper scope category', async () => { - const mockUpdateNode = jest.fn(); + const mockFilterNode = jest.fn(); // First run with empty scopes in the scopes service (useScopeServicesState as jest.Mock).mockReturnValue({ ...mockScopeServicesState, - updateNode: mockUpdateNode, + filterNode: mockFilterNode, nodes, tree, }); @@ -273,17 +273,17 @@ describe('useRegisterScopesActions', () => { return useRegisterScopesActions('something', jest.fn(), 'scopes/scope1'); }); - expect(mockUpdateNode).toHaveBeenCalledWith('scope1', true, 'something'); + expect(mockFilterNode).toHaveBeenCalledWith('scope1', 'something'); expect(mockScopeServicesState.searchAllNodes).not.toHaveBeenCalled(); }); it('should not use global scope search if feature flag is off', async () => { config.featureToggles.scopeSearchAllLevels = false; - const mockUpdateNode = jest.fn(); + const mockFilterNode = jest.fn(); // First run with empty scopes in the scopes service (useScopeServicesState as jest.Mock).mockReturnValue({ ...mockScopeServicesState, - updateNode: mockUpdateNode, + filterNode: mockFilterNode, nodes, tree, }); @@ -292,7 +292,7 @@ describe('useRegisterScopesActions', () => { return useRegisterScopesActions('something', jest.fn(), ''); }); - expect(mockUpdateNode).toHaveBeenCalledWith('', true, 'something'); + expect(mockFilterNode).toHaveBeenCalledWith('', 'something'); expect(mockScopeServicesState.searchAllNodes).not.toHaveBeenCalled(); }); diff --git a/public/app/features/commandPalette/actions/scopeActions.tsx b/public/app/features/commandPalette/actions/scopeActions.tsx index bde41152237..e85d136d2f6 100644 --- a/public/app/features/commandPalette/actions/scopeActions.tsx +++ b/public/app/features/commandPalette/actions/scopeActions.tsx @@ -58,22 +58,22 @@ export function useRegisterScopesActions( * @param parentId */ function useScopeTreeActions(searchQuery: string, parentId?: string | null) { - const { updateNode, selectScope, resetSelection, nodes, tree, selectedScopes } = useScopeServicesState(); + const { filterNode, selectScope, resetSelection, nodes, tree, selectedScopes } = useScopeServicesState(); // Initialize the scopes the first time this runs and reset the scopes that were selected on unmount. useEffect(() => { - updateNode('', true, ''); + filterNode('', ''); resetSelection(); return () => { resetSelection(); }; - }, [updateNode, resetSelection]); + }, [filterNode, resetSelection]); // Load the next level of scopes when the parentId changes. useEffect(() => { const parentScopeId = !parentId || parentId === 'scopes' ? '' : last(parentId.split('/'))!; - updateNode(parentScopeId, true, searchQuery); - }, [updateNode, searchQuery, parentId]); + filterNode(parentScopeId, searchQuery); + }, [filterNode, searchQuery, parentId]); return useMemo( () => mapScopesNodesTreeToActions(nodes, tree!, selectedScopes, selectScope), diff --git a/public/app/features/commandPalette/actions/scopesUtils.ts b/public/app/features/commandPalette/actions/scopesUtils.ts index 2848569247a..d269a58abb1 100644 --- a/public/app/features/commandPalette/actions/scopesUtils.ts +++ b/public/app/features/commandPalette/actions/scopesUtils.ts @@ -14,7 +14,7 @@ export function useScopeServicesState() { const services = useScopesServices(); if (!services) { return { - updateNode: () => {}, + filterNode: () => Promise.resolve(), selectScope: () => {}, resetSelection: () => {}, searchAllNodes: () => Promise.resolve([]), @@ -32,7 +32,7 @@ export function useScopeServicesState() { }, }; } - const { updateNode, filterNode, selectScope, resetSelection, searchAllNodes, deselectScope, apply, getScopeNodes } = + const { filterNode, selectScope, resetSelection, searchAllNodes, deselectScope, apply, getScopeNodes } = services.scopesSelectorService; const selectorServiceState: ScopesSelectorServiceState | undefined = useObservable( services.scopesSelectorService.stateObservable ?? new Observable(), @@ -42,7 +42,6 @@ export function useScopeServicesState() { return { getScopeNodes, filterNode, - updateNode, selectScope, resetSelection, searchAllNodes, diff --git a/public/app/features/scopes/selector/ScopesSelectorService.test.ts b/public/app/features/scopes/selector/ScopesSelectorService.test.ts index 57ff735f019..2c89ea58fe5 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.test.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.test.ts @@ -107,162 +107,9 @@ describe('ScopesSelectorService', () => { service = new ScopesSelectorService(apiClient, dashboardsService, store); }); - describe('updateNode', () => { - it('should update node and fetch children when expanded', async () => { - await service.updateNode('', true, ''); - expect(service.state.nodes['test-scope-node']).toEqual(mockNode); - expect(service.state.tree).toMatchObject({ - children: { 'test-scope-node': { expanded: false, scopeNodeId: 'test-scope-node' } }, - expanded: true, - query: '', - scopeNodeId: '', - }); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: '', query: '' }); - }); - - it.skip('should update node query and fetch children when query changes', async () => { - await service.updateNode('', true, ''); // Expand first - // Simulate a change in the query - await service.updateNode('', true, 'new-qu'); - await service.updateNode('', true, 'new-query'); - expect(service.state.tree).toMatchObject({ - children: {}, - expanded: true, - query: 'new-query', - scopeNodeId: '', - }); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: '', query: 'new-query' }); - }); - - it('should not fetch children when node is collapsed and query is unchanged', async () => { - // First expand the node - await service.updateNode('', true, ''); - // Then collapse it - await service.updateNode('', false, ''); - // Only the first expansion should trigger fetchNodes - expect(apiClient.fetchNodes).toHaveBeenCalledTimes(1); - }); - - it.skip('should clear query on first expansion but keep it when filtering within populated node', async () => { - const mockChildNode: ScopeNode = { - metadata: { name: 'child-node' }, - spec: { linkId: 'child-scope', linkType: 'scope', parentName: '', nodeType: 'leaf', title: 'child-node' }, - }; - - apiClient.fetchNodes.mockResolvedValue([mockChildNode]); - - // Scenario 1: First expansion (no children yet) - clear query for unfiltered view - await service.updateNode('', true, 'search-query'); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: '', query: undefined }); - - // Parent query should be cleared and child nodes should have no query (first expansion) - expect(service.state.tree?.query).toBe(''); - let childTreeNode = service.state.tree?.children?.['child-node']; - expect(childTreeNode?.query).toBe(''); - - // Scenario 2: Filtering within node that already has children - await service.updateNode('', true, 'new-search'); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: '', query: 'new-search' }); - - // Parent and child nodes should have the filter query (filtering within existing children) - expect(service.state.tree?.query).toBe('new-search'); - childTreeNode = service.state.tree?.children?.['child-node']; - expect(childTreeNode?.query).toBe('new-search'); - - expect(apiClient.fetchNodes).toHaveBeenCalledTimes(2); - }); - - it.skip('should always reset query on any expansion', async () => { - const mockChildNode: ScopeNode = { - metadata: { name: 'child-node' }, - spec: { linkId: 'child-scope', linkType: 'scope', parentName: '', nodeType: 'leaf', title: 'child-node' }, - }; - - apiClient.fetchNodes.mockResolvedValue([mockChildNode]); - - // First expansion with any query should reset parent query and not pass query to API - await service.updateNode('', true, 'some-search-query'); - - // Verify query is reset and API called without query for first expansion - expect(service.state.tree?.query).toBe(''); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: '', query: undefined }); - expect(service.state.tree?.children?.['child-node']?.query).toBe(''); - }); - - it.skip('should handle query reset correctly for nested levels beyond root', async () => { - // Set up mock nodes for multi-level hierarchy - const mockParentNode: ScopeNode = { - metadata: { name: 'parent-container' }, - spec: { linkId: '', linkType: 'scope', parentName: '', nodeType: 'container', title: 'Parent Container' }, - }; - - const mockChildNode: ScopeNode = { - metadata: { name: 'child-container' }, - spec: { - linkId: '', - linkType: 'scope', - parentName: 'parent-container', - nodeType: 'container', - title: 'Child Container', - }, - }; - - const mockGrandchildNode: ScopeNode = { - metadata: { name: 'grandchild-leaf' }, - spec: { - linkId: 'leaf-scope', - linkType: 'scope', - parentName: 'child-container', - nodeType: 'leaf', - title: 'Grandchild Leaf', - }, - }; - - // Mock different responses for different parent nodes - apiClient.fetchNodes.mockImplementation((options: { parent?: string; query?: string; limit?: number }) => { - if (options.parent === '') { - return Promise.resolve([mockParentNode]); - } else if (options.parent === 'parent-container') { - return Promise.resolve([mockChildNode]); - } else if (options.parent === 'child-container') { - return Promise.resolve([mockGrandchildNode]); - } - return Promise.resolve([]); - }); - - // Step 1: Expand root node with search query - await service.updateNode('', true, 'search-query'); - - // Root should have query reset, API called without query - expect(service.state.tree?.query).toBe(''); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: '', query: undefined }); - expect(service.state.tree?.children?.['parent-container']?.query).toBe(''); - - // Step 2: Expand first-level child with search query - await service.updateNode('parent-container', true, 'open-search-query'); - - // First-level child should have query reset, API called without query - const parentContainer = service.state.tree?.children?.['parent-container']; - expect(parentContainer?.query).toBe(''); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: 'parent-container', query: undefined }); - expect(parentContainer?.children?.['child-container']?.query).toBe(''); - - // Step 3: Now filter within the first-level child (second call to same node) - await service.updateNode('parent-container', true, 'filter-search'); - - // Now both parent and children should show the filter query since we're filtering within existing children - const newParentContainer = service.state.tree?.children?.['parent-container']; - expect(newParentContainer?.query).toBe('filter-search'); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: 'parent-container', query: 'filter-search' }); - expect(newParentContainer?.children?.['child-container']?.query).toBe('filter-search'); - - expect(apiClient.fetchNodes).toHaveBeenCalledTimes(3); - }); - }); - describe('selectScope and deselectScope', () => { beforeEach(async () => { - await service.updateNode('', true, ''); + await service.filterNode('', ''); }); it('should select a scope', async () => { @@ -311,7 +158,7 @@ describe('ScopesSelectorService', () => { it('should set parent node for recent scopes', async () => { // Load mock node - await service.updateNode('', true, ''); + await service.filterNode('', ''); await service.changeScopes(['test-scope'], 'test-scope-node'); expect(service.state.appliedScopes).toEqual([{ scopeId: 'test-scope', parentNodeId: 'test-scope-node' }]); @@ -363,7 +210,7 @@ describe('ScopesSelectorService', () => { describe('closeAndApply', () => { it('should close the selector and apply the selected scopes', async () => { - await service.updateNode('', true, ''); + await service.filterNode('', ''); await service.selectScope('test-scope-node'); await service.closeAndApply(); expect(service.state.opened).toBe(false); @@ -373,6 +220,7 @@ describe('ScopesSelectorService', () => { describe('apply', () => { it('should apply the selected scopes without closing the selector', async () => { + await service.filterNode('', ''); await service.open(); await service.selectScope('test-scope-node'); await service.apply(); @@ -391,7 +239,7 @@ describe('ScopesSelectorService', () => { describe('removeAllScopes', () => { it('should remove all selected and applied scopes', async () => { - await service.updateNode('', true, ''); + await service.filterNode('', ''); await service.selectScope('test-scope-node'); await service.apply(); await service.removeAllScopes(); @@ -399,7 +247,7 @@ describe('ScopesSelectorService', () => { }); it('should clear navigation scope when removing all scopes', async () => { - await service.updateNode('', true, ''); + await service.filterNode('', ''); await service.selectScope('test-scope-node'); await service.apply(); await service.removeAllScopes(); @@ -429,7 +277,7 @@ describe('ScopesSelectorService', () => { describe('getRecentScopes', () => { it('should parse and filter scopes', async () => { - await service.updateNode('', true, ''); + await service.filterNode('', ''); await service.selectScope('test-scope-node'); await service.apply(); storeValue[RECENT_SCOPES_KEY] = JSON.stringify([[mockScope2], [mockScope]]); @@ -439,7 +287,7 @@ describe('ScopesSelectorService', () => { }); it('should work with old version', async () => { - await service.updateNode('', true, ''); + await service.filterNode('', ''); await service.selectScope('test-scope-node'); await service.apply(); storeValue[RECENT_SCOPES_KEY] = JSON.stringify([ @@ -615,9 +463,347 @@ describe('ScopesSelectorService', () => { }); }); + describe('toggleExpandedNode', () => { + const expandableNode: ScopeNode = { + metadata: { name: 'expandable-node' }, + spec: { + linkId: '', + linkType: undefined, + parentName: '', + nodeType: 'container', + title: 'Expandable Node', + }, + }; + + const childNode: ScopeNode = { + metadata: { name: 'child-node' }, + spec: { + linkId: 'child-scope', + linkType: 'scope', + parentName: 'expandable-node', + nodeType: 'leaf', + title: 'Child Node', + }, + }; + + const leafNode: ScopeNode = { + metadata: { name: 'leaf-node' }, + spec: { + linkId: 'leaf-scope', + linkType: 'scope', + parentName: '', + nodeType: 'leaf', + title: 'Leaf Node', + }, + }; + + beforeEach(async () => { + // Mock fetchNodes to return different nodes based on parent + apiClient.fetchNodes = jest + .fn() + .mockImplementation((options: { parent?: string; query?: string; limit?: number }) => { + if (options.parent === '') { + return [expandableNode, leafNode]; + } else if (options.parent === 'expandable-node') { + return [childNode]; + } + return []; + }); + + // Load root nodes + await service.filterNode('', ''); + }); + + it('should expand a collapsed node and load its children', async () => { + // Node should start collapsed + expect(service.state.tree?.children?.['expandable-node']?.expanded).toBe(false); + + // Expand the node + await service.toggleExpandedNode('expandable-node'); + + // Node should now be expanded + expect(service.state.tree?.children?.['expandable-node']?.expanded).toBe(true); + // Children should be loaded + expect(service.state.tree?.children?.['expandable-node']?.children).toBeDefined(); + expect(service.state.tree?.children?.['expandable-node']?.children?.['child-node']).toBeDefined(); + }); + + it('should collapse an expanded node', async () => { + // First expand the node + await service.toggleExpandedNode('expandable-node'); + expect(service.state.tree?.children?.['expandable-node']?.expanded).toBe(true); + + // Now collapse it + await service.toggleExpandedNode('expandable-node'); + expect(service.state.tree?.children?.['expandable-node']?.expanded).toBe(false); + }); + + it('should reset query to empty string when toggling', async () => { + // First filter with a query + await service.filterNode('expandable-node', 'test-query'); + expect(service.state.tree?.children?.['expandable-node']?.query).toBe('test-query'); + + // Toggle the node + await service.toggleExpandedNode('expandable-node'); + + // Query should be reset + expect(service.state.tree?.children?.['expandable-node']?.query).toBe(''); + }); + + it('should throw error when node not found in tree', async () => { + await expect(service.toggleExpandedNode('non-existent-node')).rejects.toThrow( + 'Node non-existent-node not found in tree' + ); + }); + + it('should throw error when trying to toggle a non-expandable node', async () => { + await expect(service.toggleExpandedNode('leaf-node')).rejects.toThrow( + 'Trying to expand node at id leaf-node that is not expandable' + ); + }); + + it('should reload parent children when collapsing', async () => { + const fetchNodesSpy = jest.spyOn(apiClient, 'fetchNodes'); + + // Expand then collapse + await service.toggleExpandedNode('expandable-node'); + fetchNodesSpy.mockClear(); + + await service.toggleExpandedNode('expandable-node'); + + // Should reload parent's (root) children + expect(fetchNodesSpy).toHaveBeenCalledWith({ parent: '', query: '' }); + }); + + it('should reload parent children with parent query when collapsing', async () => { + // First filter the root with a query + await service.filterNode('', 'parent-query'); + + // Expand a node + await service.toggleExpandedNode('expandable-node'); + + const fetchNodesSpy = jest.spyOn(apiClient, 'fetchNodes'); + + // Collapse the node + await service.toggleExpandedNode('expandable-node'); + + // Should reload parent's children with parent's query + expect(fetchNodesSpy).toHaveBeenCalledWith({ parent: '', query: 'parent-query' }); + }); + }); + + describe('filterNode', () => { + const containerNode: ScopeNode = { + metadata: { name: 'container-node' }, + spec: { + linkId: '', + linkType: undefined, + parentName: '', + nodeType: 'container', + title: 'Container Node', + }, + }; + + const filteredChild: ScopeNode = { + metadata: { name: 'filtered-child' }, + spec: { + linkId: 'filtered-scope', + linkType: 'scope', + parentName: 'container-node', + nodeType: 'leaf', + title: 'Filtered Child', + }, + }; + + const leafNode: ScopeNode = { + metadata: { name: 'leaf-node-2' }, + spec: { + linkId: 'leaf-scope-2', + linkType: 'scope', + parentName: '', + nodeType: 'leaf', + title: 'Leaf Node 2', + }, + }; + + beforeEach(async () => { + // Mock fetchNodes to return different nodes based on query + apiClient.fetchNodes = jest + .fn() + .mockImplementation((options: { parent?: string; query?: string; limit?: number }) => { + if (options.parent === '' && !options.query) { + return [containerNode, leafNode]; + } else if (options.parent === 'container-node' && options.query === 'test-filter') { + return [filteredChild]; + } else if (options.parent === 'container-node' && !options.query) { + return [filteredChild]; + } + return []; + }); + + // Load root nodes + await service.filterNode('', ''); + }); + + it('should filter node with non-empty query', async () => { + await service.filterNode('container-node', 'test-filter'); + + // Node should be expanded + expect(service.state.tree?.children?.['container-node']?.expanded).toBe(true); + // Query should be set + expect(service.state.tree?.children?.['container-node']?.query).toBe('test-filter'); + }); + + it('should load children with the query parameter', async () => { + const fetchNodesSpy = jest.spyOn(apiClient, 'fetchNodes'); + + await service.filterNode('container-node', 'my-query'); + + expect(fetchNodesSpy).toHaveBeenCalledWith({ parent: 'container-node', query: 'my-query' }); + }); + + it('should set expanded to true when filtering', async () => { + // Node starts collapsed + expect(service.state.tree?.children?.['container-node']?.expanded).toBe(false); + + await service.filterNode('container-node', 'test-filter'); + + // Should be expanded after filtering + expect(service.state.tree?.children?.['container-node']?.expanded).toBe(true); + }); + + it('should throw error when node not found', async () => { + await expect(service.filterNode('non-existent-node', 'query')).rejects.toThrow( + 'Trying to filter node at path or id non-existent-node not found' + ); + }); + + it('should throw error when trying to filter a non-expandable node', async () => { + await expect(service.filterNode('leaf-node-2', 'query')).rejects.toThrow( + 'Trying to filter node at id leaf-node-2 that is not expandable' + ); + }); + + it('should handle multiple calls with different queries', async () => { + // First filter + await service.filterNode('container-node', 'first-query'); + expect(service.state.tree?.children?.['container-node']?.query).toBe('first-query'); + + // Second filter with different query + await service.filterNode('container-node', 'second-query'); + expect(service.state.tree?.children?.['container-node']?.query).toBe('second-query'); + + // Third filter with empty query + await service.filterNode('container-node', ''); + expect(service.state.tree?.children?.['container-node']?.query).toBe(''); + }); + + it('should start profiler interaction', async () => { + const profiler = { + startInteraction: jest.fn(), + stopInteraction: jest.fn(), + }; + + // Create new service with profiler + const serviceWithProfiler = new ScopesSelectorService(apiClient, dashboardsService, store, profiler as never); + + await serviceWithProfiler.filterNode('', ''); + + expect(profiler.startInteraction).toHaveBeenCalledWith('scopeNodeFilter'); + expect(profiler.stopInteraction).toHaveBeenCalled(); + }); + + it('should stop profiler even when error is thrown', async () => { + const profiler = { + startInteraction: jest.fn(), + stopInteraction: jest.fn(), + }; + + const serviceWithProfiler = new ScopesSelectorService(apiClient, dashboardsService, store, profiler as never); + + // Load initial nodes + await serviceWithProfiler.filterNode('', ''); + + // Try to filter a non-existent node + await expect(serviceWithProfiler.filterNode('non-existent', 'query')).rejects.toThrow(); + + // Profiler should still be stopped + expect(profiler.stopInteraction).toHaveBeenCalled(); + }); + }); + + describe('interaction between toggleExpandedNode and filterNode', () => { + const expandableNode: ScopeNode = { + metadata: { name: 'interaction-node' }, + spec: { + linkId: '', + linkType: undefined, + parentName: '', + nodeType: 'container', + title: 'Interaction Node', + }, + }; + + const childNode: ScopeNode = { + metadata: { name: 'interaction-child' }, + spec: { + linkId: 'child-scope', + linkType: 'scope', + parentName: 'interaction-node', + nodeType: 'leaf', + title: 'Child Node', + }, + }; + + beforeEach(async () => { + apiClient.fetchNodes = jest + .fn() + .mockImplementation((options: { parent?: string; query?: string; limit?: number }) => { + if (options.parent === '') { + return [expandableNode]; + } else if (options.parent === 'interaction-node') { + return [childNode]; + } + return []; + }); + + await service.filterNode('', ''); + }); + + it('should clear query when toggleExpandedNode is called after filterNode', async () => { + // Filter with a query + await service.filterNode('interaction-node', 'test-query'); + expect(service.state.tree?.children?.['interaction-node']?.query).toBe('test-query'); + + // Toggle should clear the query + await service.toggleExpandedNode('interaction-node'); + expect(service.state.tree?.children?.['interaction-node']?.query).toBe(''); + }); + + it('should set query when filterNode is called after toggleExpandedNode', async () => { + // First toggle (expand) + await service.toggleExpandedNode('interaction-node'); + expect(service.state.tree?.children?.['interaction-node']?.query).toBe(''); + + // Filter should set the query + await service.filterNode('interaction-node', 'new-query'); + expect(service.state.tree?.children?.['interaction-node']?.query).toBe('new-query'); + }); + + it('should maintain expanded state when filtering an already expanded node', async () => { + // Expand the node + await service.toggleExpandedNode('interaction-node'); + expect(service.state.tree?.children?.['interaction-node']?.expanded).toBe(true); + + // Filter should keep it expanded + await service.filterNode('interaction-node', 'query'); + expect(service.state.tree?.children?.['interaction-node']?.expanded).toBe(true); + }); + }); + describe('redirect on scope selection', () => { it('should redirect to the first scopeNavigation with /d/ URL when current URL is not a scopeNavigation', async () => { - const mockNavigations: ScopeNavigation[] = [ + dashboardsService.state.scopeNavigations = [ { spec: { scope: 'test-scope', @@ -632,8 +818,6 @@ describe('ScopesSelectorService', () => { }, }, ]; - - dashboardsService.state.scopeNavigations = mockNavigations; (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/some-other-page' }); await service.changeScopes(['test-scope']); @@ -642,7 +826,7 @@ describe('ScopesSelectorService', () => { }); it('should NOT redirect when the first scopeNavigation does not contain /d/ (e.g., logs drilldown)', async () => { - const mockNavigations: ScopeNavigation[] = [ + dashboardsService.state.scopeNavigations = [ { spec: { scope: 'test-scope', @@ -657,8 +841,6 @@ describe('ScopesSelectorService', () => { }, }, ]; - - dashboardsService.state.scopeNavigations = mockNavigations; (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/some-other-page' }); await service.changeScopes(['test-scope']); @@ -667,7 +849,7 @@ describe('ScopesSelectorService', () => { }); it('should NOT redirect when current URL matches a scopeNavigation', async () => { - const mockNavigations: ScopeNavigation[] = [ + dashboardsService.state.scopeNavigations = [ { spec: { scope: 'test-scope', @@ -682,8 +864,6 @@ describe('ScopesSelectorService', () => { }, }, ]; - - dashboardsService.state.scopeNavigations = mockNavigations; (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/d/dashboard1' }); await service.changeScopes(['test-scope']); @@ -701,7 +881,7 @@ describe('ScopesSelectorService', () => { }); it('should NOT redirect when scopeNavigation does not have a url property', async () => { - const mockNavigations = [ + dashboardsService.state.scopeNavigations = [ { spec: { scope: 'test-scope', @@ -716,8 +896,6 @@ describe('ScopesSelectorService', () => { }, }, ] as unknown as ScopeNavigation[]; - - dashboardsService.state.scopeNavigations = mockNavigations; (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/some-other-page' }); await service.changeScopes(['test-scope']); @@ -726,7 +904,7 @@ describe('ScopesSelectorService', () => { }); it('should handle multiple scopeNavigations and redirect to the first dashboard one', async () => { - const mockNavigations: ScopeNavigation[] = [ + dashboardsService.state.scopeNavigations = [ { spec: { scope: 'test-scope', @@ -754,8 +932,6 @@ describe('ScopesSelectorService', () => { }, }, ]; - - dashboardsService.state.scopeNavigations = mockNavigations; (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/some-other-page' }); await service.changeScopes(['test-scope']); @@ -790,7 +966,7 @@ describe('ScopesSelectorService', () => { }); // First update the node to populate the service state - await service.updateNode('', true, ''); + await service.filterNode('', ''); // Then select the scope to set scopeNodeId in selectedScopes await service.selectScope('test-scope-node'); @@ -837,7 +1013,7 @@ describe('ScopesSelectorService', () => { (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/some-other-page' }); // First update the node to populate the service state - await service.updateNode('', true, ''); + await service.filterNode('', ''); // Then select the scope to set scopeNodeId in selectedScopes await service.selectScope('test-scope-node'); @@ -851,16 +1027,14 @@ describe('ScopesSelectorService', () => { }); it('should fall back to scope navigation when scope node is undefined', async () => { - const mockNavigations: ScopeNavigation[] = [ + // Don't add the node to the service state, so it will be undefined + dashboardsService.state.scopeNavigations = [ { spec: { scope: 'test-scope', url: '/d/dashboard1' }, status: { title: 'Dashboard 1', groups: [] }, metadata: { name: 'dashboard1' }, }, ]; - - // Don't add the node to the service state, so it will be undefined - dashboardsService.state.scopeNavigations = mockNavigations; (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/some-other-page' }); await service.changeScopes(['test-scope']); diff --git a/public/app/features/scopes/selector/ScopesSelectorService.ts b/public/app/features/scopes/selector/ScopesSelectorService.ts index 14a16be69ac..9dea729ff88 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.ts @@ -129,81 +129,38 @@ export class ScopesSelectorService extends ScopesServiceBase { - const path = getPathOfNode(scopeNodeId, this.state.nodes); - const nodeToToggle = treeNodeAtPath(this.state.tree!, path); - - if (!nodeToToggle) { - throw new Error(`Node ${scopeNodeId} not found in tree`); - } - - if (nodeToToggle.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToToggle.scopeNodeId])) { - throw new Error(`Trying to expand node at id ${scopeNodeId} that is not expandable`); - } - - const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { - treeNode.expanded = !nodeToToggle.expanded; - treeNode.query = ''; - }); - - this.updateState({ tree: newTree }); - // If we are collapsing, we need to make sure that all the parent's children are avilable - if (nodeToToggle.expanded === true) { - const parentPath = path.slice(0, -1); - const parentNode = treeNodeAtPath(this.state.tree!, parentPath); - if (parentNode) { - await this.loadNodeChildren(parentPath, parentNode, parentNode.query); - } - } else { - await this.loadNodeChildren(path, nodeToToggle); - } - }; - - public filterNode = async (scopeNodeId: string, query: string) => { - const path = getPathOfNode(scopeNodeId, this.state.nodes); - const nodeToFilter = treeNodeAtPath(this.state.tree!, path); - - if (!nodeToFilter) { - throw new Error(`Trying to filter node at path or id ${scopeNodeId} not found`); - } - - if (nodeToFilter.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToFilter.scopeNodeId])) { - throw new Error(`Trying to filter node at id ${scopeNodeId} that is not expandable`); - } - - const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { - treeNode.expanded = true; - treeNode.query = query; - }); - this.updateState({ tree: newTree }); - - await this.loadNodeChildren(path, nodeToFilter, query); - }; - - private expandOrFilterNode = async (scopeNodeId: string, query?: string) => { - this.interactionProfiler?.startInteraction('scopeNodeDiscovery'); - - const path = getPathOfNode(scopeNodeId, this.state.nodes); - - const nodeToExpand = treeNodeAtPath(this.state.tree!, path); + this.interactionProfiler?.startInteraction('scopeToggleExpandedNode'); try { - if (!nodeToExpand) { + const path = getPathOfNode(scopeNodeId, this.state.nodes); + const nodeToToggle = treeNodeAtPath(this.state.tree!, path); + + if (!nodeToToggle) { throw new Error(`Node ${scopeNodeId} not found in tree`); } - if (nodeToExpand.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToExpand.scopeNodeId])) { + if (nodeToToggle.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToToggle.scopeNodeId])) { throw new Error(`Trying to expand node at id ${scopeNodeId} that is not expandable`); } - if (!nodeToExpand.expanded || nodeToExpand.query !== query) { - const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { - treeNode.expanded = true; - treeNode.query = query || ''; - }); - this.updateState({ tree: newTree }); + const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { + treeNode.expanded = !nodeToToggle.expanded; + treeNode.query = ''; + }); - await this.loadNodeChildren(path, nodeToExpand, query); + this.updateState({ tree: newTree }); + // If we are collapsing, we need to make sure that all the parent's children are available + if (nodeToToggle.expanded) { + const parentPath = path.slice(0, -1); + const parentNode = treeNodeAtPath(this.state.tree!, parentPath); + if (parentNode) { + await this.loadNodeChildren(parentPath, parentNode, parentNode.query); + } + } else { + await this.loadNodeChildren(path, nodeToToggle); } + // Catch and throw error so we can ensure the profiler is stopped + // todo: leverage component-level } catch (error) { throw error; } finally { @@ -211,20 +168,34 @@ export class ScopesSelectorService extends ScopesServiceBase { - const path = getPathOfNode(scopeNodeId, this.state.nodes); + public filterNode = async (scopeNodeId: string, query: string) => { + this.interactionProfiler?.startInteraction('scopeNodeFilter'); - const nodeToCollapse = treeNodeAtPath(this.state.tree!, path); + try { + const path = getPathOfNode(scopeNodeId, this.state.nodes); + const nodeToFilter = treeNodeAtPath(this.state.tree!, path); - if (!nodeToCollapse) { - throw new Error(`Trying to collapse node at path or id ${scopeNodeId} not found`); + if (!nodeToFilter) { + throw new Error(`Trying to filter node at path or id ${scopeNodeId} not found`); + } + + if (nodeToFilter.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToFilter.scopeNodeId])) { + throw new Error(`Trying to filter node at id ${scopeNodeId} that is not expandable`); + } + + const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { + treeNode.expanded = true; + treeNode.query = query; + }); + this.updateState({ tree: newTree }); + + await this.loadNodeChildren(path, nodeToFilter, query); + // Catch and throw error so we can ensure the profiler is stopped + } catch (error) { + throw error; + } finally { + this.interactionProfiler?.stopInteraction(); } - - const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { - treeNode.expanded = false; - treeNode.query = ''; - }); - this.updateState({ tree: newTree }); }; private loadNodeChildren = async (path: string[], treeNode: TreeNode, query?: string) => { @@ -332,15 +303,6 @@ export class ScopesSelectorService extends ScopesServiceBase { - if (expanded) { - return this.expandOrFilterNode(scopeNodeId, query); - } - return this.collapseNode(scopeNodeId); - }; - changeScopes = (scopeNames: string[], parentNodeId?: string, scopeNodeId?: string, redirectOnApply?: boolean) => { return this.applyScopes( scopeNames.map((id, index) => ({ @@ -494,7 +456,7 @@ export class ScopesSelectorService extends ScopesServiceBase { if (!this.state.tree?.children || Object.keys(this.state.tree?.children).length === 0) { - await this.expandOrFilterNode(''); + await this.filterNode('', ''); } // First close all nodes From cb05a4ae1bb9f0875b75b48c530767db1031ebae Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Thu, 27 Nov 2025 01:03:36 -0500 Subject: [PATCH 150/378] usePullRequestParam: Provisioned dashboard preview banner, decode pull request url before sanitize (#114516) usePullRequestParam: decode url before sanitize --- .../components/Shared/PreviewBannerViewPR.tsx | 11 ++++++++--- .../provisioning/hooks/usePullRequestParam.ts | 8 ++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.tsx b/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.tsx index 8a796ee049c..15b814a14db 100644 --- a/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.tsx +++ b/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.tsx @@ -107,9 +107,14 @@ export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl, b {/* when repo type is not local, we show branch information */} {showBranchInfo(repoType, branchInfo) && ( - branch: - {targetBranch} {'\u2192'}{' '} - {configuredBranch} + branch:{' '} + + {targetBranch} + {' '} + {'\u2192'}{' '} + + {configuredBranch} + )} diff --git a/public/app/features/provisioning/hooks/usePullRequestParam.ts b/public/app/features/provisioning/hooks/usePullRequestParam.ts index 81d10210503..8ad124953a4 100644 --- a/public/app/features/provisioning/hooks/usePullRequestParam.ts +++ b/public/app/features/provisioning/hooks/usePullRequestParam.ts @@ -9,9 +9,9 @@ export const usePullRequestParam = () => { const repoType = params.get('repo_type'); return { - prURL: prParam ? textUtil.sanitizeUrl(prParam) : undefined, - newPrURL: newPrParam ? textUtil.sanitizeUrl(newPrParam) : undefined, - repoURL: repoUrl ? textUtil.sanitizeUrl(repoUrl) : undefined, - repoType: repoType ? textUtil.sanitizeUrl(repoType) : undefined, + prURL: prParam ? textUtil.sanitizeUrl(decodeURIComponent(prParam)) : undefined, + newPrURL: newPrParam ? textUtil.sanitizeUrl(decodeURIComponent(newPrParam)) : undefined, + repoURL: repoUrl ? textUtil.sanitizeUrl(decodeURIComponent(repoUrl)) : undefined, + repoType: repoType ? textUtil.sanitizeUrl(decodeURIComponent(repoType)) : undefined, }; }; From b47352478791e6b673d667bd2c0073a4e4b2a781 Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Thu, 27 Nov 2025 02:01:40 -0500 Subject: [PATCH 151/378] Provisioning: View in repository open containing folder (#114513) * Provisioning: View in repostiory open containing folder * i18n * comment * tweaks * Simplify for display --------- Co-authored-by: Clarity-89 --- .../Repository/RepositoryLink.tsx | 4 +- ...ositoryCard.tsx => RepositoryListItem.tsx} | 22 ++--- .../provisioning/Shared/RepositoryList.tsx | 4 +- public/app/features/provisioning/utils/git.ts | 98 +++++++++++++------ public/locales/en-US/grafana.json | 3 - 5 files changed, 79 insertions(+), 52 deletions(-) rename public/app/features/provisioning/Repository/{RepositoryCard.tsx => RepositoryListItem.tsx} (84%) diff --git a/public/app/features/provisioning/Repository/RepositoryLink.tsx b/public/app/features/provisioning/Repository/RepositoryLink.tsx index f97a448efa4..fe4dbd4b9c5 100644 --- a/public/app/features/provisioning/Repository/RepositoryLink.tsx +++ b/public/app/features/provisioning/Repository/RepositoryLink.tsx @@ -4,7 +4,7 @@ import { Trans } from '@grafana/i18n'; import { LinkButton, Stack, Text, TextLink } from '@grafana/ui'; import { useGetRepositoryQuery } from 'app/api/clients/provisioning/v0alpha1'; -import { getRepoHref } from '../utils/git'; +import { getRepoHrefForProvider } from '../utils/git'; type RepositoryLinkProps = { name?: string; @@ -19,7 +19,7 @@ export function RepositoryLink({ name, jobType }: RepositoryLinkProps) { return null; } - const repoHref = getRepoHref(repo.spec?.github); + const repoHref = getRepoHrefForProvider(repo.spec); if (jobType === 'sync') { return ( diff --git a/public/app/features/provisioning/Repository/RepositoryCard.tsx b/public/app/features/provisioning/Repository/RepositoryListItem.tsx similarity index 84% rename from public/app/features/provisioning/Repository/RepositoryCard.tsx rename to public/app/features/provisioning/Repository/RepositoryListItem.tsx index 8a8205e9a2f..841ec3bd928 100644 --- a/public/app/features/provisioning/Repository/RepositoryCard.tsx +++ b/public/app/features/provisioning/Repository/RepositoryListItem.tsx @@ -2,12 +2,13 @@ import { ReactNode } from 'react'; import { t, Trans } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; -import { Stack, Text, TextLink, Icon, Card, LinkButton, Badge } from '@grafana/ui'; +import { Badge, Card, LinkButton, Stack, Text, TextLink } from '@grafana/ui'; import { Repository, ResourceCount } from 'app/api/clients/provisioning/v0alpha1'; import { RepoIcon } from '../Shared/RepoIcon'; import { StatusBadge } from '../Shared/StatusBadge'; import { PROVISIONING_URL } from '../constants'; +import { getRepoHrefForProvider } from '../utils/git'; import { getIsReadOnlyWorkflows } from '../utils/repository'; import { SyncRepository } from './SyncRepository'; @@ -16,7 +17,7 @@ interface Props { repository: Repository; } -export function RepositoryCard({ repository }: Props) { +export function RepositoryListItem({ repository }: Props) { const isReadOnlyRepo = getIsReadOnlyWorkflows(repository.spec?.workflows); const { metadata, spec, status } = repository; const name = metadata?.name ?? ''; @@ -27,24 +28,13 @@ export function RepositoryCard({ repository }: Props) { if (spec?.type === 'github') { const { url = '', branch } = spec.github ?? {}; const branchUrl = branch ? `${url}/tree/${branch}` : url; + const href = getRepoHrefForProvider(spec) || branchUrl; meta.push( - - {branchUrl} + + {href.split('/').slice(3).join('/')} ); - - if (status?.webhook?.id) { - const webhookUrl = `${url}/settings/hooks/${status.webhook.id}`; - meta.push( - - - Webhook - - - - ); - } } else if (spec?.type === 'local') { meta.push( diff --git a/public/app/features/provisioning/Shared/RepositoryList.tsx b/public/app/features/provisioning/Shared/RepositoryList.tsx index 3bfadfe872a..038359c6e8b 100644 --- a/public/app/features/provisioning/Shared/RepositoryList.tsx +++ b/public/app/features/provisioning/Shared/RepositoryList.tsx @@ -4,7 +4,7 @@ import { t, Trans } from '@grafana/i18n'; import { Alert, Box, EmptyState, FilterInput, Icon, Stack, TextLink } from '@grafana/ui'; import { Repository } from 'app/api/clients/provisioning/v0alpha1'; -import { RepositoryCard } from '../Repository/RepositoryCard'; +import { RepositoryListItem } from '../Repository/RepositoryListItem'; import { useResourceStats } from '../Wizard/hooks/useResourceStats'; import { UPGRADE_URL } from '../constants'; import { useIsProvisionedInstance } from '../hooks/useIsProvisionedInstance'; @@ -89,7 +89,7 @@ export function RepositoryList({ items }: Props) { )} {filteredItems.length ? ( - filteredItems.map((item) => ) + filteredItems.map((item) => ) ) : ( { return `${github.url}/tree/${github.branch}`; }; +// Remove leading and trailing slashes from a string. +const stripSlashes = (s: string) => s.replace(/^\/+|\/+$/g, ''); + +// Split a path into segments and URL-encode each segment. +// Ensures the final URL remains valid for all providers (GitHub, GitLab, etc.). +const splitAndEncode = (s: string) => stripSlashes(s).split('/').map(encodeURIComponent); + +type BuildRepoUrlParams = { + baseUrl?: string; + branch?: string | null; + providerSegments: string[]; + path?: string | null; +}; + +const buildRepoUrl = ({ baseUrl, branch, providerSegments, path }: BuildRepoUrlParams) => { + if (!baseUrl) { + return undefined; + } + + // Normalize base URL: trim whitespace + remove trailing slashes. + const cleanBase = stripSlashes(baseUrl.trim()); + const cleanBranch = branch?.trim() || undefined; + + // Start composing URL parts: + // base URL + provider-specific segments (e.g., "tree", "blob", etc.) + const parts = [cleanBase, ...providerSegments]; + + // Append the branch name if present. + if (cleanBranch) { + parts.push(cleanBranch); + } + + // Append encoded path segments if provided. + // This ensures nested files like "src/utils/index.ts" produce safe URLs. + if (path) { + parts.push(...splitAndEncode(path.trim())); + } + + return parts.join('/'); +}; + export const getRepoHrefForProvider = (spec?: RepositorySpec) => { if (!spec || !spec.type) { return undefined; } switch (spec.type) { - case 'github': { - const url = spec.github?.url; - const branch = spec.github?.branch; - if (!url) { - return undefined; - } - return branch ? `${url}/tree/${branch}` : url; - } - - case 'gitlab': { - const url = spec.gitlab?.url; - const branch = spec.gitlab?.branch; - if (!url) { - return undefined; - } - return branch ? `${url}/-/tree/${branch}` : url; - } - case 'bitbucket': { - const url = spec.bitbucket?.url; - const branch = spec.bitbucket?.branch; - if (!url) { - return undefined; - } - return branch ? `${url}/src/${branch}` : url; - } - case 'git': { - // Return a generic URL for pure git repositories - return spec.git?.url; - } + case 'github': + return buildRepoUrl({ + baseUrl: spec.github?.url, + branch: spec.github?.branch, + providerSegments: ['tree'], + path: spec.github?.path, + }); + case 'gitlab': + return buildRepoUrl({ + baseUrl: spec.gitlab?.url, + branch: spec.gitlab?.branch, + providerSegments: ['-', 'tree'], + path: spec.gitlab?.path, + }); + case 'bitbucket': + return buildRepoUrl({ + baseUrl: spec.bitbucket?.url, + branch: spec.bitbucket?.branch, + providerSegments: ['src'], + path: spec.bitbucket?.path, + }); + case 'git': + return buildRepoUrl({ + baseUrl: spec.git?.url, + branch: spec.git?.branch, + providerSegments: ['tree'], + path: spec.git?.path, + }); default: return undefined; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index c525ce2e768..5a55ab0843f 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11920,9 +11920,6 @@ "source-code": "Source code" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Read only", "settings": "Settings", "view": "View" From b90b8f2a3410e8b602a04c265226b9498a289fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 27 Nov 2025 11:00:43 +0100 Subject: [PATCH 152/378] Dashboard: Sidebar / outline style fixes (#114487) * Dashboard: Sidebar / outline style fixes * fixing empty outline node * fixing empty outline node --- eslint-suppressions.json | 5 --- .../edit-pane/DashboardOutline.tsx | 39 ++++++++++++++----- .../scene/DashboardControls.tsx | 2 +- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index fde5e53587d..3f5773bf84f 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1912,11 +1912,6 @@ "count": 4 } }, - "public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx": { "no-restricted-syntax": { "count": 3 diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx index 4ce6536ac5c..97e17c34780 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { SceneObject } from '@grafana/scenes'; -import { Box, Icon, Sidebar, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui'; +import { Box, Icon, Sidebar, Text, useElementSelection, useStyles2 } from '@grafana/ui'; import { isRepeatCloneOrChildOf } from '../utils/clone'; import { DashboardInteractions } from '../utils/interactions'; @@ -85,6 +85,7 @@ function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index } aria-selected={isSelected} className={styles.container} onClick={onNodeClicked} + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions style={{ '--depth': depth } as React.CSSProperties} >
@@ -99,7 +100,7 @@ function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index } )} -
- {headers.map((header, i) => ( - { - this.onHeaderChange(i, h); - }} - onBlur={this.updateSettings} - onRemove={this.onHeaderRemove} - onReset={this.onHeaderReset} - /> - ))} -
- {!dataSourceConfig.readOnly && ( - - - - - - )} - - ); - } -} + )} + + ); +}); + +CustomHeadersSettings.displayName = 'CustomHeadersSettings'; export default CustomHeadersSettings; diff --git a/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx b/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx index 089fd514418..c63634be362 100644 --- a/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx +++ b/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx @@ -1,4 +1,4 @@ -import { PureComponent, createRef } from 'react'; +import { memo, useRef, useEffect } from 'react'; import { JsonExplorer, JsonExplorerConfig } from './json_explorer/json_explorer'; // We have made some monkey-patching of json-formatter-js so we can't switch right now @@ -10,45 +10,32 @@ interface Props { onDidRender?: (formattedJson: {}) => void; } -export class JSONFormatter extends PureComponent { - private wrapperRef = createRef(); +export const JSONFormatter = memo( + ({ className, json, config = { animateOpen: true }, open = 3, onDidRender }) => { + const wrapperRef = useRef(null); - static defaultProps = { - open: 3, - config: { - animateOpen: true, - }, - }; + useEffect(() => { + const wrapperEl = wrapperRef.current; + if (!wrapperEl) { + return; + } - componentDidMount() { - this.renderJson(); + const formatter = new JsonExplorer(json, open, config); + const hasChildren = wrapperEl.hasChildNodes(); + + if (hasChildren && wrapperEl.lastChild) { + wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild); + } else { + wrapperEl.appendChild(formatter.render()); + } + + if (onDidRender) { + onDidRender(formatter.json); + } + }, [json, config, open, onDidRender]); + + return
; } +); - componentDidUpdate() { - this.renderJson(); - } - - renderJson = () => { - const { json, config, open, onDidRender } = this.props; - const wrapperEl = this.wrapperRef.current; - const formatter = new JsonExplorer(json, open, config); - // @ts-ignore - const hasChildren: boolean = wrapperEl.hasChildNodes(); - if (hasChildren) { - // @ts-ignore - wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild); - } else { - // @ts-ignore - wrapperEl.appendChild(formatter.render()); - } - - if (onDidRender) { - onDidRender(formatter.json); - } - }; - - render() { - const { className } = this.props; - return
; - } -} +JSONFormatter.displayName = 'JSONFormatter'; diff --git a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx index 49c3f4b8966..db0f53084d8 100644 --- a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx +++ b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx @@ -1,45 +1,33 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryFn } from '@storybook/react'; -import { PureComponent } from 'react'; +import { memo, useState } from 'react'; import { Field } from '../Forms/Field'; import { Props, StatsPicker } from './StatsPicker'; -interface State { - stats: string[]; -} +const WrapperWithState = memo(({ placeholder, allowMultiple, menuPlacement, width }) => { + const [stats, setStats] = useState([]); -class WrapperWithState extends PureComponent { - constructor(props: Props) { - super(props); - this.state = { - stats: [], - }; - } + return ( + + { + action('Picked:')(newStats); + setStats(newStats); + }} + menuPlacement={menuPlacement} + width={width} + /> + + ); +}); - render() { - const { placeholder, allowMultiple, menuPlacement, width } = this.props; - const { stats } = this.state; - - return ( - - { - action('Picked:')(stats); - this.setState({ stats }); - }} - menuPlacement={menuPlacement} - width={width} - /> - - ); - } -} +WrapperWithState.displayName = 'WrapperWithState'; const meta: Meta = { title: 'Pickers/StatsPicker', diff --git a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx index 009ae565dfe..0bf185e1354 100644 --- a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx +++ b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx @@ -1,5 +1,5 @@ import { difference } from 'lodash'; -import { PureComponent } from 'react'; +import { memo, useEffect } from 'react'; import { fieldReducers, SelectableValue, FieldReducerInfo } from '@grafana/data'; @@ -18,54 +18,47 @@ export interface Props { filterOptions?: (ext: FieldReducerInfo) => boolean; } -export class StatsPicker extends PureComponent { - static defaultProps: Partial = { - allowMultiple: false, - }; +export const StatsPicker = memo( + ({ + placeholder, + onChange, + stats, + allowMultiple = false, + defaultStat, + className, + width, + menuPlacement, + inputId, + filterOptions, + }) => { + useEffect(() => { + const current = fieldReducers.list(stats); + if (current.length !== stats.length) { + const found = current.map((v) => v.id); + const notFound = difference(stats, found); + console.warn('Unknown stats', notFound, stats); + onChange(current.map((stat) => stat.id)); + } - componentDidMount() { - this.checkInput(); - } + // Make sure there is only one + if (!allowMultiple && stats.length > 1) { + console.warn('Removing extra stat', stats); + onChange([stats[0]]); + } - componentDidUpdate(prevProps: Props) { - this.checkInput(); - } + // Set the reducer from callback + if (defaultStat && stats.length < 1) { + onChange([defaultStat]); + } + }, [stats, allowMultiple, defaultStat, onChange]); - checkInput = () => { - const { stats, allowMultiple, defaultStat, onChange } = this.props; - - const current = fieldReducers.list(stats); - if (current.length !== stats.length) { - const found = current.map((v) => v.id); - const notFound = difference(stats, found); - console.warn('Unknown stats', notFound, stats); - onChange(current.map((stat) => stat.id)); - } - - // Make sure there is only one - if (!allowMultiple && stats.length > 1) { - console.warn('Removing extra stat', stats); - onChange([stats[0]]); - } - - // Set the reducer from callback - if (defaultStat && stats.length < 1) { - onChange([defaultStat]); - } - }; - - onSelectionChange = (item: SelectableValue) => { - const { onChange } = this.props; - if (Array.isArray(item)) { - onChange(item.map((v) => v.value)); - } else { - onChange(item && item.value ? [item.value] : []); - } - }; - - render() { - const { stats, allowMultiple, defaultStat, placeholder, className, menuPlacement, width, inputId, filterOptions } = - this.props; + const onSelectionChange = (item: SelectableValue) => { + if (Array.isArray(item)) { + onChange(item.map((v) => v.value)); + } else { + onChange(item && item.value ? [item.value] : []); + } + }; const select = fieldReducers.selectOptions(stats, filterOptions); return ( @@ -78,10 +71,12 @@ export class StatsPicker extends PureComponent { isSearchable={true} options={select.options} placeholder={placeholder} - onChange={this.onSelectionChange} + onChange={onSelectionChange} menuPlacement={menuPlacement} inputId={inputId} /> ); } -} +); + +StatsPicker.displayName = 'StatsPicker'; diff --git a/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx b/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx index 1e792aa95d8..37e3ae49473 100644 --- a/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx +++ b/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx @@ -1,5 +1,5 @@ import { Placement } from '@popperjs/core'; -import { Component, type JSX } from 'react'; +import { useState, useRef, useCallback, type JSX } from 'react'; import { PopoverContent } from './types'; @@ -21,37 +21,28 @@ interface Props { hideAfter?: number; } -interface State { - show: boolean; -} +const PopoverController = ({ placement = 'auto', content, children, hideAfter }: Props) => { + const [show, setShow] = useState(false); + const hideTimeoutRef = useRef | null>(null); -class PopoverController extends Component { - private hideTimeout: ReturnType | null = null; - state = { show: false }; - - showPopper = () => { - if (this.hideTimeout) { - clearTimeout(this.hideTimeout); + const showPopper = useCallback(() => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); } - this.setState({ show: true }); - }; + setShow(true); + }, []); - hidePopper = () => { - this.hideTimeout = setTimeout(() => { - this.setState({ show: false }); - }, this.props.hideAfter); - }; + const hidePopper = useCallback(() => { + hideTimeoutRef.current = setTimeout(() => { + setShow(false); + }, hideAfter); + }, [hideAfter]); - render() { - const { children, content, placement = 'auto' } = this.props; - const { show } = this.state; - - return children(this.showPopper, this.hidePopper, { - show, - placement, - content, - }); - } -} + return children(showPopper, hidePopper, { + show, + placement, + content, + }); +}; export { PopoverController }; diff --git a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx index ac53b424bdb..90b9263f55d 100644 --- a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx +++ b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx @@ -1,4 +1,4 @@ -import { PureComponent } from 'react'; +import { memo } from 'react'; import { getValueFormats, SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -19,58 +19,52 @@ function formatCreateLabel(input: string) { /** * https://developers.grafana.com/ui/latest/index.html?path=/docs/pickers-unitpicker--docs */ -export class UnitPicker extends PureComponent { - onChange = (value: SelectableValue) => { - this.props.onChange(value.value); - }; +export const UnitPicker = memo(({ onChange, value, width, id }) => { + // Set the current selection + let current: SelectableValue | undefined = undefined; - render() { - const { value, width, id } = this.props; + // All units + const unitGroups = getValueFormats(); - // Set the current selection - let current: SelectableValue | undefined = undefined; - - // All units - const unitGroups = getValueFormats(); - - // Need to transform the data structure to work well with Select - const groupOptions: CascaderOption[] = unitGroups.map((group) => { - const options = group.submenu.map((unit) => { - const sel = { - label: unit.text, - value: unit.value, - }; - if (unit.value === value) { - current = sel; - } - return sel; - }); - - return { - label: group.text, - value: group.text, - items: options, + // Need to transform the data structure to work well with Select + const groupOptions: CascaderOption[] = unitGroups.map((group) => { + const options = group.submenu.map((unit) => { + const sel = { + label: unit.text, + value: unit.value, }; + if (unit.value === value) { + current = sel; + } + return sel; }); - // Show the custom unit - if (value && !current) { - current = { value, label: value }; - } + return { + label: group.text, + value: group.text, + items: options, + }; + }); - return ( - - ); + // Show the custom unit + if (value && !current) { + current = { value, label: value }; } -} + + return ( + + ); +}); + +UnitPicker.displayName = 'UnitPicker'; diff --git a/public/app/features/dashboard-scene/scene/DashboardControlsMenu.test.tsx b/public/app/features/dashboard-scene/scene/DashboardControlsMenu.test.tsx index fc6ab425ef9..101bd69ec97 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControlsMenu.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControlsMenu.test.tsx @@ -1,4 +1,4 @@ -import { act, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { VariableHide } from '@grafana/data'; @@ -65,15 +65,13 @@ describe('DashboardControlsMenu', () => { }), ]; - act(() => { - render(); - }); + render(); // Should have rendered a dropdown expect(screen.getByRole('button')).toBeInTheDocument(); // Open the dropdown - userEvent.click(screen.getByRole('button')); + await userEvent.click(screen.getByRole('button')); expect(await screen.findByText('textVar1')).toBeInTheDocument(); expect(await screen.findByText('textVar2')).toBeInTheDocument(); expect(await screen.findByText('queryVar')).toBeInTheDocument(); @@ -104,7 +102,7 @@ describe('DashboardControlsMenu', () => { expect(screen.getByRole('button')).toBeInTheDocument(); // Open the dropdown - userEvent.click(screen.getByRole('button')); + await userEvent.click(screen.getByRole('button')); expect(await screen.findByText('textVar1')).toBeInTheDocument(); expect(await screen.findByText('customVar')).toBeInTheDocument(); expect(screen.queryByText('textVar2')).not.toBeInTheDocument(); From 12c6d7e83fb8ba89d74edc1d5e0a5fdd59fdfde4 Mon Sep 17 00:00:00 2001 From: Rafael Bortolon Paulovic Date: Fri, 28 Nov 2025 13:13:35 +0100 Subject: [PATCH 187/378] fix(unified): in-proc SQLite data migration (#114537) * feat: unified storage migrations integration tests * chore: add comment and adjust db path name * chore: refactor test cases into interface * fix: unified SQLite migration with SQLStore migrator * revert changes to newResourceDBProvider --- .../unified/migrations/resource_migration.go | 16 ++ pkg/storage/unified/migrations/service.go | 12 +- pkg/storage/unified/migrations/validator.go | 99 +++++-- pkg/storage/unified/resource/transaction.go | 25 ++ pkg/storage/unified/sql/bulk.go | 250 ++++++++++-------- pkg/storage/unified/sql/db/dbimpl/db.go | 5 + pkg/util/xorm/session.go | 11 +- 7 files changed, 286 insertions(+), 132 deletions(-) create mode 100644 pkg/storage/unified/resource/transaction.go diff --git a/pkg/storage/unified/migrations/resource_migration.go b/pkg/storage/unified/migrations/resource_migration.go index 281ee1c85e3..5bbed9c5722 100644 --- a/pkg/storage/unified/migrations/resource_migration.go +++ b/pkg/storage/unified/migrations/resource_migration.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util/xorm" "k8s.io/apimachinery/pkg/runtime/schema" @@ -72,6 +73,17 @@ func (m *ResourceMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) erro m.log.Info("Starting migration for all organizations", "org_count", len(orgs), "resources", m.resources) + if mg.Dialect.DriverName() == migrator.SQLite { + // reuse transaction in SQLite to avoid "database is locked" errors + tx, err := sess.Tx() + if err != nil { + m.log.Error("Failed to get transaction from session", "error", err) + return fmt.Errorf("failed to get transaction: %w", err) + } + ctx = resource.ContextWithTransaction(ctx, tx.Tx) + m.log.Info("Stored migrator transaction in context for bulk operations (SQLite compatibility)") + } + for _, org := range orgs { if err := m.migrateOrg(ctx, sess, org); err != nil { return err @@ -107,6 +119,10 @@ func (m *ResourceMigration) migrateOrg(ctx context.Context, sess *xorm.Session, m.log.Error("Migration failed", "org_id", org.ID, "error", err, "duration", time.Since(startTime)) return fmt.Errorf("migration failed for org %d (%s): %w", org.ID, org.Name, err) } + if response.Error != nil { + m.log.Error("Migration reported error", "org_id", org.ID, "error", response.Error.String(), "duration", time.Since(startTime)) + return fmt.Errorf("migration failed for org %d (%s): %w", org.ID, org.Name, fmt.Errorf("migration error: %s", response.Error.Message)) + } // Validate the migration results if err := m.validateMigration(migrationCtx, sess, response); err != nil { diff --git a/pkg/storage/unified/migrations/service.go b/pkg/storage/unified/migrations/service.go index 1a78f837066..23b8c2706ae 100644 --- a/pkg/storage/unified/migrations/service.go +++ b/pkg/storage/unified/migrations/service.go @@ -85,8 +85,13 @@ func RegisterMigrations( // Run all registered migrations (blocking) sec := cfg.Raw.Section("database") + migrationLocking := sec.Key("migration_locking").MustBool(true) + if mg.Dialect.DriverName() == sqlstoremigrator.SQLite { + // disable migration locking for SQLite to avoid "database is locked" errors in the bulk operations + migrationLocking = false + } if err := mg.RunMigrations(ctx, - sec.Key("migration_locking").MustBool(true), + migrationLocking, sec.Key("locking_attempt_timeout_sec").MustInt()); err != nil { return fmt.Errorf("unified storage data migration failed: %w", err) } @@ -98,12 +103,14 @@ func RegisterMigrations( func registerDashboardAndFolderMigration(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) { folders := schema.GroupResource{Group: "folder.grafana.app", Resource: "folders"} dashboards := schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"} + driverName := mg.Dialect.DriverName() folderCountValidator := NewCountValidator( client, folders, "dashboard", "org_id = ? and is_folder = true", + driverName, ) dashboardCountValidator := NewCountValidator( @@ -111,9 +118,10 @@ func registerDashboardAndFolderMigration(mg *sqlstoremigrator.Migrator, migrator dashboards, "dashboard", "org_id = ? and is_folder = false", + driverName, ) - folderTreeValidator := NewFolderTreeValidator(client, folders) + folderTreeValidator := NewFolderTreeValidator(client, folders, driverName) dashboardsAndFolders := NewResourceMigration( migrator, diff --git a/pkg/storage/unified/migrations/validator.go b/pkg/storage/unified/migrations/validator.go index 7a973976402..0bdc67cc205 100644 --- a/pkg/storage/unified/migrations/validator.go +++ b/pkg/storage/unified/migrations/validator.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util/xorm" "k8s.io/apimachinery/pkg/runtime/schema" @@ -57,6 +58,7 @@ type CountValidator struct { resource schema.GroupResource table string whereClause string + driverName string } func NewCountValidator( @@ -64,6 +66,7 @@ func NewCountValidator( resource schema.GroupResource, table string, whereClause string, + driverName string, ) Validator { return &CountValidator{ name: "CountValidator", @@ -71,6 +74,7 @@ func NewCountValidator( resource: resource, table: table, whereClause: whereClause, + driverName: driverName, } } @@ -120,22 +124,32 @@ func (v *CountValidator) Validate(ctx context.Context, sess *xorm.Session, respo return fmt.Errorf("failed to count %s: %w", v.table, err) } - // Get unified storage count using GetStats API - statsResp, err := v.client.GetStats(ctx, &resourcepb.ResourceStatsRequest{ - Namespace: summary.Namespace, - Kinds: []string{fmt.Sprintf("%s/%s", summary.Group, summary.Resource)}, - }) - if err != nil { - return fmt.Errorf("failed to get stats for %s/%s in namespace %s: %w", - summary.Group, summary.Resource, summary.Namespace, err) - } - - // Find the count for this specific resource type var unifiedCount int64 - for _, stat := range statsResp.Stats { - if stat.Group == summary.Group && stat.Resource == summary.Resource { - unifiedCount = stat.Count - break + if v.driverName == migrator.SQLite { + unifiedCount, err = sess.Table("resource"). + Where("namespace = ? AND `group` = ? AND resource = ?", + summary.Namespace, summary.Group, summary.Resource). + Count() + if err != nil { + return fmt.Errorf("failed to count resource table for %s/%s in namespace %s: %w", + summary.Group, summary.Resource, summary.Namespace, err) + } + } else { + // Get unified storage count using GetStats API + statsResp, err := v.client.GetStats(ctx, &resourcepb.ResourceStatsRequest{ + Namespace: summary.Namespace, + Kinds: []string{fmt.Sprintf("%s/%s", summary.Group, summary.Resource)}, + }) + if err != nil { + return fmt.Errorf("failed to get stats for %s/%s in namespace %s: %w", + summary.Group, summary.Resource, summary.Namespace, err) + } + // Find the count for this specific resource type + for _, stat := range statsResp.Stats { + if stat.Group == summary.Group && stat.Resource == summary.Resource { + unifiedCount = stat.Count + break + } } } @@ -162,19 +176,22 @@ func (v *CountValidator) Validate(ctx context.Context, sess *xorm.Session, respo } type FolderTreeValidator struct { - name string - client resourcepb.ResourceIndexClient - resource schema.GroupResource + name string + client resourcepb.ResourceIndexClient + resource schema.GroupResource + driverName string } func NewFolderTreeValidator( client resourcepb.ResourceIndexClient, resource schema.GroupResource, + driverName string, ) Validator { return &FolderTreeValidator{ - name: "FolderTreeValidator", - client: client, - resource: resource, + name: "FolderTreeValidator", + client: client, + resource: resource, + driverName: driverName, } } @@ -185,6 +202,12 @@ type legacyFolder struct { Title string `xorm:"title"` } +type unifiedFolder struct { + GUID string `xorm:"guid"` + Name string `xorm:"name"` + Folder string `xorm:"folder"` +} + func (v *FolderTreeValidator) Name() string { return v.name } @@ -218,7 +241,12 @@ func (v *FolderTreeValidator) Validate(ctx context.Context, sess *xorm.Session, } // Build unified storage folder parent map - unifiedParentMap, err := v.buildUnifiedFolderParentMap(ctx, summary.Namespace, log) + var unifiedParentMap map[string]string + if v.driverName == migrator.SQLite { + unifiedParentMap, err = v.buildUnifiedFolderParentMapSQLite(sess, summary.Namespace, log) + } else { + unifiedParentMap, err = v.buildUnifiedFolderParentMap(ctx, summary.Namespace, log) + } if err != nil { return fmt.Errorf("failed to build unified folder parent map: %w", err) } @@ -348,3 +376,30 @@ func (v *FolderTreeValidator) buildUnifiedFolderParentMap(ctx context.Context, n return parentMap, nil } + +func (v *FolderTreeValidator) buildUnifiedFolderParentMapSQLite(sess *xorm.Session, namespace string, log log.Logger) (map[string]string, error) { + var folders []unifiedFolder + err := sess.Table("resource"). + Cols("guid", "name", "folder"). + Where("namespace = ? AND resource = ?", namespace, "folder"). + Find(&folders) + if err != nil { + return nil, fmt.Errorf("failed to query unified folders: %w", err) + } + + parentMap := make(map[string]string) + for _, folder := range folders { + parentMap[folder.Name] = folder.Folder + } + + if len(parentMap) == 0 { + log.Debug("No unified folders found for namespace", "namespace", namespace) + return make(map[string]string), nil + } + + log.Debug("Built unified folder parent map", + "folder_count", len(parentMap), + "namespace", namespace) + + return parentMap, nil +} diff --git a/pkg/storage/unified/resource/transaction.go b/pkg/storage/unified/resource/transaction.go new file mode 100644 index 00000000000..7d47f02af71 --- /dev/null +++ b/pkg/storage/unified/resource/transaction.go @@ -0,0 +1,25 @@ +package resource + +import ( + "context" + "database/sql" +) + +type transactionContextKey struct{} + +// ContextWithTransaction returns a new context with the transaction stored directly. +// This is used for SQLite migrations where the transaction needs to be shared +// between the migration code and unified storage operations within the same process. +func ContextWithTransaction(ctx context.Context, tx *sql.Tx) context.Context { + return context.WithValue(ctx, transactionContextKey{}, tx) +} + +// TransactionFromContext retrieves the transaction from context +func TransactionFromContext(ctx context.Context) *sql.Tx { + if v := ctx.Value(transactionContextKey{}); v != nil { + if tx, ok := v.(*sql.Tx); ok { + return tx + } + } + return nil +} diff --git a/pkg/storage/unified/sql/bulk.go b/pkg/storage/unified/sql/bulk.go index 7d4d7165d50..6580975a764 100644 --- a/pkg/storage/unified/sql/bulk.go +++ b/pkg/storage/unified/sql/bulk.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/fullstorydev/grpchan/inprocgrpc" "github.com/google/uuid" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/sql/db" + "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" "github.com/grafana/grafana/pkg/storage/unified/sql/dbutil" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) @@ -111,6 +113,19 @@ func (b *backend) ProcessBulk(ctx context.Context, setting resource.BulkSettings } defer b.bulkLock.Finish(setting.Collection) + // If provided, reuse the inproc transaction for SQLite + if clientCtx := inprocgrpc.ClientContext(ctx); clientCtx != nil && b.dialect.DialectName() == "sqlite" { + if externalTx := resource.TransactionFromContext(clientCtx); externalTx != nil { + b.log.Info("Using SQLite transaction from client context") + rsp := &resourcepb.BulkResponse{} + err := b.processBulkWithTx(ctx, dbimpl.NewTx(externalTx), setting, iter, rsp) + if err != nil { + rsp.Error = resource.AsErrorResult(err) + } + return rsp + } + } + // We may want to first write parquet, then read parquet if b.dialect.DialectName() == "sqlite" { file, err := os.CreateTemp("", "grafana-bulk-export-*.parquet") @@ -151,109 +166,134 @@ func (b *backend) ProcessBulk(ctx context.Context, setting resource.BulkSettings func (b *backend) processBulk(ctx context.Context, setting resource.BulkSettings, iter resource.BulkRequestIterator) *resourcepb.BulkResponse { rsp := &resourcepb.BulkResponse{} err := b.db.WithTx(ctx, ReadCommitted, func(ctx context.Context, tx db.Tx) error { - rollbackWithError := func(err error) error { - txerr := tx.Rollback() - if txerr != nil { - b.log.Warn("rollback", "error", txerr) - } else { - b.log.Info("rollback") + return b.processBulkWithTx(ctx, tx, setting, iter, rsp) + }) + if err != nil { + rsp.Error = resource.AsErrorResult(err) + } + return rsp +} + +// processBulkWithTx performs the bulk operation using the provided transaction. +// This is used both when creating our own transaction and when reusing an external one. +func (b *backend) processBulkWithTx(ctx context.Context, tx db.Tx, setting resource.BulkSettings, iter resource.BulkRequestIterator, rsp *resourcepb.BulkResponse) error { + rollbackWithError := func(err error) error { + txerr := tx.Rollback() + if txerr != nil { + b.log.Warn("rollback", "error", txerr) + } else { + b.log.Info("rollback") + } + return err + } + bulk := &bulkWroker{ + ctx: ctx, + tx: tx, + dialect: b.dialect, + logger: logging.FromContext(ctx), + } + + // Calculate the RV based on incoming request timestamps + rv := newBulkRV() + + summaries := make(map[string]*resourcepb.BulkResponse_Summary, len(setting.Collection)) + + // First clear everything in the transaction + if setting.RebuildCollection { + for _, key := range setting.Collection { + summary, err := bulk.deleteCollection(key) + if err != nil { + return rollbackWithError(err) } + summaries[resource.NSGR(key)] = summary + rsp.Summary = append(rsp.Summary, summary) + } + } else { + for _, key := range setting.Collection { + summaries[resource.NSGR(key)] = &resourcepb.BulkResponse_Summary{ + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + } + } + } + + obj := &unstructured.Unstructured{} + + // Write each event into the history + for iter.Next() { + if iter.RollbackRequested() { + return rollbackWithError(nil) + } + req := iter.Request() + if req == nil { + return rollbackWithError(fmt.Errorf("missing request")) + } + rsp.Processed++ + + if req.Action == resourcepb.BulkRequest_UNKNOWN { + rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ + Key: req.Key, + Action: req.Action, + Error: "unknown action", + }) + continue + } + + err := obj.UnmarshalJSON(req.Value) + if err != nil { + rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ + Key: req.Key, + Action: req.Action, + Error: "unable to unmarshal json", + }) + continue + } + + // Write the event to history + if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryInsert, sqlResourceRequest{ + SQLTemplate: sqltemplate.New(b.dialect), + WriteEvent: resource.WriteEvent{ + Key: req.Key, + Type: resourcepb.WatchEvent_Type(req.Action), + Value: req.Value, + PreviousRV: -1, // Used for WATCH, but we want to skip watch events + }, + Folder: req.Folder, + GUID: uuid.New().String(), + ResourceVersion: rv.next(obj), + }); err != nil { + return rollbackWithError(fmt.Errorf("insert into resource history: %w", err)) + } + } + + // Now update the resource table from history + for _, key := range setting.Collection { + k := fmt.Sprintf("%s/%s/%s", key.Namespace, key.Group, key.Resource) + summary := summaries[k] + if summary == nil { + return rollbackWithError(fmt.Errorf("missing summary key for: %s", k)) + } + + err := bulk.syncCollection(key, summary) + if err != nil { return err } - bulk := &bulkWroker{ - ctx: ctx, - tx: tx, - dialect: b.dialect, - logger: logging.FromContext(ctx), - } - // Calculate the RV based on incoming request timestamps - rv := newBulkRV() - - summaries := make(map[string]*resourcepb.BulkResponse_Summary, len(setting.Collection)) - - // First clear everything in the transaction - if setting.RebuildCollection { - for _, key := range setting.Collection { - summary, err := bulk.deleteCollection(key) - if err != nil { - return rollbackWithError(err) + if b.dialect.DialectName() == "sqlite" { + nextRV, err := b.rvManager.lock(ctx, tx, key.Group, key.Resource) + if err != nil { + b.log.Error("error locking RV", "error", err, "key", resource.NSGR(key)) + } else { + b.log.Info("successfully locked RV", "nextRV", nextRV, "key", resource.NSGR(key)) + // Save the incremented RV + if err := b.rvManager.saveRV(ctx, tx, key.Group, key.Resource, nextRV); err != nil { + b.log.Error("error saving RV", "error", err, "key", resource.NSGR(key)) + } else { + b.log.Info("successfully saved RV", "rv", nextRV, "key", resource.NSGR(key)) } - summaries[resource.NSGR(key)] = summary - rsp.Summary = append(rsp.Summary, summary) } } else { - for _, key := range setting.Collection { - summaries[resource.NSGR(key)] = &resourcepb.BulkResponse_Summary{ - Namespace: key.Namespace, - Group: key.Group, - Resource: key.Resource, - } - } - } - - obj := &unstructured.Unstructured{} - - // Write each event into the history - for iter.Next() { - if iter.RollbackRequested() { - return rollbackWithError(nil) - } - req := iter.Request() - if req == nil { - return rollbackWithError(fmt.Errorf("missing request")) - } - rsp.Processed++ - - if req.Action == resourcepb.BulkRequest_UNKNOWN { - rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ - Key: req.Key, - Action: req.Action, - Error: "unknown action", - }) - continue - } - - err := obj.UnmarshalJSON(req.Value) - if err != nil { - rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ - Key: req.Key, - Action: req.Action, - Error: "unable to unmarshal json", - }) - continue - } - - // Write the event to history - if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryInsert, sqlResourceRequest{ - SQLTemplate: sqltemplate.New(b.dialect), - WriteEvent: resource.WriteEvent{ - Key: req.Key, - Type: resourcepb.WatchEvent_Type(req.Action), - Value: req.Value, - PreviousRV: -1, // Used for WATCH, but we want to skip watch events - }, - Folder: req.Folder, - GUID: uuid.New().String(), - ResourceVersion: rv.next(obj), - }); err != nil { - return rollbackWithError(fmt.Errorf("insert into resource history: %w", err)) - } - } - - // Now update the resource table from history - for _, key := range setting.Collection { - k := fmt.Sprintf("%s/%s/%s", key.Namespace, key.Group, key.Resource) - summary := summaries[k] - if summary == nil { - return rollbackWithError(fmt.Errorf("missing summary key for: %s", k)) - } - - err := bulk.syncCollection(key, summary) - if err != nil { - return err - } - // Make sure the collection RV is above our last written event _, err = b.rvManager.ExecWithRV(ctx, key, func(tx db.Tx) (string, error) { return "", nil @@ -261,19 +301,15 @@ func (b *backend) processBulk(ctx context.Context, setting resource.BulkSettings if err != nil { b.log.Warn("error increasing RV", "error", err) } - - // Update the last import time. This is important to trigger reindexing - // of the resource for a given namespace. - if err := b.updateLastImportTime(ctx, tx, key, time.Now()); err != nil { - return rollbackWithError(err) - } } - return nil - }) - if err != nil { - rsp.Error = resource.AsErrorResult(err) + + // Update the last import time. This is important to trigger reindexing + // of the resource for a given namespace. + if err := b.updateLastImportTime(ctx, tx, key, time.Now()); err != nil { + return rollbackWithError(err) + } } - return rsp + return nil } func (b *backend) updateLastImportTime(ctx context.Context, tx db.Tx, key *resourcepb.ResourceKey, now time.Time) error { diff --git a/pkg/storage/unified/sql/db/dbimpl/db.go b/pkg/storage/unified/sql/db/dbimpl/db.go index 262b03334aa..00099733bcf 100644 --- a/pkg/storage/unified/sql/db/dbimpl/db.go +++ b/pkg/storage/unified/sql/db/dbimpl/db.go @@ -48,6 +48,11 @@ type sqlTx struct { *sql.Tx } +// NewTx wraps an existing *sql.Tx with sqlTx +func NewTx(tx *sql.Tx) db.Tx { + return sqlTx{tx} +} + func (tx sqlTx) QueryContext(ctx context.Context, query string, args ...any) (db.Rows, error) { // // codeql-suppress go/sql-query-built-from-user-controlled-sources "The query comes from a safe template source // and the parameters are passed as arguments." diff --git a/pkg/util/xorm/session.go b/pkg/util/xorm/session.go index b174b8e4ed9..03edf4b2983 100644 --- a/pkg/util/xorm/session.go +++ b/pkg/util/xorm/session.go @@ -7,6 +7,7 @@ package xorm import ( "context" "database/sql" + "errors" "fmt" "hash/crc32" "reflect" @@ -43,7 +44,7 @@ type Session struct { afterProcessors []executedProcessor prepareStmt bool - stmtCache map[uint32]*core.Stmt //key: hash.Hash32 of (queryStr, len(queryStr)) + stmtCache map[uint32]*core.Stmt // key: hash.Hash32 of (queryStr, len(queryStr)) // !evalphobia! stored the last executed query on this session lastSQL string @@ -236,6 +237,14 @@ func (session *Session) DB() *core.DB { return session.db } +// Tx returns the underlying transaction +func (session *Session) Tx() (*core.Tx, error) { + if session.tx == nil { + return nil, errors.New("no open transaction") + } + return session.tx, nil +} + func cleanupProcessorsClosures(slices *[]func(any)) { if len(*slices) > 0 { *slices = make([]func(any), 0) From 43c3322cafe8e1e7ba8600df8394293b374bae52 Mon Sep 17 00:00:00 2001 From: Ana Ivanov <38096095+anaivanov@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:28:19 +0100 Subject: [PATCH 188/378] Increase limitPerPlugin from 40 to 80 (#114573) * Increase limitPerPlugin from 40 to 60 * Increase limitPerPlugin from 60 to 80 --- .../app/features/commandPalette/actions/useExtensionActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/commandPalette/actions/useExtensionActions.ts b/public/app/features/commandPalette/actions/useExtensionActions.ts index 112bdc5ab0d..643a4647e2d 100644 --- a/public/app/features/commandPalette/actions/useExtensionActions.ts +++ b/public/app/features/commandPalette/actions/useExtensionActions.ts @@ -13,7 +13,7 @@ export default function useExtensionActions(): CommandPaletteAction[] { const { links } = usePluginLinks({ extensionPointId: PluginExtensionPoints.CommandPalette, context, - limitPerPlugin: 40, + limitPerPlugin: 80, }); return useMemo(() => { From 3c76e9ee7203f0cddb7ea0a56a8499f86f679824 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Fri, 28 Nov 2025 12:42:36 +0000 Subject: [PATCH 189/378] Teams: Refactor most functionality to use hooks (#113713) --- eslint-suppressions.json | 5 - .../src/clients/rtkq/legacy/endpoints.gen.ts | 14 +- .../src/handlers/all-handlers.ts | 6 +- .../handlers/api/access-control/handlers.ts | 11 ++ .../src/handlers/api/teams/handlers.ts | 21 +++ pkg/services/team/model.go | 10 +- pkg/services/team/teamapi/team.go | 4 + public/api-enterprise-spec.json | 11 +- public/api-merged.json | 17 +- public/app/api/clients/iam/v0alpha1/index.ts | 4 +- .../app/core/components/RolePicker/utils.ts | 12 +- public/app/core/reducers/root.test.ts | 64 -------- public/app/features/teams/CreateTeam.test.tsx | 2 +- public/app/features/teams/CreateTeam.tsx | 52 ++++-- .../app/features/teams/TeamGroupSync.test.tsx | 6 +- public/app/features/teams/TeamGroupSync.tsx | 7 +- public/app/features/teams/TeamList.test.tsx | 6 +- public/app/features/teams/TeamList.tsx | 117 ++++++------- public/app/features/teams/TeamPages.tsx | 19 +-- .../app/features/teams/TeamSettings.test.tsx | 28 ++-- public/app/features/teams/TeamSettings.tsx | 25 ++- public/app/features/teams/hooks.ts | 154 ++++++++++++++++++ public/app/features/teams/state/actions.ts | 130 ++------------- .../app/features/teams/state/reducers.test.ts | 74 --------- public/app/features/teams/state/reducers.ts | 60 +------ .../features/teams/state/selectors.test.ts | 22 --- public/app/features/teams/state/selectors.ts | 10 +- public/app/types/accessControl.ts | 15 +- public/app/types/teams.ts | 53 +----- public/locales/en-US/grafana.json | 2 + public/openapi3.json | 19 ++- 31 files changed, 384 insertions(+), 596 deletions(-) create mode 100644 packages/grafana-test-utils/src/handlers/api/access-control/handlers.ts delete mode 100644 public/app/core/reducers/root.test.ts create mode 100644 public/app/features/teams/hooks.ts delete mode 100644 public/app/features/teams/state/reducers.test.ts delete mode 100644 public/app/features/teams/state/selectors.test.ts diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 71b855ae3b9..e3e822abdc4 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3183,11 +3183,6 @@ "count": 1 } }, - "public/app/features/teams/state/reducers.ts": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/features/templating/fieldAccessorCache.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 diff --git a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts index 45ff0d1e91e..e0f6b885e00 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts @@ -1644,7 +1644,12 @@ const injectedRtkApi = api invalidatesTags: ['teams'], }), getTeamById: build.query({ - query: (queryArg) => ({ url: `/teams/${queryArg.teamId}` }), + query: (queryArg) => ({ + url: `/teams/${queryArg.teamId}`, + params: { + accesscontrol: queryArg.accesscontrol, + }, + }), providesTags: ['teams'], }), updateTeam: build.mutation({ @@ -3474,6 +3479,7 @@ export type DeleteTeamByIdApiArg = { export type GetTeamByIdApiResponse = /** status 200 (empty) */ TeamDto; export type GetTeamByIdApiArg = { teamId: string; + accesscontrol?: boolean; }; export type UpdateTeamApiResponse = /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; @@ -6051,10 +6057,8 @@ export type SearchTeamGroupsQueryResult = { totalCount?: number; }; export type UpdateTeamCommand = { - Email?: string; - ExternalUID?: string; - ID?: number; - Name?: string; + email?: string; + name?: string; }; export type TeamMemberDto = { auth_module?: string; diff --git a/packages/grafana-test-utils/src/handlers/all-handlers.ts b/packages/grafana-test-utils/src/handlers/all-handlers.ts index 5a0f1cdc140..5fa473b55d5 100644 --- a/packages/grafana-test-utils/src/handlers/all-handlers.ts +++ b/packages/grafana-test-utils/src/handlers/all-handlers.ts @@ -1,5 +1,6 @@ import { HttpHandler } from 'msw'; +import accessControlHandlers from './api/access-control/handlers'; import dashboardsHandlers from './api/dashboards/handlers'; import folderHandlers from './api/folders/handlers'; import pluginsHandlers from './api/plugins/handlers'; @@ -14,11 +15,12 @@ import appPlatformIamv0alpha1Handlers from './apis/iam.grafana.app/v0alpha1/hand const allHandlers: HttpHandler[] = [ // Legacy handlers - ...teamsHandlers, + ...accessControlHandlers, ...dashboardsHandlers, ...folderHandlers, - ...searchHandlers, ...pluginsHandlers, + ...searchHandlers, + ...teamsHandlers, ...userHandlers, // App platform handlers diff --git a/packages/grafana-test-utils/src/handlers/api/access-control/handlers.ts b/packages/grafana-test-utils/src/handlers/api/access-control/handlers.ts new file mode 100644 index 00000000000..ce5f8f43d5e --- /dev/null +++ b/packages/grafana-test-utils/src/handlers/api/access-control/handlers.ts @@ -0,0 +1,11 @@ +import { HttpResponse, http } from 'msw'; + +const searchTeamRolesHandler = () => + http.post('/api/access-control/teams/roles/search', async () => { + // TODO: Add better mock roles response as needed + return HttpResponse.json([]); + }); + +const handlers = [searchTeamRolesHandler()]; + +export default handlers; diff --git a/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts b/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts index e9fb6c0a112..c8a42bf9bba 100644 --- a/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts +++ b/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts @@ -127,6 +127,26 @@ const createTeamHandler = () => return HttpResponse.json({ message: 'Team created', teamId: 10, uid: 'aethyfifmhwcgd' }, { status: 200 }); }); +const updateTeamHandler = () => + http.put<{ uid: string }, { name: string; email: string }>('/api/teams/:uid', async ({ params, request }) => { + const teamData = mockTeamsMap.get(params.uid); + const body = await request.json(); + if (!teamData) { + return HttpResponse.json({ message: 'Not found' }, { status: 404 }); + } + const updatedTeam = { + ...teamData, + team: { + ...teamData.team, + name: body.name, + email: body.email, + }, + }; + mockTeamsMap.set(params.uid, updatedTeam); + + return HttpResponse.json({ message: 'Team updated' }); + }); + const handlers = [ teamsPreferencesHandler(), teamsGroupsHandler(), @@ -135,6 +155,7 @@ const handlers = [ getTeamHandler(), deleteTeamHandler(), createTeamHandler(), + updateTeamHandler(), ]; export default handlers; diff --git a/pkg/services/team/model.go b/pkg/services/team/model.go index 4d01c0a3d0b..3696505139a 100644 --- a/pkg/services/team/model.go +++ b/pkg/services/team/model.go @@ -47,11 +47,11 @@ type CreateTeamCommand struct { } type UpdateTeamCommand struct { - ID int64 - Name string - Email string - ExternalUID string - OrgID int64 `json:"-"` + ID int64 `json:"-"` + Name string `json:"name"` + Email string `json:"email"` + ExternalUID string `json:"-"` + OrgID int64 `json:"-"` } type DeleteTeamCommand struct { diff --git a/pkg/services/team/teamapi/team.go b/pkg/services/team/teamapi/team.go index 9fca7089d2b..230d4ab0f2d 100644 --- a/pkg/services/team/teamapi/team.go +++ b/pkg/services/team/teamapi/team.go @@ -312,6 +312,10 @@ type GetTeamByIDParams struct { // in:path // required:true TeamID string `json:"team_id"` + // in:query + // required:false + // default: false + AccessControl bool `json:"accesscontrol"` } // swagger:parameters deleteTeamByID diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index c976dcb9e7d..054cf609e40 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -8744,17 +8744,10 @@ "UpdateTeamCommand": { "type": "object", "properties": { - "Email": { + "email": { "type": "string" }, - "ExternalUID": { - "type": "string" - }, - "ID": { - "type": "integer", - "format": "int64" - }, - "Name": { + "name": { "type": "string" } } diff --git a/public/api-merged.json b/public/api-merged.json index 0d9c4129adc..12dd95170cf 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -10117,6 +10117,12 @@ "name": "team_id", "in": "path", "required": true + }, + { + "type": "boolean", + "default": false, + "name": "accesscontrol", + "in": "query" } ], "responses": { @@ -23164,17 +23170,10 @@ "UpdateTeamCommand": { "type": "object", "properties": { - "Email": { + "email": { "type": "string" }, - "ExternalUID": { - "type": "string" - }, - "ID": { - "type": "integer", - "format": "int64" - }, - "Name": { + "name": { "type": "string" } } diff --git a/public/app/api/clients/iam/v0alpha1/index.ts b/public/app/api/clients/iam/v0alpha1/index.ts index 190f7ede868..f54e38f7e15 100644 --- a/public/app/api/clients/iam/v0alpha1/index.ts +++ b/public/app/api/clients/iam/v0alpha1/index.ts @@ -2,7 +2,5 @@ import { generatedAPI } from '@grafana/api-clients/rtkq/iam/v0alpha1'; export const iamAPIv0alpha1 = generatedAPI.enhanceEndpoints({}); -export const { useGetDisplayMappingQuery, useLazyGetDisplayMappingQuery } = iamAPIv0alpha1; - // eslint-disable-next-line no-barrel-files/no-barrel-files -export type { DisplayList } from '@grafana/api-clients/rtkq/iam/v0alpha1'; +export * from '@grafana/api-clients/rtkq/iam/v0alpha1'; diff --git a/public/app/core/components/RolePicker/utils.ts b/public/app/core/components/RolePicker/utils.ts index 413671f0090..f386634e307 100644 --- a/public/app/core/components/RolePicker/utils.ts +++ b/public/app/core/components/RolePicker/utils.ts @@ -1,3 +1,4 @@ +import { RoleDto } from 'app/api/clients/legacy'; import { Role } from 'app/types/accessControl'; export const isNotDelegatable = (role: Role) => { @@ -23,9 +24,10 @@ export const addDisplayNameForFixedRole = (role: Role) => { // Adds a display name for use when the list of roles is filtered // If either group or displayName are undefined, we fall back (see RoleMenuOption.tsx) -export const addFilteredDisplayName = (role: Role) => { - if (role.group && role.displayName) { - role.filteredDisplayName = role.group + ':' + role.displayName; - } - return role; +export const addFilteredDisplayName = (role: RoleDto): Role => { + const filteredDisplayName = role.group && role.displayName ? `${role.group}:${role.displayName}` : ''; + return { + ...role, + filteredDisplayName, + }; }; diff --git a/public/app/core/reducers/root.test.ts b/public/app/core/reducers/root.test.ts deleted file mode 100644 index c9f5d5068bf..00000000000 --- a/public/app/core/reducers/root.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Team } from 'app/types/teams'; - -import { reducerTester } from '../../../test/core/redux/reducerTester'; -import { initialTeamsState, teamsLoaded } from '../../features/teams/state/reducers'; -import { StoreState } from '../../types/store'; -import { cleanUpAction } from '../actions/cleanUp'; - -import { createRootReducer } from './root'; - -describe('rootReducer', () => { - const rootReducer = createRootReducer(); - - describe('when called with any action except cleanUpAction', () => { - it('then it should not clean state', () => { - const teams = [{ id: 1 } as Team]; - const state = { - teams: { ...initialTeamsState }, - } as StoreState; - - reducerTester() - .givenReducer(rootReducer, state) - .whenActionIsDispatched(teamsLoaded({ teams: teams, page: 1, noTeams: false, perPage: 30, totalCount: 1 })) - .thenStatePredicateShouldEqual((resultingState) => { - expect(resultingState.teams).toEqual({ - hasFetched: true, - noTeams: false, - perPage: 30, - totalPages: 1, - query: '', - page: 1, - teams, - }); - return true; - }); - }); - }); - - describe('when called with cleanUpAction', () => { - it('then it should clean state', () => { - const teams = [{ id: 1 }] as Team[]; - const state: StoreState = { - teams: { - hasFetched: true, - query: '', - page: 1, - noTeams: false, - totalPages: 1, - perPage: 30, - teams, - }, - } as StoreState; - - reducerTester() - .givenReducer(rootReducer, state, false, true) - .whenActionIsDispatched( - cleanUpAction({ cleanupAction: (storeState) => (storeState.teams = initialTeamsState) }) - ) - .thenStatePredicateShouldEqual((resultingState) => { - expect(resultingState.teams).toEqual({ ...initialTeamsState }); - return true; - }); - }); - }); -}); diff --git a/public/app/features/teams/CreateTeam.test.tsx b/public/app/features/teams/CreateTeam.test.tsx index 519b835cd52..12db809d40f 100644 --- a/public/app/features/teams/CreateTeam.test.tsx +++ b/public/app/features/teams/CreateTeam.test.tsx @@ -8,7 +8,7 @@ import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; -import { CreateTeam } from './CreateTeam'; +import CreateTeam from './CreateTeam'; setBackendSrv(backendSrv); setupMockServer(); diff --git a/public/app/features/teams/CreateTeam.tsx b/public/app/features/teams/CreateTeam.tsx index 050f7aefa1e..82ee804f6d3 100644 --- a/public/app/features/teams/CreateTeam.tsx +++ b/public/app/features/teams/CreateTeam.tsx @@ -3,16 +3,19 @@ import { useForm } from 'react-hook-form'; import { NavModelItem } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { getBackendSrv, locationService } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import { Button, Field, Input, FieldSet, Stack } from '@grafana/ui'; +import { extractErrorMessage } from 'app/api/utils'; import { Page } from 'app/core/components/Page/Page'; import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker'; -import { updateTeamRoles } from 'app/core/components/RolePicker/api'; import { useRoleOptions } from 'app/core/components/RolePicker/hooks'; +import { useAppNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/services/context_srv'; -import { Role, AccessControlAction } from 'app/types/accessControl'; +import { Role } from 'app/types/accessControl'; import { TeamDTO } from 'app/types/teams'; +import { useCreateTeam } from './hooks'; + const pageNav: NavModelItem = { icon: 'users-alt', id: 'team-new', @@ -20,8 +23,11 @@ const pageNav: NavModelItem = { subTitle: 'Create a new team. Teams let you grant permissions to a group of users.', }; -export const CreateTeam = (): JSX.Element => { +const CreateTeam = (): JSX.Element => { const currentOrgId = contextSrv.user.orgId; + + const notifyApp = useAppNotification(); + const [createTeamTrigger] = useCreateTeam(); const [pendingRoles, setPendingRoles] = useState([]); const [{ roleOptions }] = useRoleOptions(currentOrgId); const { @@ -30,21 +36,28 @@ export const CreateTeam = (): JSX.Element => { formState: { errors }, } = useForm(); - const canUpdateRoles = - contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) && - contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove); - const createTeam = async (formModel: TeamDTO) => { try { - const newTeam = await getBackendSrv().post('/api/teams', formModel); - if (newTeam.teamId) { - await contextSrv.fetchUserPermissions(); - if (contextSrv.licensedAccessControlEnabled() && canUpdateRoles) { - await updateTeamRoles(pendingRoles, newTeam.teamId, newTeam.orgId); - } - locationService.push(`/org/teams/edit/${newTeam.uid}`); + const { data, error } = await createTeamTrigger( + { + email: formModel.email || '', + name: formModel.name, + }, + pendingRoles + ); + + const errorMessage = error ? extractErrorMessage(error) : undefined; + + if (errorMessage) { + notifyApp.error(errorMessage); + return; + } + + if (data && data.uid) { + locationService.push(`/org/teams/edit/${data.uid}`); } } catch (e) { + notifyApp.error(t('teams.create-team.failed-to-create', 'Failed to create team')); console.error(e); } }; @@ -85,8 +98,13 @@ export const CreateTeam = (): JSX.Element => { 'This is optional and is primarily used for allowing custom team avatars' )} > - {/* eslint-disable-next-line @grafana/i18n/no-untranslated-strings */} - + diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx index e3bd21b7e3a..d3a83d9c5dc 100644 --- a/public/app/features/teams/TeamGroupSync.test.tsx +++ b/public/app/features/teams/TeamGroupSync.test.tsx @@ -4,7 +4,7 @@ import { setBackendSrv } from '@grafana/runtime'; import { setupMockServer } from '@grafana/test-utils/server'; import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; import { backendSrv } from 'app/core/services/backend_srv'; -import { Team, TeamGroup, TeamState } from 'app/types/teams'; +import { TeamGroup, TeamState } from 'app/types/teams'; import TeamGroupSync from './TeamGroupSync'; import { getMockTeamGroups } from './mocks/teamMocks'; @@ -13,12 +13,10 @@ setBackendSrv(backendSrv); setupMockServer(); const setup = (preloadedTeamState?: Partial) => { - return render(, { + return render(, { preloadedState: { team: { - members: [], groups: [], - team: { uid: MOCK_TEAMS[0].metadata.name } as Team, ...preloadedTeamState, }, }, diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index e87a1d4380f..37887ee8d6a 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -29,6 +29,7 @@ const mapDispatchToProps = { interface OwnProps { isReadOnly: boolean; + teamUid: string; } interface State { @@ -52,7 +53,7 @@ export class TeamGroupSync extends PureComponent { } async fetchTeamGroups() { - this.props.loadTeamGroups(); + this.props.loadTeamGroups(this.props.teamUid); } onToggleAdding = () => { @@ -65,12 +66,12 @@ export class TeamGroupSync extends PureComponent { onAddGroup: FormEventHandler = (event) => { event.preventDefault(); - this.props.addTeamGroup(this.state.newGroupId); + this.props.addTeamGroup(this.props.teamUid, this.state.newGroupId); this.setState({ isAdding: false, newGroupId: '' }); }; onRemoveGroup = (group: TeamGroup) => { - this.props.removeTeamGroup(group.groupId); + this.props.removeTeamGroup(this.props.teamUid, group.groupId); }; isNewGroupValid() { diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index a78cbe2eceb..017b6ad2d25 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -40,16 +40,16 @@ describe('TeamList', () => { it('should enable the new team button', async () => { render(); - expect(screen.getByRole('link', { name: /new team/i })).not.toHaveStyle('pointer-events: none'); + expect(await screen.findByRole('link', { name: /new team/i })).not.toHaveStyle('pointer-events: none'); }); }); describe('when user does not have access to create a team', () => { - it('should disable the new team button', () => { + it('should disable the new team button', async () => { jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); render(); - expect(screen.getByRole('link', { name: /new team/i })).toHaveStyle('pointer-events: none'); + expect(await screen.findByRole('link', { name: /new team/i })).toHaveStyle('pointer-events: none'); }); }); }); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index e2ecbf07c4a..3ffd09b94fe 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import Skeleton from 'react-loading-skeleton'; -import { connect, ConnectedProps } from 'react-redux'; +import { SortingRule } from 'react-table'; import { Trans, t } from '@grafana/i18n'; import { @@ -14,6 +14,7 @@ import { InlineField, InteractiveTable, LinkButton, + LoadingPlaceholder, Pagination, Stack, Tag, @@ -24,16 +25,14 @@ import { Page } from 'app/core/components/Page/Page'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { contextSrv } from 'app/core/services/context_srv'; import { Role, AccessControlAction } from 'app/types/accessControl'; -import { StoreState } from 'app/types/store'; import { TeamWithRoles } from 'app/types/teams'; import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker'; import { EnterpriseAuthFeaturesCard } from '../admin/EnterpriseAuthFeaturesCard'; -import { deleteTeam, loadTeams, changePage, changeQuery, changeSort } from './state/actions'; +import { useDeleteTeam, useGetTeams } from './hooks'; type Cell = CellProps; -export interface OwnProps {} export interface State { roleOptions: Role[]; @@ -49,26 +48,31 @@ const skeletonData: TeamWithRoles[] = new Array(3).fill(null).map((_, index) => isProvisioned: false, })); -const TeamList = ({ - teams, - query, - noTeams, - hasFetched, - loadTeams, - deleteTeam, - changeQuery, - totalPages, - page, - rolesLoading, - changePage, - changeSort, -}: Props) => { +const TeamList = () => { + const canCreate = contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate); + const displayRolePicker = shouldDisplayRolePicker(); + const pageSize = 20; + const [roleOptions, setRoleOptions] = useState([]); const styles = useStyles2(getStyles); + const [query, setQuery] = useState(''); + const [page, setPage] = useState(1); + const [sort, setSort] = useState(); + const { data, isLoading } = useGetTeams({ query, pageSize, page, sort }); + const [deleteTeam] = useDeleteTeam(); - useEffect(() => { - loadTeams(true); - }, [loadTeams]); + const teams = data?.teams || []; + const totalPages = Math.ceil((data?.totalCount || 0) / pageSize) || 0; + const noTeams = teams?.length === 0; + const changeSort = useCallback( + (sort: SortingRule) => { + setSort(`${sort.id}-${sort.desc ? 'desc' : 'asc'}`); + }, + [setSort] + ); + const changePage = (page: number) => { + setPage(page); + }; useEffect(() => { if (contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { @@ -76,9 +80,6 @@ const TeamList = ({ } }, []); - const canCreate = contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate); - const displayRolePicker = shouldDisplayRolePicker(); - const columns: Array> = useMemo( () => [ { @@ -86,7 +87,7 @@ const TeamList = ({ header: '', disableGrow: true, cell: ({ cell: { value } }: Cell<'avatarUrl'>) => { - if (!hasFetched) { + if (isLoading) { return ; } @@ -97,7 +98,7 @@ const TeamList = ({ id: 'name', header: 'Name', cell: ({ cell: { value }, row: { original } }: Cell<'name'>) => { - if (!hasFetched) { + if (isLoading) { return ; } @@ -123,7 +124,7 @@ const TeamList = ({ id: 'email', header: 'Email', cell: ({ cell: { value } }: Cell<'email'>) => { - if (!hasFetched) { + if (isLoading) { return ; } return value; @@ -135,7 +136,7 @@ const TeamList = ({ header: 'Members', disableGrow: true, cell: ({ cell: { value } }: Cell<'memberCount'>) => { - if (!hasFetched) { + if (isLoading) { return ; } return value; @@ -147,8 +148,8 @@ const TeamList = ({ { id: 'role', header: 'Role', - cell: ({ cell: { value }, row: { original } }: Cell<'memberCount'>) => { - if (!hasFetched) { + cell: ({ row: { original } }: Cell<'memberCount'>) => { + if (isLoading) { return ; } const canSeeTeamRoles = contextSrv.hasPermissionInMetadata( @@ -160,7 +161,7 @@ const TeamList = ({ @@ -174,7 +175,7 @@ const TeamList = ({ id: 'isProvisioned', header: '', cell: ({ cell: { value } }: Cell<'isProvisioned'>) => { - if (!hasFetched) { + if (isLoading) { return ; } return !!value && ; @@ -185,7 +186,7 @@ const TeamList = ({ header: '', disableGrow: true, cell: ({ row: { original } }: Cell) => { - if (!hasFetched) { + if (isLoading) { return ( @@ -216,14 +217,14 @@ const TeamList = ({ })} size="sm" disabled={!canDelete} - onConfirm={() => deleteTeam(original.uid)} + onConfirm={() => deleteTeam({ uid: original.uid })} /> ); }, }, ], - [displayRolePicker, hasFetched, rolesLoading, roleOptions, deleteTeam, styles] + [displayRolePicker, isLoading, styles.blockSkeleton, roleOptions, deleteTeam] ); return ( @@ -238,7 +239,7 @@ const TeamList = ({ } > - {noTeams ? ( + {!isLoading && !query && teams.length === 0 ? (
- {hasFetched && teams.length === 0 ? ( + {!isLoading && teams.length === 0 && ( - ) : ( + )} + {isLoading && } + {!isLoading && teams.length > 0 && ( String(team.id)} - fetchData={changeSort} + fetchData={({ sortBy }) => { + const sortingRule = sortBy.at(0); + if (sortingRule) { + return changeSort(sortingRule); + } + }} /> ; -export default connector(TeamList); +export default TeamList; const getStyles = () => ({ blockSkeleton: css({ diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 89a5cb5e032..3391a08c17d 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -1,7 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { memo, useRef } from 'react'; import { useParams } from 'react-router-dom-v5-compat'; -import { useAsync } from 'react-use'; import { featureEnabled } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; @@ -10,14 +9,13 @@ import config from 'app/core/config'; import { getNavModel } from 'app/core/selectors/navModel'; import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types/accessControl'; -import { StoreState, useDispatch, useSelector } from 'app/types/store'; +import { StoreState, useSelector } from 'app/types/store'; import TeamGroupSync, { TeamSyncUpgradeContent } from './TeamGroupSync'; import TeamPermissions from './TeamPermissions'; import TeamSettings from './TeamSettings'; -import { loadTeam } from './state/actions'; +import { useGetTeam } from './hooks'; import { getTeamLoadingNav } from './state/navModel'; -import { getTeam } from './state/selectors'; type TeamPageRouteParams = { uid: string; @@ -32,11 +30,6 @@ enum PageTypes { const PAGES = ['members', 'settings', 'groupsync']; -const teamSelector = createSelector( - [(state: StoreState) => state.team, (_: StoreState, teamUid: string) => teamUid], - (team, teamUid) => getTeam(team, teamUid) -); - const pageNavSelector = createSelector( [ (state: StoreState) => state.navIndex, @@ -52,7 +45,8 @@ const pageNavSelector = createSelector( const TeamPages = memo(() => { const isSyncEnabled = useRef(featureEnabled('teamsync')); const { uid: teamUid = '', page } = useParams(); - const team = useSelector((state) => teamSelector(state, teamUid)); + + const { data: team, isLoading } = useGetTeam({ uid: teamUid }); let defaultPage = 'members'; // With RBAC the settings page will always be available @@ -62,9 +56,6 @@ const TeamPages = memo(() => { const pageName = page ?? defaultPage; const pageNav = useSelector((state) => pageNavSelector(state, pageName, teamUid)); - const dispatch = useDispatch(); - const { loading: isLoading } = useAsync(async () => dispatch(loadTeam(teamUid)), [teamUid]); - const renderPage = () => { const currentPage = PAGES.includes(pageName) ? pageName : PAGES[0]; @@ -89,7 +80,7 @@ const TeamPages = memo(() => { case PageTypes.GroupSync: if (isSyncEnabled.current) { if (canReadTeamPermissions) { - return ; + return ; } } else if (config.featureToggles.featureHighlights) { return ( diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx index 9d31644657a..f5cd0a6fcf3 100644 --- a/public/app/features/teams/TeamSettings.test.tsx +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -1,12 +1,13 @@ -import { render, screen, waitFor } from 'test/test-utils'; +import { render, screen } from 'test/test-utils'; import { setBackendSrv } from '@grafana/runtime'; import { setupMockServer } from '@grafana/test-utils/server'; import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; +import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; -import { Props, TeamSettings } from './TeamSettings'; +import TeamSettings from './TeamSettings'; jest.spyOn(contextSrv, 'hasPermission').mockImplementation(() => true); jest.spyOn(contextSrv, 'hasPermissionInMetadata').mockImplementation(() => true); @@ -14,9 +15,9 @@ jest.spyOn(contextSrv, 'hasPermissionInMetadata').mockImplementation(() => true) setBackendSrv(backendSrv); setupMockServer(); -const setup = (propOverrides?: object) => { +const setup = () => { const team = MOCK_TEAMS[0]; - const props: Props = { + const props = { team: { id: Number(team.metadata.labels['grafana.app/deprecatedInternalID']), uid: team.metadata.name, @@ -25,12 +26,14 @@ const setup = (propOverrides?: object) => { orgId: 1, isProvisioned: false, }, - updateTeam: jest.fn(), }; - Object.assign(props, propOverrides); - - return render(); + return render( + <> + + + + ); }; describe('Team settings', () => { @@ -41,8 +44,7 @@ describe('Team settings', () => { }); it('should validate required fields', async () => { - const mockUpdate = jest.fn(); - const { user } = setup({ updateTeam: mockUpdate }); + const { user } = setup(); await screen.findByText('Team details'); await user.clear(screen.getByRole('textbox', { name: /Name/ })); @@ -50,12 +52,10 @@ describe('Team settings', () => { await user.click(screen.getByRole('button', { name: 'Save team details' })); expect(await screen.findByText('Name is required')).toBeInTheDocument(); - await waitFor(() => expect(mockUpdate).not.toHaveBeenCalled()); }); it('should submit form with correct values', async () => { - const mockUpdate = jest.fn(); - const { user } = setup({ updateTeam: mockUpdate }); + const { user } = setup(); await user.clear(screen.getByRole('textbox', { name: /Name/ })); await user.clear(screen.getByLabelText(/Email/i)); @@ -63,6 +63,6 @@ describe('Team settings', () => { await user.type(screen.getByLabelText(/Email/i), 'team@test.com'); await user.click(screen.getByRole('button', { name: 'Save team details' })); - await waitFor(() => expect(mockUpdate).toHaveBeenCalledWith('New team', 'team@test.com')); + expect(await screen.findByText('Team updated')).toBeInTheDocument(); }); }); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index a4779df63cc..7a847d9ebf3 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,5 +1,4 @@ import { useForm } from 'react-hook-form'; -import { ConnectedProps, connect } from 'react-redux'; import { Trans, t } from '@grafana/i18n'; import { Button, Field, FieldSet, Input, Stack } from '@grafana/ui'; @@ -10,22 +9,16 @@ import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types/accessControl'; import { Team } from 'app/types/teams'; -import { updateTeam } from './state/actions'; +import { useUpdateTeam } from './hooks'; -const mapDispatchToProps = { - updateTeam, -}; - -const connector = connect(null, mapDispatchToProps); - -interface OwnProps { +interface Props { team: Team; } -export type Props = ConnectedProps & OwnProps; -export const TeamSettings = ({ team, updateTeam }: Props) => { +const TeamSettings = ({ team }: Props) => { const canWriteTeamSettings = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsWrite, team); const currentOrgId = contextSrv.user.orgId; + const [updateTeam] = useUpdateTeam(); const [{ roleOptions }] = useRoleOptions(currentOrgId); const { @@ -43,7 +36,13 @@ export const TeamSettings = ({ team, updateTeam }: Props) => { contextSrv.hasPermission(AccessControlAction.ActionRolesList); const onSubmit = async (formTeam: Team) => { - updateTeam(formTeam.name, formTeam.email || ''); + return updateTeam({ + uid: team.uid, + team: { + name: formTeam.name, + email: formTeam.email || '', + }, + }); }; return ( @@ -103,4 +102,4 @@ export const TeamSettings = ({ team, updateTeam }: Props) => { ); }; -export default connector(TeamSettings); +export default TeamSettings; diff --git a/public/app/features/teams/hooks.ts b/public/app/features/teams/hooks.ts new file mode 100644 index 00000000000..2c563ef3d2f --- /dev/null +++ b/public/app/features/teams/hooks.ts @@ -0,0 +1,154 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { useEffect, useMemo } from 'react'; + +import { + useSearchTeamsQuery as useLegacySearchTeamsQuery, + useCreateTeamMutation, + useDeleteTeamByIdMutation, + useListTeamsRolesQuery, + CreateTeamCommand, + useSetTeamRolesMutation, + useGetTeamByIdQuery, + useUpdateTeamMutation, + UpdateTeamCommand, +} from 'app/api/clients/legacy'; +import { updateNavIndex } from 'app/core/actions'; +import { addFilteredDisplayName } from 'app/core/components/RolePicker/utils'; +import { contextSrv } from 'app/core/services/context_srv'; +import { AccessControlAction, Role } from 'app/types/accessControl'; +import { useDispatch } from 'app/types/store'; + +import { buildNavModel } from './state/navModel'; + +const rolesEnabled = + contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList); + +const canUpdateRoles = () => + contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) && + contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove); + +/** + * Get list of teams and their associated roles (if roles are enabled) + */ +export const useGetTeams = ({ + query, + pageSize, + page, + sort, +}: { + query?: string; + pageSize?: number; + page?: number; + sort?: string; +}) => { + const legacyResponse = useLegacySearchTeamsQuery({ perpage: pageSize, accesscontrol: true, page, sort, query }); + + const teamIds = useMemo(() => { + const teams = legacyResponse.data?.teams || []; + const ids = teams.map((team) => team.id); + return ids.filter((id): id is number => id !== undefined); + }, [legacyResponse.data?.teams]); + + const teamsRolesResponse = useListTeamsRolesQuery( + rolesEnabled && teamIds.length ? { rolesSearchQuery: { teamIds } } : skipToken + ); + + const teamsWithRoles = useMemo(() => { + if (!rolesEnabled || (rolesEnabled && teamsRolesResponse.isLoading)) { + return legacyResponse.data?.teams || []; + } + return (legacyResponse.data?.teams || []).map((team) => { + const roles = team.id ? teamsRolesResponse.data?.[team.id] || [] : []; + const mappedRoles = roles.map((role) => addFilteredDisplayName(role)); + return { + ...team, + roles: mappedRoles, + }; + }); + }, [legacyResponse, teamsRolesResponse]); + + return { + ...legacyResponse, + isLoading: legacyResponse.isLoading || (rolesEnabled ? teamsRolesResponse.isLoading : false), + data: { + teams: teamsWithRoles, + totalCount: legacyResponse.data?.totalCount, + }, + }; +}; + +/** + * Get a single team by UID + */ +export const useGetTeam = ({ uid }: { uid: string }) => { + const response = useGetTeamByIdQuery({ teamId: uid, accesscontrol: true }); + const dispatch = useDispatch(); + + // TODO: Eventually remove and handle nav index logic elsewhere + useEffect(() => { + if (response.data) { + dispatch(updateNavIndex(buildNavModel(response.data))); + } + }, [response.data, dispatch]); + + return response; +}; + +/** + * Update a team by UID + */ +export const useUpdateTeam = () => { + const [updateTeam, response] = useUpdateTeamMutation(); + + const trigger = async ({ uid, team }: { uid: string; team: UpdateTeamCommand }) => { + const mutationResult = await updateTeam({ + teamId: uid, + updateTeamCommand: team, + }); + + return mutationResult; + }; + + return [trigger, response] as const; +}; + +/** + * Delete a team by UID + */ +export const useDeleteTeam = () => { + const [deleteTeam, response] = useDeleteTeamByIdMutation(); + + return [({ uid }: { uid: string }) => deleteTeam({ teamId: uid }), response] as const; +}; + +/** + * Create a new team, and link any pending roles + */ +export const useCreateTeam = () => { + const [createTeam, response] = useCreateTeamMutation(); + const [setTeamRoles] = useSetTeamRolesMutation(); + + const trigger = async (team: CreateTeamCommand, pendingRoles?: Role[]) => { + const mutationResult = await createTeam({ + createTeamCommand: team, + }); + + const { data } = mutationResult; + + if (data && data.teamId && pendingRoles && pendingRoles.length) { + await contextSrv.fetchUserPermissions(); + if (contextSrv.licensedAccessControlEnabled() && canUpdateRoles()) { + await setTeamRoles({ + teamId: data.teamId, + setTeamRolesCommand: { + roleUids: pendingRoles.map((role) => role.uid), + }, + }); + } + } + + return mutationResult; + }; + + return [trigger, response] as const; +}; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 49c22c2c449..d0d05e0946c 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,134 +1,26 @@ -import { debounce } from 'lodash'; - import { getBackendSrv } from '@grafana/runtime'; -import { FetchDataArgs } from '@grafana/ui'; -import { updateNavIndex } from 'app/core/actions'; -import { contextSrv } from 'app/core/services/context_srv'; -import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { AccessControlAction } from 'app/types/accessControl'; import { ThunkResult } from 'app/types/store'; -import { Team, TeamWithRoles } from 'app/types/teams'; -import { buildNavModel } from './navModel'; -import { - teamGroupsLoaded, - queryChanged, - pageChanged, - teamLoaded, - teamsLoaded, - sortChanged, - rolesFetchBegin, - rolesFetchEnd, -} from './reducers'; +import { teamGroupsLoaded } from './reducers'; -export function loadTeams(initial = false): ThunkResult { - return async (dispatch, getState) => { - const { query, page, perPage, sort } = getState().teams; - // Early return if the user cannot list teams - if (!contextSrv.hasPermission(AccessControlAction.ActionTeamsRead)) { - dispatch(teamsLoaded({ teams: [], totalCount: 0, page: 1, perPage, noTeams: true })); - return; - } - - const response = await getBackendSrv().get( - '/api/teams/search', - accessControlQueryParam({ query, page, perpage: perPage, sort }) - ); - - // We only want to check if there is no teams on the initial request. - // A query that returns no teams should not render the empty list banner. - let noTeams = false; - if (initial) { - noTeams = response.teams.length === 0; - } - - if ( - contextSrv.licensedAccessControlEnabled() && - contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList) - ) { - dispatch(rolesFetchBegin()); - const teamIds = response?.teams.map((t: TeamWithRoles) => t.id); - const roles = await getBackendSrv().post(`/api/access-control/teams/roles/search`, { teamIds }); - response.teams.forEach((t: TeamWithRoles) => { - t.roles = roles ? roles[t.id] || [] : []; - }); - dispatch(rolesFetchEnd()); - } - - dispatch(teamsLoaded({ noTeams, ...response })); - }; -} - -const loadTeamsWithDebounce = debounce((dispatch) => dispatch(loadTeams()), 500); - -export function loadTeam(uid: string): ThunkResult> { +export function loadTeamGroups(teamUid: string): ThunkResult { return async (dispatch) => { - const response = await getBackendSrv().get(`/api/teams/${uid}`, accessControlQueryParam()); - dispatch(teamLoaded(response)); - dispatch(updateNavIndex(buildNavModel(response))); - }; -} - -export function deleteTeam(uid: string): ThunkResult { - return async (dispatch) => { - await getBackendSrv().delete(`/api/teams/${uid}`); - // Update users permissions in case they lost teams.read with the deletion - await contextSrv.fetchUserPermissions(); - dispatch(loadTeams()); - }; -} - -export function changeQuery(query: string): ThunkResult { - return async (dispatch) => { - dispatch(queryChanged(query)); - loadTeamsWithDebounce(dispatch); - }; -} - -export function changePage(page: number): ThunkResult { - return async (dispatch) => { - dispatch(pageChanged(page)); - dispatch(loadTeams()); - }; -} - -export function changeSort({ sortBy }: FetchDataArgs): ThunkResult { - const sort = sortBy.length ? `${sortBy[0].id}-${sortBy[0].desc ? 'desc' : 'asc'}` : undefined; - return async (dispatch) => { - dispatch(sortChanged(sort)); - dispatch(loadTeams()); - }; -} - -export function updateTeam(name: string, email: string): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; - await getBackendSrv().put(`/api/teams/${team.uid}`, { name, email }); - dispatch(loadTeam(team.uid)); - }; -} - -export function loadTeamGroups(): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; - const response = await getBackendSrv().get(`/api/teams/${team.uid}/groups`); + const response = await getBackendSrv().get(`/api/teams/${teamUid}/groups`); dispatch(teamGroupsLoaded(response)); }; } -export function addTeamGroup(groupId: string): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; - await getBackendSrv().post(`/api/teams/${team.uid}/groups`, { groupId: groupId }); - dispatch(loadTeamGroups()); +export function addTeamGroup(teamUid: string, groupId: string): ThunkResult { + return async (dispatch) => { + await getBackendSrv().post(`/api/teams/${teamUid}/groups`, { groupId: groupId }); + dispatch(loadTeamGroups(teamUid)); }; } -export function removeTeamGroup(groupId: string): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; +export function removeTeamGroup(teamUid: string, groupId: string): ThunkResult { + return async (dispatch) => { // need to use query parameter due to escaped characters in the request - await getBackendSrv().delete(`/api/teams/${team.uid}/groups?groupId=${encodeURIComponent(groupId)}`); - dispatch(loadTeamGroups()); + await getBackendSrv().delete(`/api/teams/${teamUid}/groups?groupId=${encodeURIComponent(groupId)}`); + dispatch(loadTeamGroups(teamUid)); }; } diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts deleted file mode 100644 index 4ea5c963739..00000000000 --- a/public/app/features/teams/state/reducers.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { TeamsState, TeamState } from 'app/types/teams'; - -import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { getMockTeam, getMockTeamGroups } from '../mocks/teamMocks'; - -import { - initialTeamsState, - initialTeamState, - teamGroupsLoaded, - teamLoaded, - queryChanged, - teamReducer, - teamsLoaded, - teamsReducer, -} from './reducers'; - -describe('teams reducer', () => { - describe('when teamsLoaded is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamsReducer, { ...initialTeamsState }) - .whenActionIsDispatched( - teamsLoaded({ teams: [getMockTeam()], page: 1, perPage: 30, noTeams: false, totalCount: 100 }) - ) - .thenStateShouldEqual({ - ...initialTeamsState, - hasFetched: true, - teams: [getMockTeam()], - noTeams: false, - totalPages: 4, - perPage: 30, - page: 1, - }); - }); - }); - - describe('when setSearchQueryAction is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamsReducer, { ...initialTeamsState }) - .whenActionIsDispatched(queryChanged('test')) - .thenStateShouldEqual({ - ...initialTeamsState, - query: 'test', - }); - }); - }); -}); - -describe('team reducer', () => { - describe('when loadTeamsAction is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamReducer, { ...initialTeamState }) - .whenActionIsDispatched(teamLoaded(getMockTeam())) - .thenStateShouldEqual({ - ...initialTeamState, - team: getMockTeam(), - }); - }); - }); - - describe('when loadTeamGroupsAction is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamReducer, { ...initialTeamState }) - .whenActionIsDispatched(teamGroupsLoaded(getMockTeamGroups(1))) - .thenStateShouldEqual({ - ...initialTeamState, - groups: getMockTeamGroups(1), - }); - }); - }); -}); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 2ef9dde3838..50011316a72 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,60 +1,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { TeamsState, Team, TeamState, TeamGroup } from 'app/types/teams'; - -export const initialTeamsState: TeamsState = { - teams: [], - page: 1, - query: '', - perPage: 30, - totalPages: 0, - noTeams: false, - hasFetched: false, -}; - -type TeamsFetched = { - teams: Team[]; - page: number; - perPage: number; - noTeams: boolean; - totalCount: number; -}; - -const teamsSlice = createSlice({ - name: 'teams', - initialState: initialTeamsState, - reducers: { - teamsLoaded: (state, action: PayloadAction): TeamsState => { - const { totalCount, perPage, ...rest } = action.payload; - const totalPages = Math.ceil(totalCount / perPage); - return { ...state, ...rest, totalPages, perPage, hasFetched: true }; - }, - queryChanged: (state, action: PayloadAction): TeamsState => { - return { ...state, page: 1, query: action.payload }; - }, - pageChanged: (state, action: PayloadAction): TeamsState => { - return { ...state, page: action.payload }; - }, - sortChanged: (state, action: PayloadAction): TeamsState => { - return { ...state, sort: action.payload, page: 1 }; - }, - rolesFetchBegin: (state) => { - return { ...state, rolesLoading: true }; - }, - rolesFetchEnd: (state) => { - return { ...state, rolesLoading: false }; - }, - }, -}); - -export const { teamsLoaded, queryChanged, pageChanged, sortChanged, rolesFetchBegin, rolesFetchEnd } = - teamsSlice.actions; - -export const teamsReducer = teamsSlice.reducer; +import { TeamState, TeamGroup } from 'app/types/teams'; export const initialTeamState: TeamState = { - team: {} as Team, - members: [], groups: [], }; @@ -62,20 +10,16 @@ const teamSlice = createSlice({ name: 'team', initialState: initialTeamState, reducers: { - teamLoaded: (state, action: PayloadAction): TeamState => { - return { ...state, team: action.payload }; - }, teamGroupsLoaded: (state, action: PayloadAction): TeamState => { return { ...state, groups: action.payload }; }, }, }); -export const { teamLoaded, teamGroupsLoaded } = teamSlice.actions; +export const { teamGroupsLoaded } = teamSlice.actions; export const teamReducer = teamSlice.reducer; export default { - teams: teamsReducer, team: teamReducer, }; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts deleted file mode 100644 index 983c5d35a71..00000000000 --- a/public/app/features/teams/state/selectors.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { TeamState } from 'app/types/teams'; - -import { getMockTeam } from '../mocks/teamMocks'; - -import { getTeam } from './selectors'; - -describe('Team selectors', () => { - describe('Get team', () => { - const mockTeam = getMockTeam(); - - it('should return team if matching with location team', () => { - const mockState: TeamState = { - team: mockTeam, - members: [], - groups: [], - }; - - const team = getTeam(mockState, 'aaaaaa'); - expect(team).toEqual(mockTeam); - }); - }); -}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index ec99297158c..4ada9ee8d46 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,11 +1,3 @@ -import { Team, TeamState } from 'app/types/teams'; +import { TeamState } from 'app/types/teams'; export const getTeamGroups = (state: TeamState) => state.groups; - -export const getTeam = (state: TeamState, currentTeamUid: string): Team | null => { - if (state.team.uid === currentTeamUid) { - return state.team; - } - - return null; -}; diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index d09459b9f9b..b2e2fc31a9b 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -1,3 +1,5 @@ +import { RoleDto } from 'app/api/clients/legacy'; + /** * UserPermission is a map storing permissions in a form of * { @@ -174,17 +176,6 @@ export enum AccessControlAction { MigrationAssistantMigrate = 'migrationassistant:migrate', } -export interface Role { - uid: string; - name: string; - displayName: string; +export interface Role extends RoleDto { filteredDisplayName: string; // name to be shown in filtered role list - description: string; - group: string; - global: boolean; - delegatable?: boolean; - mapped?: boolean; - version: number; - created: string; - updated: string; } diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts index f592b362894..72202a21295 100644 --- a/public/app/types/teams.ts +++ b/public/app/types/teams.ts @@ -1,4 +1,4 @@ -import { WithAccessControlMetadata } from '@grafana/data'; +import { TeamDto as TeamDtoLegacy } from 'app/api/clients/legacy'; import { Role } from './accessControl'; @@ -13,42 +13,7 @@ export interface TeamDTO { name: string; } -// This is the team resource with permissions and metadata expanded -export interface Team extends WithAccessControlMetadata { - /** - * Internal id of team - * @deprecated use uid instead - */ - id: number; - /** - * A unique identifier for the team. - */ - uid: string; // Prefer UUID - /** - * AvatarUrl is the team's avatar URL. - */ - avatarUrl?: string; - /** - * Email of the team. - */ - email?: string; - /** - * MemberCount is the number of the team members. - */ - memberCount: number; - /** - * Name of the team. - */ - name: string; - /** - * OrgId is the ID of an organisation the team belongs to. - */ - orgId: number; - /** - * isProvisioned is set if the team has been provisioned from IdP. - */ - isProvisioned: boolean; -} +export type Team = TeamDtoLegacy; export interface TeamWithRoles extends Team { /** @@ -73,20 +38,6 @@ export interface TeamGroup { teamId: number; } -export interface TeamsState { - teams: Team[]; - page: number; - query: string; - perPage: number; - noTeams: boolean; - totalPages: number; - hasFetched: boolean; - sort?: string; - rolesLoading?: boolean; -} - export interface TeamState { - team: Team; - members: TeamMember[]; groups: TeamGroup[]; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index f2d2366b9cd..6230117bcf2 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -13277,6 +13277,7 @@ "create-team": { "create": "Create", "description-email": "This is optional and is primarily used for allowing custom team avatars", + "failed-to-create": "Failed to create team", "label-email": "Email", "label-name": "Name", "label-role": "Role" @@ -13308,6 +13309,7 @@ "title-edit-team": "Edit team", "tooltip-edit-team": "Edit team" }, + "loading-teams": "Loading teams...", "new-team": "New Team", "placeholder-search-teams": "Search teams" }, diff --git a/public/openapi3.json b/public/openapi3.json index 20b97b3f14d..52322514490 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -12703,17 +12703,10 @@ }, "UpdateTeamCommand": { "properties": { - "Email": { + "email": { "type": "string" }, - "ExternalUID": { - "type": "string" - }, - "ID": { - "format": "int64", - "type": "integer" - }, - "Name": { + "name": { "type": "string" } }, @@ -24935,6 +24928,14 @@ "schema": { "type": "string" } + }, + { + "in": "query", + "name": "accesscontrol", + "schema": { + "default": false, + "type": "boolean" + } } ], "responses": { From f0e9c2e8a386b476c16f84f4686507d0389601d8 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 28 Nov 2025 12:53:38 +0000 Subject: [PATCH 190/378] FS: Fix HTML loader jumping a few pixels (#114581) --- pkg/services/frontend/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/services/frontend/index.html b/pkg/services/frontend/index.html index 0a589f81a49..b0364fcbac9 100644 --- a/pkg/services/frontend/index.html +++ b/pkg/services/frontend/index.html @@ -79,6 +79,7 @@ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + line-height: 1; /* prevent shift when css loads in that changes the body line-height */ } .fs-variant-loader, .fs-variant-error, .fs-custom-domain-error { From d3d294c99eee0755e6f24e3dd057834dbf6050c5 Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Fri, 28 Nov 2025 15:03:11 +0200 Subject: [PATCH 191/378] Dashboards: Per panel GroupBy action (#113445) * wip per panel group by * wip groupBy per panel * wip groupBy per panel * groupBy per panel action tests * fix * fix * fix * fix * CR mods * switch to dropdown * adjust apply * optimise action logic to avoid unnecessary triggers * canary scenes * wip (cherry picked from commit 51a00db93d0805f481a9e48213382468f1eb2986) * optimise action logic to avoid unnecessary triggers (cherry picked from commit c4de2dfff88c02c5aef61d1cbee070b5f6e5ccbc) * refactor * refactor * memoize values/ refactor * refactor * refactor components - do not make async call unless queries/groupByOptions change * canary scenes * fix test * Optimise handlers * Reset options if they are not applied * refactor subscriptions * refactor * scenes bump * fixes * properly deactivate header actions on panel edit * list * refactor showing menu using css, remove header deactivation code from panel-edit * cleanup * cleanup * cleanup + action redesign * i18n * pr mods * translations * fix * fix * fix design --------- Co-authored-by: Sergej-Vlasov Co-authored-by: Dominik Prokop --- .../src/types/featureToggles.gen.ts | 4 + .../src/selectors/components.ts | 3 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.json | 13 ++ .../scene/VizPanelHeaderActions.test.tsx | 154 +++++++++++++++++ .../scene/VizPanelHeaderActions.tsx | 136 +++++++++++++++ .../scene/VizPanelSubHeader.tsx | 5 +- .../PanelGroupByAction/PanelGroupByAction.tsx | 150 +++++++++++++++++ .../PanelGroupByActionPopover.tsx | 156 ++++++++++++++++++ .../serialization/layoutSerializers/utils.ts | 7 + .../transformSaveModelToScene.ts | 4 + .../features/dashboard-scene/utils/utils.ts | 4 + public/locales/en-US/grafana.json | 6 + 14 files changed, 648 insertions(+), 2 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/VizPanelHeaderActions.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/VizPanelHeaderActions.tsx create mode 100644 public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByAction.tsx create mode 100644 public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByActionPopover.tsx diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 35cccd78375..d853a64395a 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -377,6 +377,10 @@ export interface FeatureToggles { */ perPanelNonApplicableDrilldowns?: boolean; /** + * Enabled a group by action per panel + */ + panelGroupBy?: boolean; + /** * Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard */ panelFilterVariable?: boolean; diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 98fa6053d11..36d0088b258 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -443,6 +443,9 @@ export const versionedComponents = { PanelDataErrorMessage: { '10.4.0': 'data-testid Panel data error message', }, + PanelGroupByHeaderAction: { + '12.4.0': 'data-testid Panel group by header action', + }, }, Visualization: { Graph: { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 950476d1a8f..c112e0b77a0 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -607,6 +607,13 @@ var ( FrontendOnly: true, Owner: grafanaDashboardsSquad, }, + { + Name: "panelGroupBy", + Description: "Enabled a group by action per panel", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDashboardsSquad, + }, { Name: "panelFilterVariable", Description: "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index a7423d583ab..1382204ce0f 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -84,6 +84,7 @@ kubernetesDashboardsV2,experimental,@grafana/dashboards-squad,false,false,false dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true perPanelNonApplicableDrilldowns,experimental,@grafana/dashboards-squad,false,false,true +panelGroupBy,experimental,@grafana/dashboards-squad,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index ea80af0831e..8451faca5d0 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2512,6 +2512,19 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "panelGroupBy", + "resourceVersion": "1764257043719", + "creationTimestamp": "2025-11-27T15:24:03Z" + }, + "spec": { + "description": "Enabled a group by action per panel", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, { "metadata": { "name": "panelTimeSettings", diff --git a/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.test.tsx b/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.test.tsx new file mode 100644 index 00000000000..24b83e853fe --- /dev/null +++ b/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.test.tsx @@ -0,0 +1,154 @@ +import { of } from 'rxjs'; + +import { DataQueryRequest, DataSourceApi, LoadingState } from '@grafana/data'; +import { getPanelPlugin } from '@grafana/data/test'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { + GroupByVariable, + SceneDataTransformer, + SceneQueryRunner, + SceneVariableSet, + VizPanel, + VizPanelState, +} from '@grafana/scenes'; + +import { activateFullSceneTree } from '../utils/test-utils'; + +import { DashboardScene } from './DashboardScene'; +import { VizPanelHeaderActions } from './VizPanelHeaderActions'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; + +const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => { + return of({ + state: LoadingState.Loading, + series: [], + timeRange: request.range, + }); +}); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => { + return runRequestMock(ds, request); + }, + getDataSourceSrv: () => ({ + get: jest.fn().mockResolvedValue({ + getRef: () => ({ uid: 'ds-1', type: 'test' }), + }), + getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds-1', type: 'test' }), + }), + getPluginImportUtils: () => ({ + getPanelPluginFromCache: jest.fn(() => undefined), + }), +})); + +setPluginImportUtils({ + importPanelPlugin: () => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: () => undefined, +}); + +describe('VizPanelHeaderActions', () => { + it('renders PanelGroupByAction when the group by variable applies to the panel', async () => { + const { headerActions } = await buildScene(); + + expect(headerActions.state.supportsApplicability).toBe(true); + }); + + it('does not set when applicability is disabled', async () => { + const { headerActions } = await buildScene({ applicabilityEnabled: false }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); + + it('does not set when the datasource uid does not match', async () => { + const { headerActions } = await buildScene({ + variableDatasourceUid: 'other-ds', + }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); + + it('does not set if variable ds changes to a different type', async () => { + const { headerActions, groupByVariable } = await buildScene(); + + expect(headerActions.state.supportsApplicability).toBe(true); + + groupByVariable.setState({ datasource: { uid: 'ds-2' } }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); + + it('does not set if variable applicability becomes disabled', async () => { + const { headerActions, groupByVariable } = await buildScene(); + + expect(headerActions.state.supportsApplicability).toBe(true); + + groupByVariable.setState({ applicabilityEnabled: false }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); + + it('sdoes not set if queryRunner changes datasource to different one than vars', async () => { + const { headerActions, queryRunner } = await buildScene(); + + expect(headerActions.state.supportsApplicability).toBe(true); + + queryRunner.setState({ datasource: { uid: 'ds-2' } }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); +}); + +interface BuildSceneOptions { + applicabilityEnabled?: boolean; + variableDatasourceUid?: string; +} + +async function buildScene(options?: BuildSceneOptions) { + const headerActions = new VizPanelHeaderActions({}); + + const queryRunner = new SceneQueryRunner({ + datasource: { uid: 'ds-1' }, + queries: [{ refId: 'A', datasource: { uid: 'ds-1' } }], + }); + + const groupByVariable = new GroupByVariable({ + name: 'group', + label: 'group', + value: [], + text: [], + options: [], + applicabilityEnabled: options?.applicabilityEnabled ?? true, + datasource: { uid: options?.variableDatasourceUid ?? 'ds-1' }, + }); + + const dataProvider = new SceneDataTransformer({ + $data: queryRunner, + transformations: [], + }); + + const panelState: VizPanelState = { + key: 'panel-1', + title: 'Panel A', + pluginId: 'timeseries', + headerActions, + $data: dataProvider, + options: {}, + fieldConfig: { defaults: {}, overrides: [] }, + }; + + const panel = new VizPanel(panelState); + + const scene = new DashboardScene({ + $variables: new SceneVariableSet({ + variables: [groupByVariable], + }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), + }); + + activateFullSceneTree(scene); + + await new Promise((r) => setTimeout(r, 1)); + + return { headerActions, panel, groupByVariable, queryRunner }; +} diff --git a/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.tsx b/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.tsx new file mode 100644 index 00000000000..3130d746c94 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.tsx @@ -0,0 +1,136 @@ +import { Unsubscribable } from 'rxjs'; + +import { + GroupByVariable, + SceneComponentProps, + sceneGraph, + SceneObjectBase, + SceneObjectState, + SceneQueryRunner, + VizPanel, +} from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { verifyDrilldownApplicability } from '../utils/drilldownUtils'; + +import { PanelGroupByAction } from './panel-actions/PanelGroupByAction/PanelGroupByAction'; + +export interface VizPanelHeaderActionsState extends SceneObjectState { + hideGroupByAction?: boolean; + supportsApplicability?: boolean; +} + +export class VizPanelHeaderActions extends SceneObjectBase { + static Component = VizPanelHeaderActionsRenderer; + + private _groupByVar?: GroupByVariable; + private _groupBySub?: Unsubscribable; + private _queryRunnerDatasource?: DataSourceRef; + + constructor(state: Partial) { + super({ + hideGroupByAction: state.hideGroupByAction ?? false, + ...state, + }); + + this.addActivationHandler(this._onActivate); + } + + private _onActivate = () => { + if (!this.parent || !(this.parent instanceof VizPanel)) { + throw new Error('VizPanelHeaderActions must be a child of a VizPanel'); + } + + if (!this.state.hideGroupByAction) { + this.subscribeToGroupByChanges(); + } + + return () => { + this._groupBySub?.unsubscribe(); + }; + }; + + private setAplicabilitySupport(groupByDs?: DataSourceRef | null, groupByApplicability?: boolean) { + this.setState({ + supportsApplicability: verifyDrilldownApplicability( + this, + this._queryRunnerDatasource, + groupByDs ?? this._groupByVar?.state.datasource ?? null, + groupByApplicability ?? this._groupByVar?.state.applicabilityEnabled ?? false + ), + }); + } + + private subscribeToGroupByChanges() { + const vars = sceneGraph.getVariables(this); + const queryRunner = this.getQueryRunner(); + + this._groupByVar = vars.state.variables.find((variable) => variable instanceof GroupByVariable); + this._queryRunnerDatasource = queryRunner?.state.datasource; + + this.setAplicabilitySupport(); + + // check when var set updates and search for groupBy var + this._subs.add( + vars.subscribeToState((n) => { + this._groupByVar = n.variables.find((variable) => variable instanceof GroupByVariable); + + if (this._groupByVar) { + this._groupBySub?.unsubscribe(); + this._groupBySub = this._groupByVar?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { + this.setAplicabilitySupport(n.datasource, n.applicabilityEnabled); + } + }); + } + }) + ); + + // update query runner datasource changes + this._subs.add( + queryRunner?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource) { + this._queryRunnerDatasource = n.datasource; + + this.setAplicabilitySupport(); + } + }) + ); + + this._groupBySub = this._groupByVar?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { + this.setAplicabilitySupport(n.datasource, n.applicabilityEnabled); + } + }); + } + + public getQueryRunner() { + const panel = this.parent; + const dataObject = panel ? sceneGraph.getData(panel) : undefined; + const queryRunner = dataObject?.state.$data; + + if (!queryRunner || !(queryRunner instanceof SceneQueryRunner)) { + return null; + } + + return queryRunner; + } +} + +export function VizPanelHeaderActionsRenderer({ model }: SceneComponentProps) { + const { hideGroupByAction, supportsApplicability } = model.useState(); + const variables = sceneGraph.getVariables(model); + const groupByVariable = variables.state.variables.find((variable) => variable instanceof GroupByVariable); + const queryRunner = model.getQueryRunner(); + const queries = queryRunner?.state.data?.request?.targets ?? []; + + return ( + <> + {!hideGroupByAction && supportsApplicability && ( +
+ +
+ )} + + ); +} diff --git a/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx b/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx index f2247661e04..ff6f6c0712e 100644 --- a/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx +++ b/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx @@ -116,7 +116,7 @@ export class VizPanelSubHeader extends SceneObjectBase { private refreshDrilldownVarsSubscriptions() { if (this._groupByVar) { this._groupBySub?.unsubscribe(); - this._groupByVar?.subscribeToState((n, p) => { + this._groupBySub = this._groupByVar?.subscribeToState((n, p) => { if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { this.setDrilldownApplicabilitySupportHelper(undefined, { datasource: n.datasource, @@ -127,7 +127,8 @@ export class VizPanelSubHeader extends SceneObjectBase { } if (this._adHocVar) { - this._adHocVar?.subscribeToState((n, p) => { + this._adHocSub?.unsubscribe(); + this._adHocSub = this._adHocVar?.subscribeToState((n, p) => { if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { this.setDrilldownApplicabilitySupportHelper({ datasource: n.datasource, diff --git a/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByAction.tsx b/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByAction.tsx new file mode 100644 index 00000000000..b12cc52cbe3 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByAction.tsx @@ -0,0 +1,150 @@ +import { useState, useCallback, useEffect, useMemo, useRef } from 'react'; +import { lastValueFrom } from 'rxjs'; + +import { fuzzySearch } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { Trans } from '@grafana/i18n'; +import { GroupByVariable, SceneDataQuery, VariableValueOption, VariableValueSingle } from '@grafana/scenes'; +import { Button, Icon, Popover } from '@grafana/ui'; + +import { PanelGroupByActionPopover } from './PanelGroupByActionPopover'; + +interface Props { + groupByVariable: GroupByVariable; + queries: SceneDataQuery[]; +} + +export function PanelGroupByAction({ groupByVariable, queries }: Props) { + const { options: groupByOptions } = groupByVariable.useState(); + + const [options, setOptions] = useState([]); + const [selectedValues, setSelectedValues] = useState([]); + const [searchValue, setSearchValue] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [reloadOptions, setReloadOptions] = useState(false); + const [isPopoverVisible, setPopoverVisible] = useState(false); + + const ref = useRef(null); + + const fetchOptions = useCallback(async () => { + if (!groupByVariable || !reloadOptions) { + return; + } + + setIsLoading(true); + try { + if (groupByOptions.length === 0) { + await lastValueFrom(groupByVariable.validateAndUpdate()); + const options = await getApplicableGroupByOptions(groupByVariable, groupByVariable.state.options, queries); + + setSelectedValues(getGroupByValue(groupByVariable)); + setOptions(options); + return; + } + + const options = await getApplicableGroupByOptions(groupByVariable, groupByOptions, queries); + + setSelectedValues(getGroupByValue(groupByVariable)); + setOptions(options); + } catch (error) { + setSelectedValues([]); + setOptions([]); + } finally { + setIsLoading(false); + setReloadOptions(false); + } + }, [groupByOptions, groupByVariable, queries, reloadOptions]); + + useEffect(() => { + setReloadOptions(true); + }, [groupByOptions, queries]); + + useEffect(() => { + if (isPopoverVisible) { + fetchOptions(); + } + }, [fetchOptions, isPopoverVisible]); + + const filteredOptions = useMemo(() => { + if (!searchValue) { + return options; + } + + const haystack = options.map((option) => option.label); + const indices = fuzzySearch(haystack, searchValue); + return indices.map((idx) => options[idx]); + }, [options, searchValue]); + + const onCancel = () => { + setSearchValue(''); + setPopoverVisible(false); + }; + + const openPopover = () => { + // Reset checked state to match current variable value when opening + setSelectedValues(getGroupByValue(groupByVariable)); + setPopoverVisible(true); + }; + + return ( + + ); +} + +function getGroupByValue(groupByVariable: GroupByVariable) { + return Array.isArray(groupByVariable.state.value) + ? groupByVariable.state.value + : groupByVariable.state.value + ? [groupByVariable.state.value] + : []; +} + +async function getApplicableGroupByOptions( + groupByVariable: GroupByVariable, + options: VariableValueOption[], + queries: SceneDataQuery[] +) { + const values = options.map((option) => option.value); + const applicability = await groupByVariable.getGroupByApplicabilityForQueries(values, queries); + + return applicability + ? applicability.filter((item) => item.applicable).map((item) => ({ label: item.key, value: item.key })) + : options; +} diff --git a/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByActionPopover.tsx b/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByActionPopover.tsx new file mode 100644 index 00000000000..06191e1f7bf --- /dev/null +++ b/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByActionPopover.tsx @@ -0,0 +1,156 @@ +import { css, cx } from '@emotion/css'; +import { useCallback } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { t, Trans } from '@grafana/i18n'; +import { GroupByVariable, VariableValueOption, VariableValueSingle } from '@grafana/scenes'; +import { Button, Checkbox, ClickOutsideWrapper, Icon, Input, Stack, useStyles2 } from '@grafana/ui'; + +interface Props { + groupByVariable: GroupByVariable; + onCancel: () => void; + isLoading: boolean; + searchValue: string; + setSearchValue: (value: string) => void; + options: VariableValueOption[]; + values: VariableValueSingle[]; + onValuesChange: (value: VariableValueSingle[]) => void; +} + +export function PanelGroupByActionPopover({ + groupByVariable, + onCancel, + isLoading, + searchValue, + setSearchValue, + options, + values, + onValuesChange, +}: Props) { + const styles = useStyles2(getStyles); + + const onCheckedChanged = useCallback( + (option: VariableValueOption) => (event: React.FormEvent) => { + const newValues = event.currentTarget.checked + ? values.concat(option.value) + : values.filter((c) => c !== option.value); + + onValuesChange(newValues); + }, + [onValuesChange, values] + ); + + const isChecked = (option: VariableValueOption) => { + return values.includes(option.value); + }; + + const handleApply = useCallback(() => { + if (!values.length) { + return; + } + + groupByVariable.changeValueTo(values, values.map(String), true); + onCancel(); + }, [groupByVariable, onCancel, values]); + + const isAnyOptionChecked = () => { + if (!values.length) { + return false; + } + + return values.some((value) => options.find((option) => option.value === value)); + }; + + return ( + + {/* This is just blocking click events from bubbeling and should not have a keyboard interaction. */} + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */} +
ev.stopPropagation()}> + +
+ } + placeholder={t('panel-group-by.search-placeholder', 'Search')} + value={searchValue} + onChange={(e) => setSearchValue(e.currentTarget.value)} + /> +
+ +
+ {isLoading ? ( +
+ Loading options +
+ ) : options.length === 0 ? ( +
+ No options found +
+ ) : ( + options.map((option) => { + return ( +
+ +
+ ); + }) + )} +
+ + + + + +
+
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + menuContainer: css({ + display: 'flex', + flexDirection: 'column', + background: theme.colors.background.elevated, + border: `1px solid ${theme.colors.border.weak}`, + borderRadius: theme.shape.radius.default, + boxShadow: theme.shadows.z3, + padding: theme.spacing(2), + }), + searchContainer: css({ + width: '100%', + paddingBottom: theme.spacing(1), + borderBottom: `1px solid ${theme.colors.border.weak}`, + }), + listContainer: css({ + flex: 1, + overflow: 'auto', + minHeight: '100px', + maxHeight: '300px', + padding: theme.spacing(0.5), + borderBottom: `1px solid ${theme.colors.border.weak}`, + }), + option: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + padding: theme.spacing(1), + cursor: 'pointer', + borderRadius: theme.shape.radius.default, + '&:hover': { + background: theme.colors.background.secondary, + }, + '&:focus-visible': { + outline: `2px solid ${theme.colors.primary.border}`, + outlineOffset: '-2px', + }, + }), + emptyMessage: css({ + padding: theme.spacing(2), + textAlign: 'center', + color: theme.colors.text.secondary, + }), +}); diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts index 512631961d3..788dd53d11a 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts @@ -31,6 +31,7 @@ import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior'; import { PanelNotices } from '../../scene/PanelNotices'; +import { VizPanelHeaderActions } from '../../scene/VizPanelHeaderActions'; import { VizPanelSubHeader } from '../../scene/VizPanelSubHeader'; import { AutoGridItem } from '../../scene/layout-auto-grid/AutoGridItem'; import { DashboardGridItem } from '../../scene/layout-default/DashboardGridItem'; @@ -71,6 +72,9 @@ export function buildVizPanel(panel: PanelKind, id?: number): VizPanel { seriesLimit: config.panelSeriesLimit, $data: createPanelDataProvider(panel), titleItems, + headerActions: new VizPanelHeaderActions({ + hideGroupByAction: !config.featureToggles.panelGroupBy, + }), subHeader: new VizPanelSubHeader({ hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns, }), @@ -121,6 +125,9 @@ export function buildLibraryPanel(panel: LibraryPanelKind, id?: number): VizPane }), ], extendPanelContext: setDashboardPanelContext, + headerActions: new VizPanelHeaderActions({ + hideGroupByAction: !config.featureToggles.panelGroupBy, + }), pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID, title: panel.spec.title, options: {}, diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 9194adf6a7b..3c334b022c1 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -46,6 +46,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { PanelNotices } from '../scene/PanelNotices'; +import { VizPanelHeaderActions } from '../scene/VizPanelHeaderActions'; import { VizPanelSubHeader } from '../scene/VizPanelSubHeader'; import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; @@ -442,6 +443,9 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem { hoverHeaderOffset: 0, $data: createPanelDataProvider(panel), titleItems, + headerActions: new VizPanelHeaderActions({ + hideGroupByAction: !config.featureToggles.panelGroupBy, + }), subHeader: new VizPanelSubHeader({ hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns, }), diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 7188756b055..6c486021e62 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -24,6 +24,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel'; +import { VizPanelHeaderActions } from '../scene/VizPanelHeaderActions'; import { VizPanelSubHeader } from '../scene/VizPanelSubHeader'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; @@ -283,6 +284,9 @@ export function getDefaultVizPanel(): VizPanel { menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior], }), + headerActions: new VizPanelHeaderActions({ + hideGroupByAction: !config.featureToggles.panelGroupBy, + }), $data: new SceneDataTransformer({ $data: new SceneQueryRunner({ queries: [{ refId: 'A' }], diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6230117bcf2..d9efaadc4d6 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11094,6 +11094,12 @@ "could-anything-matching-query": "Could not find anything matching your query" } }, + "panel-group-by": { + "button": "Group by", + "loading": "Loading options", + "no-options": "No options found", + "search-placeholder": "Search" + }, "panel-type-filter": { "clear-button": "Clear types", "select-aria-label": "Panel type filter", From 8227ecb499784dd466e168be28fe87b8179719d3 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Fri, 28 Nov 2025 06:17:59 -0700 Subject: [PATCH 192/378] Dashboard Controls: Display dashboard links on the right side of the toolbar (#114378) * have dashboard links on the right side * Lets try a compromise (#114389) Try to compromise on white space. Having a lot of links will create unessecary empty space under the time controls, but it is necessary if we want to be able to have the links on the right --------- Co-authored-by: Oscar Kilhed Co-authored-by: Sergej-Vlasov --- .../scene/DashboardControls.tsx | 23 +++++++++++++++---- .../scene/DashboardLinksControls.tsx | 23 +++++++++++++++++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 66b87804f76..0679ab32f88 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -176,8 +176,13 @@ function DashboardControlsRenderer({ model }: SceneComponentProps
)} - {!hideDashboardControls && model.hasDashboardControls() && } + {!hideDashboardControls && model.hasDashboardControls() && ( +
+ +
+ )} {config.featureToggles.dashboardNewLayouts && } + {!hideLinksControls && !editPanel && }
{!hideVariableControls && ( <> @@ -185,7 +190,6 @@ function DashboardControlsRenderer({ model }: SceneComponentProps )} - {!hideLinksControls && !editPanel && } {editPanel && } {showDebugger && }
@@ -268,17 +272,26 @@ function getStyles(theme: GrafanaTheme2) { }), rightControls: css({ display: 'flex', - justifyContent: 'flex-end', gap: theme.spacing(1), - marginBottom: theme.spacing(1), float: 'right', - alignItems: 'flex-start', + alignItems: 'center', + flexWrap: 'wrap', + maxWidth: '100%', + minWidth: 0, }), timeControls: css({ display: 'flex', justifyContent: 'flex-end', gap: theme.spacing(1), marginBottom: theme.spacing(1), + order: 2, + marginLeft: 'auto', + flexShrink: 0, + alignSelf: 'flex-start', + }), + dashboardControlsButton: css({ + order: 2, + marginLeft: 'auto', }), rightControlsWrap: css({ flexWrap: 'wrap', diff --git a/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx b/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx index 8053055fe81..11639554e5d 100644 --- a/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx @@ -1,5 +1,9 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; import { sceneGraph } from '@grafana/scenes'; import { DashboardLink } from '@grafana/schema'; +import { useStyles2 } from '@grafana/ui'; import { DashboardLinkRenderer } from './DashboardLinkRenderer'; import { DashboardScene } from './DashboardScene'; @@ -12,18 +16,33 @@ export interface Props { export function DashboardLinksControls({ links, dashboard }: Props) { sceneGraph.getTimeRange(dashboard).useState(); const uid = dashboard.state.uid; + const styles = useStyles2(getStyles); if (!links || !uid) { return null; } return ( - <> +
{links .filter((link) => link.placement === undefined) .map((link: DashboardLink, index: number) => ( ))} - +
); } + +function getStyles(theme: GrafanaTheme2) { + return { + linksContainer: css({ + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(1), + maxWidth: '100%', + minWidth: 0, + order: 1, + flex: '1 1 0%', + }), + }; +} From c75137b2d3706adadb382cd5e4c58891601bf2e3 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:18:14 +0000 Subject: [PATCH 193/378] QueryVariableForm: Refil query variable query on default data source update (#114491) * refil qury variable query on default ds update * refactor datasource update logic * adjust test --- .../components/QueryVariableForm.test.tsx | 2 +- .../variables/components/QueryVariableForm.tsx | 16 +++++++++++----- .../editors/QueryVariableEditor.test.tsx | 10 ++++++---- .../variables/editors/QueryVariableEditor.tsx | 4 ++-- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx index 63a1912fde7..d3d23396974 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx @@ -178,7 +178,7 @@ describe('QueryVariableEditorForm', () => { await userEvent.click(screen.getByText(/prometheus/i)); expect(mockOnDataSourceChange).toHaveBeenCalledTimes(1); - expect(mockOnDataSourceChange).toHaveBeenCalledWith(promDatasource, undefined); + expect(mockOnDataSourceChange).toHaveBeenCalledWith(promDatasource); }); it('should call onQueryChange when changing the query', async () => { diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx index 3849fb0a2c5..030de016a5f 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx @@ -1,4 +1,4 @@ -import { FormEvent } from 'react'; +import { FormEvent, useCallback } from 'react'; import { useAsync } from 'react-use'; import { DataSourceInstanceSettings, SelectableValue, TimeRange } from '@grafana/data'; @@ -27,7 +27,7 @@ type VariableQueryType = QueryVariable['state']['query']; interface QueryVariableEditorFormProps { datasource?: DataSourceRef; - onDataSourceChange: (dsSettings: DataSourceInstanceSettings) => void; + onDataSourceChange: (dsSettings: DataSourceInstanceSettings, preserveQuery?: boolean) => void; query: VariableQueryType; onQueryChange: (query: VariableQueryType) => void; onLegacyQueryChange: (query: VariableQueryType, definition: string) => void; @@ -89,17 +89,23 @@ export function QueryVariableEditorForm({ onQueryChange(query); } + // update data source if it is not defined in variable model if (!datasourceRef) { const instanceSettings = getDataSourceSrv().getInstanceSettings({ type: datasource.type, uid: datasource.uid }); - if (instanceSettings) { - onDataSourceChange(instanceSettings); + onDataSourceChange(instanceSettings, true); } } return { datasource, VariableQueryEditor }; }, [datasourceRef]); + // adjusting type miss match between DataSourcePicker onChange and onDataSourceChange + const datasourceChangeHandler = useCallback( + (dsSettings: DataSourceInstanceSettings) => onDataSourceChange(dsSettings), + [onDataSourceChange] + ); + const { datasource, VariableQueryEditor } = dsConfig ?? {}; return ( @@ -111,7 +117,7 @@ export function QueryVariableEditorForm({ label={t('dashboard-scene.query-variable-editor-form.label-data-source', 'Data source')} htmlFor="data-source-picker" > - + {datasource && VariableQueryEditor && ( diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx index 26f9516e40a..99385438e87 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx @@ -152,10 +152,12 @@ describe('QueryVariableEditor', () => { const onRunQueryMock = jest.fn(); const variable = new QueryVariable({ datasource: undefined, query: '' }); - await setup({ - variable, - onRunQuery: onRunQueryMock, - }); + await act(() => + setup({ + variable, + onRunQuery: onRunQueryMock, + }) + ); await waitFor(async () => { expect(variable.state.datasource).not.toBe(undefined); diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx index da1649776b2..106468b2b31 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx @@ -68,10 +68,10 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito const onAllowCustomValueChange = (event: FormEvent) => { variable.setState({ allowCustomValue: event.currentTarget.checked }); }; - const onDataSourceChange = (dsInstanceSettings: DataSourceInstanceSettings) => { + const onDataSourceChange = (dsInstanceSettings: DataSourceInstanceSettings, preserveQuery = false) => { const datasource = getDataSourceRef(dsInstanceSettings); - if ((variable.state.datasource?.type || '') !== datasource.type) { + if (!preserveQuery && (variable.state.datasource?.type || '') !== datasource.type) { variable.setState({ datasource, query: '', definition: '' }); return; } From 930f7ce48991c2e5eb4b2f2e10d2146f446bfd2c Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Fri, 28 Nov 2025 14:21:33 +0100 Subject: [PATCH 194/378] Dashboards: Cover the Switch variable in schema transformations - part 2. (#114549) * feat: add v2alpha_1 conversion for the switch variable * chore: gofmt fixes * chore: update comments in tests * chore: fix gofmt * Update specs * tests: update the v2alpha1 openapi snapshot --------- Co-authored-by: Ivan Ortega --- .../kinds/v2alpha1/dashboard_spec.cue | 21 +- .../dashboard/v2alpha1/dashboard_spec.cue | 21 +- .../dashboard/v2alpha1/dashboard_spec_gen.go | 71 +++- .../v2alpha1/zz_generated.openapi.go | 349 ++++++++++++------ ...enerated.openapi_violation_exceptions.list | 17 +- .../input/v1beta1.variable-conversions.json | 26 ++ ...v1beta1.variable-conversions.v0alpha1.json | 26 ++ ...v1beta1.variable-conversions.v2alpha1.json | 13 + .../v1beta1.variable-conversions.v2beta1.json | 13 + .../conversion/v1beta1_to_v2alpha1.go | 73 ++++ .../conversion/v2alpha1_to_v2beta1.go | 16 + .../conversion/v2alpha1_to_v2beta1_test.go | 144 ++++++++ .../conversion/v2beta1_to_v2alpha1.go | 17 + .../conversion/v2beta1_to_v2alpha1_test.go | 45 +++ .../dashboard/v2alpha1/types.spec.gen.ts | 33 +- .../dashboard.grafana.app-v2alpha1.json | 72 +++- 16 files changed, 809 insertions(+), 148 deletions(-) create mode 100644 apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 5bca05229db..5768c358c24 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -710,9 +710,9 @@ VariableCustomFormatterFn: { // `custom`: Define the variable options manually using a comma-separated list. // `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | - "system" | "snapshot" + "system" | "snapshot" | "switch" -VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind +VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind // Sort variable options // Accepted values are: @@ -970,6 +970,23 @@ AdhocVariableKind: { spec: AdhocVariableSpec } +// Switch variable specification +SwitchVariableSpec: { + name: string | *"" + current: string | *"false" + enabledValue: string | *"true" + disabledValue: string | *"false" + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +SwitchVariableKind: { + kind: "SwitchVariable" + spec: SwitchVariableSpec +} + ConditionalRenderingGroupKind: { kind: "ConditionalRenderingGroup" spec: ConditionalRenderingGroupSpec diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index 76e0921eb88..9a8a621345f 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -714,9 +714,9 @@ VariableCustomFormatterFn: { // `custom`: Define the variable options manually using a comma-separated list. // `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | - "system" | "snapshot" + "system" | "snapshot" | "switch" -VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind +VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind // Sort variable options // Accepted values are: @@ -974,6 +974,23 @@ AdhocVariableKind: { spec: AdhocVariableSpec } +// Switch variable specification +SwitchVariableSpec: { + name: string | *"" + current: string | *"false" + enabledValue: string | *"true" + disabledValue: string | *"false" + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +SwitchVariableKind: { + kind: "SwitchVariable" + spec: SwitchVariableSpec +} + ConditionalRenderingGroupKind: { kind: "ConditionalRenderingGroup" spec: ConditionalRenderingGroupSpec diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index 37d9ae8897f..b83cbbd6fb3 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -1291,11 +1291,11 @@ func NewDashboardTimeRangeOption() *DashboardTimeRangeOption { } // +k8s:openapi-gen=true -type DashboardVariableKind = DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind +type DashboardVariableKind = DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind // NewDashboardVariableKind creates a new DashboardVariableKind object. func NewDashboardVariableKind() *DashboardVariableKind { - return NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind() + return NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind() } // Query variable kind @@ -1814,6 +1814,45 @@ func NewDashboardMetricFindValue() *DashboardMetricFindValue { return &DashboardMetricFindValue{} } +// +k8s:openapi-gen=true +type DashboardSwitchVariableKind struct { + Kind string `json:"kind"` + Spec DashboardSwitchVariableSpec `json:"spec"` +} + +// NewDashboardSwitchVariableKind creates a new DashboardSwitchVariableKind object. +func NewDashboardSwitchVariableKind() *DashboardSwitchVariableKind { + return &DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: *NewDashboardSwitchVariableSpec(), + } +} + +// Switch variable specification +// +k8s:openapi-gen=true +type DashboardSwitchVariableSpec struct { + Name string `json:"name"` + Current string `json:"current"` + EnabledValue string `json:"enabledValue"` + DisabledValue string `json:"disabledValue"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` +} + +// NewDashboardSwitchVariableSpec creates a new DashboardSwitchVariableSpec object. +func NewDashboardSwitchVariableSpec() *DashboardSwitchVariableSpec { + return &DashboardSwitchVariableSpec{ + Name: "", + Current: "false", + EnabledValue: "true", + DisabledValue: "false", + Hide: DashboardVariableHideDontHide, + SkipUrlSync: false, + } +} + // +k8s:openapi-gen=true type DashboardSpec struct { Annotations []DashboardAnnotationQueryKind `json:"annotations"` @@ -2404,7 +2443,7 @@ func (resource *DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTab } // +k8s:openapi-gen=true -type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind struct { +type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind struct { QueryVariableKind *DashboardQueryVariableKind `json:"QueryVariableKind,omitempty"` TextVariableKind *DashboardTextVariableKind `json:"TextVariableKind,omitempty"` ConstantVariableKind *DashboardConstantVariableKind `json:"ConstantVariableKind,omitempty"` @@ -2413,15 +2452,16 @@ type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasou CustomVariableKind *DashboardCustomVariableKind `json:"CustomVariableKind,omitempty"` GroupByVariableKind *DashboardGroupByVariableKind `json:"GroupByVariableKind,omitempty"` AdhocVariableKind *DashboardAdhocVariableKind `json:"AdhocVariableKind,omitempty"` + SwitchVariableKind *DashboardSwitchVariableKind `json:"SwitchVariableKind,omitempty"` } -// NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind creates a new DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind object. -func NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind() *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind { - return &DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind{} +// NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind creates a new DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind object. +func NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind() *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind { + return &DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind{} } -// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind` as JSON. -func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind) MarshalJSON() ([]byte, error) { +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind` as JSON. +func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind) MarshalJSON() ([]byte, error) { if resource.QueryVariableKind != nil { return json.Marshal(resource.QueryVariableKind) } @@ -2446,12 +2486,15 @@ func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKin if resource.AdhocVariableKind != nil { return json.Marshal(resource.AdhocVariableKind) } + if resource.SwitchVariableKind != nil { + return json.Marshal(resource.SwitchVariableKind) + } return []byte("null"), nil } -// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind` from JSON. -func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind) UnmarshalJSON(raw []byte) error { +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind` from JSON. +func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind) UnmarshalJSON(raw []byte) error { if raw == nil { return nil } @@ -2524,6 +2567,14 @@ func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKi resource.QueryVariableKind = &dashboardQueryVariableKind return nil + case "SwitchVariable": + var dashboardSwitchVariableKind DashboardSwitchVariableKind + if err := json.Unmarshal(raw, &dashboardSwitchVariableKind); err != nil { + return err + } + + resource.SwitchVariableKind = &dashboardSwitchVariableKind + return nil case "TextVariable": var dashboardTextVariableKind DashboardTextVariableKind if err := json.Unmarshal(raw, &dashboardTextVariableKind); err != nil { diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index a167aa6d36e..f595b8a3040 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -14,125 +14,127 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.AnnotationActions": schema_pkg_apis_dashboard_v2alpha1_AnnotationActions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.AnnotationPermission": schema_pkg_apis_dashboard_v2alpha1_AnnotationPermission(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.Dashboard": schema_pkg_apis_dashboard_v2alpha1_Dashboard(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAccess": schema_pkg_apis_dashboard_v2alpha1_DashboardAccess(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAction": schema_pkg_apis_dashboard_v2alpha1_DashboardAction(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardActionVariable": schema_pkg_apis_dashboard_v2alpha1_DashboardActionVariable(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2alpha1_DashboardAdHocFilterWithLabels(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationPanelFilter(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQueryKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutItemKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutItemSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardClient": schema_pkg_apis_dashboard_v2alpha1_DashboardClient(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConversionStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardConversionStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDashboardLink": schema_pkg_apis_dashboard_v2alpha1_DashboardDashboardLink(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataLink": schema_pkg_apis_dashboard_v2alpha1_DashboardDataLink(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardDataQueryKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataSourceRef": schema_pkg_apis_dashboard_v2alpha1_DashboardDataSourceRef(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataTransformerConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardDataTransformerConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardDatasourceVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardDatasourceVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDynamicConfigValue": schema_pkg_apis_dashboard_v2alpha1_DashboardDynamicConfigValue(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardElementReference": schema_pkg_apis_dashboard_v2alpha1_DashboardElementReference(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFetchOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardFetchOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldColor": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldColor(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfigSource": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfigSource(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardInfinityOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardInfinityOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardIntervalVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardIntervalVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardJSONCodec": schema_pkg_apis_dashboard_v2alpha1_DashboardJSONCodec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelKindSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelKindSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelRef": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelRef(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardList": schema_pkg_apis_dashboard_v2alpha1_DashboardList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMatcherConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardMatcherConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMetadata": schema_pkg_apis_dashboard_v2alpha1_DashboardMetadata(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMetricFindValue": schema_pkg_apis_dashboard_v2alpha1_DashboardMetricFindValue(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKindOrLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelKindOrLibraryPanelKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelQueryKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelQuerySpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryGroupKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryGroupSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryOptionsSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryOptionsSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRangeMap": schema_pkg_apis_dashboard_v2alpha1_DashboardRangeMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRegexMap": schema_pkg_apis_dashboard_v2alpha1_DashboardRegexMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardRowRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutRowKind": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutRowSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSpecialValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardSpecialValueMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrArrayOfString": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrArrayOfString(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrFloat64": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrFloat64(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardTabRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTextVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTextVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardThreshold": schema_pkg_apis_dashboard_v2alpha1_DashboardThreshold(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardThresholdsConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardThresholdsConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeRangeOption": schema_pkg_apis_dashboard_v2alpha1_DashboardTimeRangeOption(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeSettingsSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTimeSettingsSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTransformationKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTransformationKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1ActionStyle": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1ActionStyle(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1FieldConfigSourceOverrides": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1FieldConfigSourceOverrides(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1RangeMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1RangeMapOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1RegexMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1RegexMapOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1SpecialValueMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1SpecialValueMapOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMappingResult": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMappingResult(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVariableOption": schema_pkg_apis_dashboard_v2alpha1_DashboardVariableOption(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVersionInfo": schema_pkg_apis_dashboard_v2alpha1_DashboardVersionInfo(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVersionList": schema_pkg_apis_dashboard_v2alpha1_DashboardVersionList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVizConfigKind": schema_pkg_apis_dashboard_v2alpha1_DashboardVizConfigKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVizConfigSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardVizConfigSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v2alpha1_DashboardWithAccessInfo(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanel": schema_pkg_apis_dashboard_v2alpha1_LibraryPanel(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelList": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelSpec": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelStatus": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.VersionsQueryOptions": schema_pkg_apis_dashboard_v2alpha1_VersionsQueryOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.AnnotationActions": schema_pkg_apis_dashboard_v2alpha1_AnnotationActions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.AnnotationPermission": schema_pkg_apis_dashboard_v2alpha1_AnnotationPermission(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.Dashboard": schema_pkg_apis_dashboard_v2alpha1_Dashboard(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAccess": schema_pkg_apis_dashboard_v2alpha1_DashboardAccess(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAction": schema_pkg_apis_dashboard_v2alpha1_DashboardAction(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardActionVariable": schema_pkg_apis_dashboard_v2alpha1_DashboardActionVariable(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2alpha1_DashboardAdHocFilterWithLabels(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationPanelFilter(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutItemKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutItemSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardClient": schema_pkg_apis_dashboard_v2alpha1_DashboardClient(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConversionStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardConversionStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDashboardLink": schema_pkg_apis_dashboard_v2alpha1_DashboardDashboardLink(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataLink": schema_pkg_apis_dashboard_v2alpha1_DashboardDataLink(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardDataQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataSourceRef": schema_pkg_apis_dashboard_v2alpha1_DashboardDataSourceRef(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataTransformerConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardDataTransformerConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardDatasourceVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardDatasourceVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDynamicConfigValue": schema_pkg_apis_dashboard_v2alpha1_DashboardDynamicConfigValue(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardElementReference": schema_pkg_apis_dashboard_v2alpha1_DashboardElementReference(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFetchOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardFetchOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldColor": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldColor(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfigSource": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfigSource(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardInfinityOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardInfinityOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardIntervalVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardIntervalVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardJSONCodec": schema_pkg_apis_dashboard_v2alpha1_DashboardJSONCodec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelKindSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelKindSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelRef": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelRef(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardList": schema_pkg_apis_dashboard_v2alpha1_DashboardList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMatcherConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardMatcherConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMetadata": schema_pkg_apis_dashboard_v2alpha1_DashboardMetadata(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMetricFindValue": schema_pkg_apis_dashboard_v2alpha1_DashboardMetricFindValue(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKindOrLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelKindOrLibraryPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelQuerySpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryGroupKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryGroupSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryOptionsSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryOptionsSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRangeMap": schema_pkg_apis_dashboard_v2alpha1_DashboardRangeMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRegexMap": schema_pkg_apis_dashboard_v2alpha1_DashboardRegexMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardRowRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutRowKind": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutRowSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSpecialValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardSpecialValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrArrayOfString": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrArrayOfString(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrFloat64": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrFloat64(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardSwitchVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardSwitchVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardTabRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTextVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTextVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardThreshold": schema_pkg_apis_dashboard_v2alpha1_DashboardThreshold(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardThresholdsConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardThresholdsConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeRangeOption": schema_pkg_apis_dashboard_v2alpha1_DashboardTimeRangeOption(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeSettingsSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTimeSettingsSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTransformationKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTransformationKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1ActionStyle": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1ActionStyle(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1FieldConfigSourceOverrides": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1FieldConfigSourceOverrides(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1RangeMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1RangeMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1RegexMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1RegexMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1SpecialValueMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1SpecialValueMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMappingResult": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMappingResult(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVariableOption": schema_pkg_apis_dashboard_v2alpha1_DashboardVariableOption(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVersionInfo": schema_pkg_apis_dashboard_v2alpha1_DashboardVersionInfo(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVersionList": schema_pkg_apis_dashboard_v2alpha1_DashboardVersionList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVizConfigKind": schema_pkg_apis_dashboard_v2alpha1_DashboardVizConfigKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVizConfigSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardVizConfigSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v2alpha1_DashboardWithAccessInfo(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanel": schema_pkg_apis_dashboard_v2alpha1_LibraryPanel(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelList": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelSpec": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelStatus": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.VersionsQueryOptions": schema_pkg_apis_dashboard_v2alpha1_VersionsQueryOptions(ref), } } @@ -3457,7 +3459,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKind(ref common.Re } } -func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3503,11 +3505,16 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariable Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind"), }, }, + "SwitchVariableKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableKind"), + }, + }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableKind"}, } } @@ -4064,7 +4071,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref common.ReferenceCallba Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind"), }, }, }, @@ -4075,7 +4082,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref common.ReferenceCallba }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDashboardLink", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKindOrLibraryPanelKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeSettingsSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDashboardLink", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKindOrLibraryPanelKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeSettingsSpec"}, } } @@ -4184,6 +4191,102 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrFloat64(ref common.Refe } } +func schema_pkg_apis_dashboard_v2alpha1_DashboardSwitchVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha1_DashboardSwitchVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Switch variable specification", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "current": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "enabledValue": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "disabledValue": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "hide": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "skipUrlSync": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "current", "enabledValue", "disabledValue", "hide", "skipUrlSync"}, + }, + }, + } +} + func schema_pkg_apis_dashboard_v2alpha1_DashboardTabRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list index 25c9bf58f1c..31ac411ad28 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list @@ -55,14 +55,15 @@ API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/ap API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardIntervalVariableSpec,AutoMin API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardPanelKindOrLibraryPanelKind,LibraryPanelKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardPanelKindOrLibraryPanelKind,PanelKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,AdhocVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,ConstantVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,CustomVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,DatasourceVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,GroupByVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,IntervalVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,QueryVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,TextVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,AdhocVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,ConstantVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,CustomVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,DatasourceVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,GroupByVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,IntervalVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,QueryVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,SwitchVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,TextVariableKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardStringOrArrayOfString,ArrayOfString API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardStringOrArrayOfString,String API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardStringOrFloat64,Float64 diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json index a8d22b09eee..ae1ef7cd04e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json @@ -258,6 +258,32 @@ "multi": true, "skipUrlSync": false }, + { + "name": "switch_var", + "type": "switch", + "label": "Enable Feature", + "description": "Toggle feature on/off", + "query": "", + "current": { + "selected": false, + "text": "false", + "value": "false" + }, + "options": [ + { + "selected": false, + "text": "true", + "value": "true" + }, + { + "selected": true, + "text": "false", + "value": "false" + } + ], + "hide": 0, + "skipUrlSync": false + }, { "name": "legacy_string_var", "type": "query", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json index e4a5b7b74eb..4e12e6982ef 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json @@ -277,6 +277,32 @@ "skipUrlSync": false, "type": "groupby" }, + { + "current": { + "selected": false, + "text": "false", + "value": "false" + }, + "description": "Toggle feature on/off", + "hide": 0, + "label": "Enable Feature", + "name": "switch_var", + "options": [ + { + "selected": false, + "text": "true", + "value": "true" + }, + { + "selected": true, + "text": "false", + "value": "false" + } + ], + "query": "", + "skipUrlSync": false, + "type": "switch" + }, { "current": { "selected": false, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json index a4740089365..c7c6c646a94 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json @@ -317,6 +317,19 @@ "description": "Group results by field" } }, + { + "kind": "SwitchVariable", + "spec": { + "name": "switch_var", + "current": "false", + "enabledValue": "true", + "disabledValue": "false", + "label": "Enable Feature", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Toggle feature on/off" + } + }, { "kind": "QueryVariable", "spec": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json index d35e57fa601..7b1a899d7fa 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json @@ -318,6 +318,19 @@ "description": "Group results by field" } }, + { + "kind": "SwitchVariable", + "spec": { + "name": "switch_var", + "current": "false", + "enabledValue": "true", + "disabledValue": "false", + "label": "Enable Feature", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Toggle feature on/off" + } + }, { "kind": "QueryVariable", "spec": { diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index d20855a9509..7ef4d6ed475 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -956,6 +956,10 @@ func transformVariables(ctx context.Context, dashboard map[string]interface{}, d if textVar, err := buildTextVariable(varMap, commonProps); err == nil { variables = append(variables, textVar) } + case "switch": + if switchVar, err := buildSwitchVariable(varMap, commonProps); err == nil { + variables = append(variables, switchVar) + } case "groupby": if groupByVar, err := buildGroupByVariable(ctx, varMap, commonProps, dsIndexProvider); err == nil { variables = append(variables, groupByVar) @@ -1415,6 +1419,75 @@ func buildTextVariable(varMap map[string]interface{}, commonProps CommonVariable }, nil } +// Helper function to extract string value from an option map (value or text field) +func getOptionValue(optMap map[string]interface{}) string { + if val, ok := optMap["value"].(string); ok && val != "" { + return val + } + if val, ok := optMap["text"].(string); ok && val != "" { + return val + } + return "" +} + +// Switch Variable +func buildSwitchVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) { + current := "" + if currentVal, exists := varMap["current"]; exists { + if currentMap, ok := currentVal.(map[string]interface{}); ok { + current = getOptionValue(currentMap) + } + } + + // In V1 the enabled value is the first value of the options array, + // while the disabled value is second one. + // (Falling back to "true" and "false" if options are not available) + enabledValue := "true" + disabledValue := "false" + + if options, ok := varMap["options"].([]interface{}); ok { + // Get enabledValue from first option + if len(options) > 0 { + if opt1, ok := options[0].(map[string]interface{}); ok { + if val := getOptionValue(opt1); val != "" { + enabledValue = val + } + } + } + // Get disabledValue from second option + if len(options) > 1 { + if opt2, ok := options[1].(map[string]interface{}); ok { + if val := getOptionValue(opt2); val != "" { + disabledValue = val + } + } + } + } + + // Set current to disabledValue if not set + if current == "" { + current = disabledValue + } + + switchVar := &dashv2alpha1.DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: dashv2alpha1.DashboardSwitchVariableSpec{ + Name: commonProps.Name, + Current: current, + EnabledValue: enabledValue, + DisabledValue: disabledValue, + Label: commonProps.Label, + Description: commonProps.Description, + Hide: commonProps.Hide, + SkipUrlSync: commonProps.SkipUrlSync, + }, + } + + return dashv2alpha1.DashboardVariableKind{ + SwitchVariableKind: switchVar, + }, nil +} + // Adhoc Variable func buildAdhocVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardVariableKind, error) { datasource := varMap["datasource"] diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go index e5bd1f834d3..b09198c72e2 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go @@ -733,6 +733,22 @@ func convertVariable_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVariableKind, } } + if in.SwitchVariableKind != nil { + out.SwitchVariableKind = &dashv2beta1.DashboardSwitchVariableKind{ + Kind: in.SwitchVariableKind.Kind, + Spec: dashv2beta1.DashboardSwitchVariableSpec{ + Name: in.SwitchVariableKind.Spec.Name, + Current: in.SwitchVariableKind.Spec.Current, + EnabledValue: in.SwitchVariableKind.Spec.EnabledValue, + DisabledValue: in.SwitchVariableKind.Spec.DisabledValue, + Label: in.SwitchVariableKind.Spec.Label, + Hide: dashv2beta1.DashboardVariableHide(in.SwitchVariableKind.Spec.Hide), + SkipUrlSync: in.SwitchVariableKind.Spec.SkipUrlSync, + Description: in.SwitchVariableKind.Spec.Description, + }, + } + } + return nil } diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go new file mode 100644 index 00000000000..3451234cdec --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go @@ -0,0 +1,144 @@ +package conversion + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" + "github.com/grafana/grafana/apps/dashboard/pkg/migration" + migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" +) + +// TestV2alpha1ToV2beta1 tests the conversion logic for v2alpha1 to v2beta1. +func TestV2alpha1ToV2beta1(t *testing.T) { + // Initialize the migrator with test providers + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider) + + // Set up conversion scheme + scheme := runtime.NewScheme() + err := RegisterConversions(scheme, dsProvider, leProvider) + require.NoError(t, err) + + testCases := []struct { + name string + createV2alpha1 func() *dashv2alpha1.Dashboard + validateV2beta1 func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) + }{ + { + name: "dashboard with switch variable", + createV2alpha1: func() *dashv2alpha1.Dashboard { + label := "Enable Feature" + description := "Toggle feature" + return &dashv2alpha1.Dashboard{ + Spec: dashv2alpha1.DashboardSpec{ + Title: "Test Dashboard", + Variables: []dashv2alpha1.DashboardVariableKind{ + { + SwitchVariableKind: &dashv2alpha1.DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: dashv2alpha1.DashboardSwitchVariableSpec{ + Name: "switch_var", + Current: "false", + EnabledValue: "true", + DisabledValue: "false", + Label: &label, + Description: &description, + Hide: dashv2alpha1.DashboardVariableHideDontHide, + SkipUrlSync: false, + }, + }, + }, + }, + }, + } + }, + validateV2beta1: func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) { + require.Len(t, v2beta1.Spec.Variables, 1) + variable := v2beta1.Spec.Variables[0] + require.NotNil(t, variable.SwitchVariableKind, "SwitchVariableKind should not be nil") + assert.Equal(t, "SwitchVariable", variable.SwitchVariableKind.Kind) + assert.Equal(t, "switch_var", variable.SwitchVariableKind.Spec.Name) + assert.Equal(t, "false", variable.SwitchVariableKind.Spec.Current) + assert.Equal(t, "true", variable.SwitchVariableKind.Spec.EnabledValue) + assert.Equal(t, "false", variable.SwitchVariableKind.Spec.DisabledValue) + assert.NotNil(t, variable.SwitchVariableKind.Spec.Label) + assert.Equal(t, "Enable Feature", *variable.SwitchVariableKind.Spec.Label) + assert.NotNil(t, variable.SwitchVariableKind.Spec.Description) + assert.Equal(t, "Toggle feature", *variable.SwitchVariableKind.Spec.Description) + assert.Equal(t, dashv2beta1.DashboardVariableHideDontHide, variable.SwitchVariableKind.Spec.Hide) + assert.False(t, variable.SwitchVariableKind.Spec.SkipUrlSync) + }, + }, + { + name: "dashboard with switch variable - custom values for enabled and disable states", + createV2alpha1: func() *dashv2alpha1.Dashboard { + label := "Enable Feature" + description := "Toggle feature" + return &dashv2alpha1.Dashboard{ + Spec: dashv2alpha1.DashboardSpec{ + Title: "Test Dashboard", + Variables: []dashv2alpha1.DashboardVariableKind{ + { + SwitchVariableKind: &dashv2alpha1.DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: dashv2alpha1.DashboardSwitchVariableSpec{ + Name: "switch_var", + Current: "true", + EnabledValue: "enabled", + DisabledValue: "disabled", + Label: &label, + Description: &description, + Hide: dashv2alpha1.DashboardVariableHideHideLabel, + SkipUrlSync: true, + }, + }, + }, + }, + }, + } + }, + validateV2beta1: func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) { + require.Len(t, v2beta1.Spec.Variables, 1) + variable := v2beta1.Spec.Variables[0] + require.NotNil(t, variable.SwitchVariableKind) + assert.Equal(t, "switch_var", variable.SwitchVariableKind.Spec.Name) + assert.Equal(t, "true", variable.SwitchVariableKind.Spec.Current) + assert.Equal(t, "enabled", variable.SwitchVariableKind.Spec.EnabledValue) + assert.Equal(t, "disabled", variable.SwitchVariableKind.Spec.DisabledValue) + assert.Equal(t, dashv2beta1.DashboardVariableHideHideLabel, variable.SwitchVariableKind.Spec.Hide) + assert.True(t, variable.SwitchVariableKind.Spec.SkipUrlSync) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create v2alpha1 dashboard + v2alpha1 := tc.createV2alpha1() + + // Collect original statistics + originalStats := collectStatsV2alpha1(v2alpha1.Spec) + + // Convert to v2beta1 + var v2beta1 dashv2beta1.Dashboard + err := scheme.Convert(v2alpha1, &v2beta1, nil) + require.NoError(t, err, "Failed to convert v2alpha1 to v2beta1") + + // Collect v2beta1 statistics + v2beta1Stats := collectStatsV2beta1(v2beta1.Spec) + + // Verify no data loss + err = detectConversionDataLoss(originalStats, v2beta1Stats, "V2alpha1", "V2beta1") + assert.NoError(t, err, "Data loss detected in conversion") + + // Run custom validation + tc.validateV2beta1(t, &v2beta1) + }) + } +} diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go index 580fef1b3ea..f0f06957b63 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go @@ -771,6 +771,23 @@ func convertVariable_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardVariableKind, } } + if in.SwitchVariableKind != nil { + out.SwitchVariableKind = &dashv2alpha1.DashboardSwitchVariableKind{ + Kind: in.SwitchVariableKind.Kind, + Spec: dashv2alpha1.DashboardSwitchVariableSpec{ + Name: in.SwitchVariableKind.Spec.Name, + Current: in.SwitchVariableKind.Spec.Current, + EnabledValue: in.SwitchVariableKind.Spec.EnabledValue, + DisabledValue: in.SwitchVariableKind.Spec.DisabledValue, + Label: in.SwitchVariableKind.Spec.Label, + Hide: dashv2alpha1.DashboardVariableHide(in.SwitchVariableKind.Spec.Hide), + SkipUrlSync: in.SwitchVariableKind.Spec.SkipUrlSync, + Description: in.SwitchVariableKind.Spec.Description, + }, + } + return nil + } + return nil } diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go index 773961a31fb..25703b56aa7 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go @@ -479,6 +479,51 @@ func TestV2beta1ToV2alpha1(t *testing.T) { assert.Equal(t, "", variable.QueryVariableKind.Spec.Query.Kind, "Empty group should result in empty kind") }, }, + { + name: "dashboard with switch variable", + createV2beta1: func() *dashv2beta1.Dashboard { + label := "Enable Feature" + description := "Toggle feature" + return &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ + Title: "Test Dashboard", + Variables: []dashv2beta1.DashboardVariableKind{ + { + SwitchVariableKind: &dashv2beta1.DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: dashv2beta1.DashboardSwitchVariableSpec{ + Name: "switch_var", + Current: "false", + EnabledValue: "true", + DisabledValue: "false", + Label: &label, + Description: &description, + Hide: dashv2beta1.DashboardVariableHideDontHide, + SkipUrlSync: false, + }, + }, + }, + }, + }, + } + }, + validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { + require.Len(t, v2alpha1.Spec.Variables, 1) + variable := v2alpha1.Spec.Variables[0] + require.NotNil(t, variable.SwitchVariableKind, "SwitchVariableKind should not be nil") + assert.Equal(t, "SwitchVariable", variable.SwitchVariableKind.Kind) + assert.Equal(t, "switch_var", variable.SwitchVariableKind.Spec.Name) + assert.Equal(t, "false", variable.SwitchVariableKind.Spec.Current) + assert.Equal(t, "true", variable.SwitchVariableKind.Spec.EnabledValue) + assert.Equal(t, "false", variable.SwitchVariableKind.Spec.DisabledValue) + assert.NotNil(t, variable.SwitchVariableKind.Spec.Label) + assert.Equal(t, "Enable Feature", *variable.SwitchVariableKind.Spec.Label) + assert.NotNil(t, variable.SwitchVariableKind.Spec.Description) + assert.Equal(t, "Toggle feature", *variable.SwitchVariableKind.Spec.Description) + assert.Equal(t, dashv2alpha1.DashboardVariableHideDontHide, variable.SwitchVariableKind.Spec.Hide) + assert.False(t, variable.SwitchVariableKind.Spec.SkipUrlSync) + }, + }, { name: "dashboard with rows layout", createV2beta1: func() *dashv2beta1.Dashboard { diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 3cb2c8aab5b..060a0e3945f 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -1050,7 +1050,7 @@ export const defaultTimeRangeOption = (): TimeRangeOption => ({ to: "now", }); -export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind; +export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind; export const defaultVariableKind = (): VariableKind => (defaultQueryVariableKind()); @@ -1436,6 +1436,37 @@ export const defaultMetricFindValue = (): MetricFindValue => ({ text: "", }); +export interface SwitchVariableKind { + kind: "SwitchVariable"; + spec: SwitchVariableSpec; +} + +export const defaultSwitchVariableKind = (): SwitchVariableKind => ({ + kind: "SwitchVariable", + spec: defaultSwitchVariableSpec(), +}); + +// Switch variable specification +export interface SwitchVariableSpec { + name: string; + current: string; + enabledValue: string; + disabledValue: string; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultSwitchVariableSpec = (): SwitchVariableSpec => ({ + name: "", + current: "false", + enabledValue: "true", + disabledValue: "false", + hide: "dontHide", + skipUrlSync: false, +}); + export interface Spec { annotations: AnnotationQueryKind[]; // Configuration of dashboard cursor sync behavior. diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index 90e70f63baf..1d5fff1367b 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -3100,7 +3100,7 @@ } } }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind": { + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind": { "type": "object", "properties": { "AdhocVariableKind": { @@ -3124,6 +3124,9 @@ "QueryVariableKind": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKind" }, + "SwitchVariableKind": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableKind" + }, "TextVariableKind": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableKind" } @@ -3520,7 +3523,7 @@ "description": "Configured template variables.", "type": "array", "items": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind" } } } @@ -3587,6 +3590,71 @@ } } }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string", + "default": "" + }, + "spec": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableSpec" + } + ] + } + } + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableSpec": { + "description": "Switch variable specification", + "type": "object", + "required": [ + "name", + "current", + "enabledValue", + "disabledValue", + "hide", + "skipUrlSync" + ], + "properties": { + "current": { + "type": "string", + "default": "" + }, + "description": { + "type": "string" + }, + "disabledValue": { + "type": "string", + "default": "" + }, + "enabledValue": { + "type": "string", + "default": "" + }, + "hide": { + "type": "string", + "default": "" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + } + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabRepeatOptions": { "type": "object", "required": [ From ce72dc6224329ca2b95730ac71abc859d609f89d Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 28 Nov 2025 15:41:19 +0200 Subject: [PATCH 195/378] API clients: Remove duplicate config entries (#114569) --- packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts b/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts index 776ad4131ae..06e6f5739b8 100644 --- a/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts +++ b/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts @@ -108,8 +108,6 @@ const config: ConfigFile = { ...createAPIConfig('preferences', 'v1alpha1'), ...createAPIConfig('provisioning', 'v0alpha1'), ...createAPIConfig('shorturl', 'v1beta1'), - ...createAPIConfig('shorturl', 'v1beta1'), - ...createAPIConfig('shorturl', 'v1beta1'), // PLOP_INJECT_API_CLIENT - Used by the API client generator }, }; From b5eac7baadbbb5b9a17574d0d69805d9a22dfe00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 28 Nov 2025 15:07:15 +0100 Subject: [PATCH 196/378] Sidebar: Clickout side to close pane (#114523) * Sidebar: Clickout side to close pane * fixes * fixing tests * fix * ignore clicks in portals * fix e2e * fix test * Fix click away issues * add missing file --- e2e-playwright/dashboard-new-layouts/utils.ts | 1 + .../src/selectors/components.ts | 3 ++ .../src/components/Sidebar/Sidebar.story.tsx | 7 ++-- .../src/components/Sidebar/Sidebar.test.tsx | 3 +- .../src/components/Sidebar/Sidebar.tsx | 18 +++++++++-- .../components/Sidebar/SidebarPaneHeader.tsx | 16 +++++++--- .../src/components/Sidebar/useSidebar.tsx | 6 ++++ .../components/Sidebar/useSidebarClickAway.ts | 32 +++++++++++++++++++ .../DashboardEditPaneRenderer.test.tsx | 4 +-- .../edit-pane/DashboardEditPaneSplitter.tsx | 1 + .../edit-pane/DashboardOutline.test.tsx | 12 +++++-- .../edit-pane/DashboardOutline.tsx | 5 +-- .../edit-pane/EditPaneHeader.test.tsx | 19 +++++++++-- .../edit-pane/EditPaneHeader.tsx | 2 +- 14 files changed, 107 insertions(+), 22 deletions(-) create mode 100644 packages/grafana-ui/src/components/Sidebar/useSidebarClickAway.ts diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index 89f4de0eec3..83508063ef5 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -50,6 +50,7 @@ export const flows = { await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.dockToggle).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) .click(); diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 36d0088b258..c643a0d7f2c 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -61,6 +61,9 @@ export const versionedComponents = { closePane: { '12.4.0': 'data-testid Sidebar close pane', }, + dockToggle: { + '12.4.0': 'data-testid sidebar-dock-toggle', + }, }, EditPaneHeader: { deleteButton: { diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx index 8590f91a8b4..175e792d451 100644 --- a/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx @@ -62,6 +62,7 @@ export const Example: StoryFn = (args) => { position: args.position, bottomMargin: 0, edgeMargin: 0, + onClosePane: () => setOpenPane(''), }); return ( @@ -79,7 +80,7 @@ export const Example: StoryFn = (args) => { {openPane === 'settings' && ( - togglePane('')}> + @@ -88,12 +89,12 @@ export const Example: StoryFn = (args) => { )} {openPane === 'outline' && ( - togglePane('')} /> + )} {openPane === 'add' && ( - togglePane('')} /> + )} diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx index 187fbc90c4b..5e5116b3ada 100644 --- a/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx @@ -30,6 +30,7 @@ function TestSetup() { const contextValue = useSidebar({ position: 'right', hasOpenPane: openPane !== '', + onClosePane: () => setOpenPane(''), }); return ( @@ -37,7 +38,7 @@ function TestSetup() { {openPane === 'settings' && ( - setOpenPane('')} /> + )} diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx index 8c1bf784e53..311e11f390c 100644 --- a/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx @@ -2,14 +2,17 @@ import { css, cx } from '@emotion/css'; import { ReactNode, useContext } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; +import { getPortalContainer } from '../Portal/Portal'; import { SidebarButton } from './SidebarButton'; import { SidebarPaneHeader } from './SidebarPaneHeader'; import { SidebarResizer } from './SidebarResizer'; import { SIDE_BAR_WIDTH_ICON_ONLY, SIDE_BAR_WIDTH_WITH_TEXT, SidebarContext, SidebarContextValue } from './useSidebar'; +import { useCustomClickAway } from './useSidebarClickAway'; export interface Props { children?: ReactNode; @@ -30,9 +33,20 @@ export function SidebarComp({ children, contextValue }: Props) { const style = { [position]: theme.spacing(edgeMargin), bottom: theme.spacing(bottomMargin) }; + const ref = useCustomClickAway((evt) => { + const portalContainer = getPortalContainer(); + // ignore clicks inside portal container + if (evt.target instanceof Node && portalContainer && portalContainer.contains(evt.target)) { + return; + } + if (!isDocked && hasOpenPane) { + contextValue.onClosePane?.(); + } + }); + return ( -
+
{!tabsMode && } {children}
@@ -61,7 +75,7 @@ export function SiderbarToolbar({ children }: SiderbarToolbarProps) { icon={'web-section-alt'} onClick={context.onToggleDock} title={context.isDocked ? t('grafana-ui.sidebar.undock', 'Undock') : t('grafana-ui.sidebar.dock', 'Dock')} - data-testid="sidebar-dock-toggle" + data-testid={selectors.components.Sidebar.dockToggle} /> )}
diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx index 42fe30b083c..e1e5e0bca59 100644 --- a/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx +++ b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { ReactNode } from 'react'; +import { ReactNode, useContext } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -9,23 +9,29 @@ import { useStyles2 } from '../../themes/ThemeContext'; import { IconButton } from '../IconButton/IconButton'; import { Text } from '../Text/Text'; +import { SidebarContext } from './useSidebar'; + export interface Props { children?: ReactNode; title: string; - onClose?: () => void; } -export function SidebarPaneHeader({ children, onClose, title }: Props) { +export function SidebarPaneHeader({ children, title }: Props) { const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); + + if (!context) { + throw new Error('SidebarPaneHeader must be used within a Sidebar'); + } return (
- {onClose && ( + {context.onClosePane && ( void; onResize: (diff: number) => void; + /** Called when pane is closed or clicked outside of (in undocked mode) */ + onClosePane?: () => void; } export const SidebarContext: React.Context = React.createContext< @@ -35,6 +37,8 @@ export interface UseSideBarOptions { edgeMargin?: number; /** defaults to 2 grid units (16px) */ contentMargin?: number; + /** Called when pane is closed or clicked outside of (in undocked mode) */ + onClosePane?: () => void; } export const SIDE_BAR_WIDTH_ICON_ONLY = 5; @@ -48,6 +52,7 @@ export function useSidebar({ bottomMargin = 2, edgeMargin = 2, contentMargin = 2, + onClosePane, }: UseSideBarOptions): SidebarContextValue { const theme = useTheme2(); const [isDocked, setIsDocked] = React.useState(false); @@ -109,5 +114,6 @@ export function useSidebar({ edgeMargin, bottomMargin, contentMargin, + onClosePane, }; } diff --git a/packages/grafana-ui/src/components/Sidebar/useSidebarClickAway.ts b/packages/grafana-ui/src/components/Sidebar/useSidebarClickAway.ts new file mode 100644 index 00000000000..5f1f2ccfb89 --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/useSidebarClickAway.ts @@ -0,0 +1,32 @@ +import React from 'react'; + +/** + * Cannot use the react-use useClickAway directly as it relies on mousedown event which is not ideal as the element selection uses pointerdown + * @param ref + * @param onClickAway + */ +export function useCustomClickAway(onClickAway: (evt: MouseEvent | TouchEvent) => void) { + const ref = React.useRef(null); + const refCb = React.useRef(onClickAway); + + React.useLayoutEffect(() => { + refCb.current = onClickAway; + }); + + React.useEffect(() => { + const handler = (e: MouseEvent | TouchEvent) => { + const element = ref.current; + if (element && e.target instanceof Node && !element.contains(e.target)) { + refCb.current(e); + } + }; + + document.addEventListener('pointerdown', handler); + + return () => { + document.removeEventListener('pointerdown', handler); + }; + }, []); + + return ref; +} diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx index 41c81222661..d7f5c73280d 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx @@ -63,9 +63,9 @@ describe('DashboardEditPaneRenderer', () => { act(() => screen.getByLabelText('Outline').click()); - expect(await screen.findByTestId('sidebar-dock-toggle')).toBeInTheDocument(); + expect(await screen.findByTestId(selectors.components.Sidebar.dockToggle)).toBeInTheDocument(); - act(() => screen.getByTestId('sidebar-dock-toggle').click()); + act(() => screen.getByTestId(selectors.components.Sidebar.dockToggle).click()); expect(scene.state.editPane.state.isDocked).toBe(true); }); diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx index ce7df7a460b..59af972e3b0 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx @@ -69,6 +69,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls hasOpenPane: Boolean(openPane), contentMargin: 1, position: 'right', + onClosePane: () => editPane.closePane(), }); /** diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx index 7b461551773..f743f0a3ae7 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx @@ -5,7 +5,7 @@ import { getPanelPlugin } from '@grafana/data/test'; import { selectors } from '@grafana/e2e-selectors'; import { setPluginImportUtils } from '@grafana/runtime'; import { SceneVariableSet, VizPanel } from '@grafana/scenes'; -import { ElementSelectionContext } from '@grafana/ui'; +import { ElementSelectionContext, Sidebar, useSidebar } from '@grafana/ui'; import { DashboardScene } from '../scene/DashboardScene'; import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem'; @@ -86,6 +86,12 @@ function buildTestScene() { return testScene; } +function WrapSidebar({ children }: { children: React.ReactElement }) { + const sidebarContext = useSidebar({}); + + return {children}; +} + describe('DashboardOutline', () => { afterEach(() => { jest.clearAllMocks(); @@ -101,7 +107,9 @@ describe('DashboardOutline', () => { render( - + + + ); // select Row lvl 1 diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx index 97e17c34780..de80db73254 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx @@ -25,10 +25,7 @@ export function DashboardOutline({ editPane, isEditing }: Props) { return ( <> - editPane.closePane()} - /> + diff --git a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.test.tsx b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.test.tsx index 627ceb96d3b..e3dc190a073 100644 --- a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.test.tsx @@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'; import { selectors } from '@grafana/e2e-selectors'; import { SceneTimeRange } from '@grafana/scenes'; +import { Sidebar, useSidebar } from '@grafana/ui'; import { DashboardScene } from '../scene/DashboardScene'; import { RowItem } from '../scene/layout-rows/RowItem'; @@ -53,6 +54,12 @@ const buildTestScene = (scene: DashboardScene) => { return scene; }; +function WrapSidebar({ children }: { children: React.ReactElement }) { + const sidebarContext = useSidebar({}); + + return {children}; +} + describe('EditPaneHeader', () => { const mockEditPane = { state: { selection: null }, @@ -71,7 +78,11 @@ describe('EditPaneHeader', () => { const elementSelection = new ElementSelection([['row-test', row.getRef()]]); const editableElement = elementSelection.createSelectionElement()!; - render(); + render( + + + + ); await user.click(screen.getByTestId(selectors.components.EditPaneHeader.deleteButton)); expect(DashboardInteractions.trackRemoveRowClick).toHaveBeenCalled(); @@ -84,7 +95,11 @@ describe('EditPaneHeader', () => { const elementSelection = new ElementSelection([['tab-test', tab.getRef()]]); const editableElement = elementSelection.createSelectionElement()!; - render(); + render( + + + + ); await user.click(screen.getByTestId(selectors.components.EditPaneHeader.deleteButton)); expect(DashboardInteractions.trackRemoveTabClick).toHaveBeenCalled(); diff --git a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx index ea14464fc64..2f096293171 100644 --- a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx +++ b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx @@ -30,7 +30,7 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) { }; return ( - editPane.closePane()}> + {element.renderActions && element.renderActions()} {(onCopy || onDuplicate) && ( From 0856cac9ad164b34f3418d0d8018fb876ec20fa4 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:25:28 +0000 Subject: [PATCH 197/378] QueryEditorRows: Clear hideSeriesFrom override on query edit (#114315) clear hideSeriesFrom override on query edit --- .../PanelDataPane/PanelDataQueriesTab.tsx | 1 + .../query/components/QueryEditorRows.tsx | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index cea1dfc6cb0..d70fbf9c9c4 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -403,6 +403,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx index 2cbdc1aba87..e0dacee2669 100644 --- a/public/app/features/query/components/QueryEditorRows.tsx +++ b/public/app/features/query/components/QueryEditorRows.tsx @@ -9,8 +9,10 @@ import { HistoryItem, PanelData, getDataSourceRef, + isSystemOverrideWithRef, } from '@grafana/data'; import { getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { SceneObjectRef, VizPanel } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; @@ -44,6 +46,7 @@ export interface Props { queryLibraryRef?: string; onCancelQueryLibraryEdit?: () => void; isOpen?: boolean; + panelRef?: SceneObjectRef; } export class QueryEditorRows extends PureComponent { @@ -63,6 +66,20 @@ export class QueryEditorRows extends PureComponent { return item; }) ); + + if (this.props.panelRef) { + const panel = this.props.panelRef.resolve(); + const hideSeriesOverrideIndex = panel.state.fieldConfig.overrides.findIndex( + isSystemOverrideWithRef('hideSeriesFrom') + ); + + if (hideSeriesOverrideIndex !== -1) { + const newOverrides = [...panel.state.fieldConfig.overrides]; + newOverrides.splice(hideSeriesOverrideIndex, 1); + + panel.setState({ fieldConfig: { ...panel.state.fieldConfig, overrides: newOverrides } }); + } + } } onReplaceQuery(query: DataQuery, index: number) { From a2e34d229667b0a857b9a0c192cc7edcef0e06b8 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 28 Nov 2025 16:13:02 +0100 Subject: [PATCH 198/378] Alerting: Fetch only silenced alerts on the Silences page (#114564) --- .../alerting/unified/Silences.test.tsx | 23 +++++++++++++++++++ .../components/silences/SilencesTable.tsx | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx index 4ff786b2bc4..e5bce1433f2 100644 --- a/public/app/features/alerting/unified/Silences.test.tsx +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -1,3 +1,4 @@ +import { HttpResponse, http } from 'msw'; import { Route, Routes } from 'react-router-dom-v5-compat'; import { render, screen, userEvent, waitFor, within } from 'test/test-utils'; import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector'; @@ -154,6 +155,28 @@ describe('Silences', () => { TEST_TIMEOUT ); + it( + 'fetches silenced alerts with correct filter parameters', + async () => { + let capturedParams: URLSearchParams | undefined; + server.use( + http.get('/api/alertmanager/:datasourceUid/api/v2/alerts', ({ request }) => { + capturedParams = new URL(request.url).searchParams; + return HttpResponse.json([]); + }) + ); + + renderSilences(); + + await waitFor(() => expect(capturedParams).toBeDefined()); + + expect(capturedParams?.get('silenced')).toBe('true'); + expect(capturedParams?.get('active')).toBe('false'); + expect(capturedParams?.get('inhibited')).toBe('false'); + }, + TEST_TIMEOUT + ); + it( 'shows the correct number of silenced alerts', async () => { diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx index 8d3c80acad1..a06acda7c35 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx @@ -56,7 +56,7 @@ const SilencesTable = () => { const { data: alertManagerAlerts = [], isLoading: amAlertsIsLoading } = alertmanagerApi.endpoints.getAlertmanagerAlerts.useQuery( - { amSourceName: alertManagerSourceName, filter: { silenced: true, active: true, inhibited: true } }, + { amSourceName: alertManagerSourceName, filter: { silenced: true, active: false, inhibited: false } }, { ...API_QUERY_OPTIONS, skip: !canPreview } ); From a5d00d6264b8c83a56b898258957a13c62063bde Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Fri, 28 Nov 2025 16:17:38 +0100 Subject: [PATCH 199/378] Alerting: Fix make update-workspace for historian app (#114578) --- apps/iam/go.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/iam/go.mod b/apps/iam/go.mod index ad6c6039cc5..894e28c7a61 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -20,6 +20,8 @@ replace github.com/grafana/grafana/apps/alerting/notifications => ../alerting/no replace github.com/grafana/grafana/apps/alerting/rules => ../alerting/rules +replace github.com/grafana/grafana/apps/alerting/historian => ../alerting/historian + replace github.com/grafana/grafana/apps/correlations => ../correlations replace github.com/grafana/grafana/apps/investigations => ../investigations From e53236e9cdbe315beee75146a5df0fb16ab1325e Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 28 Nov 2025 16:53:00 +0100 Subject: [PATCH 200/378] Alerting: Add historian app to go.work (#114591) --- Dockerfile | 1 + apps/alerting/historian/go.mod | 29 ++++++++------ apps/alerting/historian/go.sum | 70 +++++++++++++++++----------------- go.work | 1 + 4 files changed, 56 insertions(+), 45 deletions(-) diff --git a/Dockerfile b/Dockerfile index c07eb0360e6..5bcca3643b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -111,6 +111,7 @@ COPY apps/iam apps/iam COPY apps apps COPY kindsv2 kindsv2 COPY apps/alerting/alertenrichment apps/alerting/alertenrichment +COPY apps/alerting/historian apps/alerting/historian COPY apps/alerting/notifications apps/alerting/notifications COPY apps/alerting/rules apps/alerting/rules COPY pkg/codegen pkg/codegen diff --git a/apps/alerting/historian/go.mod b/apps/alerting/historian/go.mod index eec687eb79f..234cca1eabe 100644 --- a/apps/alerting/historian/go.mod +++ b/apps/alerting/historian/go.mod @@ -5,7 +5,7 @@ go 1.25.3 require ( github.com/grafana/grafana-app-sdk v0.48.2 k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) require ( @@ -15,16 +15,20 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect @@ -32,20 +36,23 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/onsi/ginkgo/v2 v2.22.2 // indirect + github.com/onsi/gomega v1.36.2 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -58,20 +65,20 @@ require ( go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.9.0 // indirect + golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect google.golang.org/grpc v1.76.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.2 // indirect diff --git a/apps/alerting/historian/go.sum b/apps/alerting/historian/go.sum index a0f2396b250..7eadc30e628 100644 --- a/apps/alerting/historian/go.sum +++ b/apps/alerting/historian/go.sum @@ -12,8 +12,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= @@ -23,16 +23,18 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -42,8 +44,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= @@ -71,8 +73,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -87,10 +89,10 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -102,14 +104,14 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -148,8 +150,8 @@ go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOV go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -163,8 +165,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -181,8 +183,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -197,14 +199,14 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= -google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -224,8 +226,8 @@ k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= diff --git a/go.work b/go.work index f0f5d9acc19..a33973ac23f 100644 --- a/go.work +++ b/go.work @@ -7,6 +7,7 @@ use ( . // skip:golangci-lint ./apps/advisor ./apps/alerting/alertenrichment + ./apps/alerting/historian ./apps/alerting/notifications ./apps/alerting/rules ./apps/annotation From 32c1ad1b53cce639732f30d257de3db62b4165f7 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 28 Nov 2025 18:20:57 +0100 Subject: [PATCH 201/378] chore: update grafana-app-sdk to v0.48.4 (#114563) (chore): update grafana-app-sdk to v0.48.4 --- apps/advisor/go.mod | 18 ++++---- apps/advisor/go.sum | 60 +++++++++++++-------------- apps/alerting/alertenrichment/go.mod | 4 +- apps/alerting/alertenrichment/go.sum | 8 ++-- apps/alerting/historian/go.mod | 15 ++++--- apps/alerting/historian/go.sum | 28 ++++++------- apps/alerting/notifications/go.mod | 17 ++++---- apps/alerting/notifications/go.sum | 28 ++++++------- apps/alerting/rules/go.mod | 15 ++++--- apps/alerting/rules/go.sum | 28 ++++++------- apps/annotation/go.mod | 15 ++++--- apps/annotation/go.sum | 28 ++++++------- apps/collections/go.mod | 6 +-- apps/collections/go.sum | 12 +++--- apps/correlations/go.mod | 15 ++++--- apps/correlations/go.sum | 28 ++++++------- apps/dashboard/go.mod | 17 ++++---- apps/dashboard/go.sum | 34 +++++++-------- apps/example/go.mod | 17 ++++---- apps/example/go.sum | 34 +++++++-------- apps/folder/go.mod | 6 +-- apps/folder/go.sum | 12 +++--- apps/iam/go.mod | 29 +++++++------ apps/iam/go.sum | 62 ++++++++++++++-------------- apps/investigations/go.mod | 15 ++++--- apps/investigations/go.sum | 28 ++++++------- apps/logsdrilldown/go.mod | 15 ++++--- apps/logsdrilldown/go.sum | 28 ++++++------- apps/playlist/go.mod | 15 ++++--- apps/playlist/go.sum | 28 ++++++------- apps/plugins/go.mod | 16 +++---- apps/plugins/go.sum | 32 +++++++------- apps/preferences/go.mod | 6 +-- apps/preferences/go.sum | 12 +++--- apps/provisioning/go.mod | 14 +++---- apps/provisioning/go.sum | 28 ++++++------- apps/scope/go.mod | 2 +- apps/scope/go.sum | 4 +- apps/sdk.mk | 2 +- apps/secret/go.mod | 11 +++-- apps/secret/go.sum | 24 +++++------ apps/shorturl/go.mod | 17 ++++---- apps/shorturl/go.sum | 34 +++++++-------- go.mod | 29 +++++++------ go.sum | 62 ++++++++++++++-------------- go.work.sum | 9 ++++ pkg/aggregator/go.mod | 10 ++--- pkg/aggregator/go.sum | 20 ++++----- pkg/apimachinery/go.mod | 12 +++--- pkg/apimachinery/go.sum | 22 +++++----- pkg/apiserver/go.mod | 14 +++---- pkg/apiserver/go.sum | 28 ++++++------- pkg/build/go.mod | 8 ++-- pkg/build/go.sum | 16 +++---- pkg/promlib/go.mod | 10 ++--- pkg/promlib/go.sum | 24 +++++------ 56 files changed, 554 insertions(+), 577 deletions(-) diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 6e944d56c3b..2db5b66ac8c 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -8,8 +8,8 @@ require ( github.com/google/go-github/v70 v70.0.0 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 @@ -43,7 +43,7 @@ replace github.com/grafana/grafana/apps/plugins => ../plugins replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 require ( - cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect @@ -104,7 +104,7 @@ require ( github.com/gchaincl/sqlhooks v1.3.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-ldap/ldap/v3 v3.4.4 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect @@ -256,7 +256,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect @@ -290,9 +290,9 @@ require ( golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect gonum.org/v1/gonum v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -314,7 +314,7 @@ require ( modernc.org/sqlite v1.39.1 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect xorm.io/builder v0.3.13 // indirect ) diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 61d3d00b8c2..eb864bd4834 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -49,8 +49,8 @@ cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJW cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= @@ -107,8 +107,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= @@ -266,8 +266,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= @@ -346,9 +346,9 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= @@ -384,8 +384,8 @@ github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkPro github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -616,10 +616,10 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= @@ -1074,8 +1074,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1134,8 +1134,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= @@ -1160,12 +1158,12 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= @@ -1707,10 +1705,10 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1742,8 +1740,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1863,8 +1861,8 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/alerting/alertenrichment/go.mod b/apps/alerting/alertenrichment/go.mod index c717b9bf364..7a015ce4580 100644 --- a/apps/alerting/alertenrichment/go.mod +++ b/apps/alerting/alertenrichment/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/alertenrichment go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -36,5 +36,5 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/apps/alerting/alertenrichment/go.sum b/apps/alerting/alertenrichment/go.sum index fc4692a07a9..ba98672f90c 100644 --- a/apps/alerting/alertenrichment/go.sum +++ b/apps/alerting/alertenrichment/go.sum @@ -23,8 +23,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 h1:PgMfX4OPENz/iXmtDDIW9+poZY4UD0hhmXm7flVclDo= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28/go.mod h1:av5N0Naq+8VV9MLF7zAkihy/mVq5UbS2EvRSJukDHlY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -114,7 +114,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/alerting/historian/go.mod b/apps/alerting/historian/go.mod index 234cca1eabe..934f9ac5772 100644 --- a/apps/alerting/historian/go.mod +++ b/apps/alerting/historian/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/historian go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -30,7 +30,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -52,11 +52,10 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -75,9 +74,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,6 +87,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/alerting/historian/go.sum b/apps/alerting/historian/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/alerting/historian/go.sum +++ b/apps/alerting/historian/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 24c17cc900a..c0ea062e70b 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/alerting/notifications go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -37,7 +37,6 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -62,14 +61,13 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.etcd.io/etcd/api/v3 v3.6.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect @@ -84,7 +82,6 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect @@ -93,9 +90,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -109,6 +106,6 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 9b84e11f3d6..7a8ccd1d4e8 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -71,10 +71,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= @@ -187,8 +187,8 @@ go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= @@ -279,13 +279,13 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -324,7 +324,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/alerting/rules/go.mod b/apps/alerting/rules/go.mod index 17aa567f077..dceeb576100 100644 --- a/apps/alerting/rules/go.mod +++ b/apps/alerting/rules/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/alerting/rules go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/prometheus/common v0.67.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -52,11 +52,10 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -75,9 +74,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,6 +87,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/alerting/rules/go.sum b/apps/alerting/rules/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/alerting/rules/go.sum +++ b/apps/alerting/rules/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/annotation/go.mod b/apps/annotation/go.mod index 988575c7ce1..5173ad8c627 100644 --- a/apps/annotation/go.mod +++ b/apps/annotation/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/annotation go 1.24.0 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -52,11 +52,10 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -75,9 +74,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,6 +87,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/annotation/go.sum b/apps/annotation/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/annotation/go.sum +++ b/apps/annotation/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/collections/go.mod b/apps/collections/go.mod index 281a6579c67..621440d3bfe 100644 --- a/apps/collections/go.mod +++ b/apps/collections/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/collections go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 @@ -25,7 +25,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -64,6 +64,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/collections/go.sum b/apps/collections/go.sum index 8d576bfaa8b..eb2916cb29a 100644 --- a/apps/collections/go.sum +++ b/apps/collections/go.sum @@ -33,10 +33,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -181,7 +181,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/correlations/go.mod b/apps/correlations/go.mod index 988f18b89d6..42072c1cb2b 100644 --- a/apps/correlations/go.mod +++ b/apps/correlations/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/correlations go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -52,11 +52,10 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -75,9 +74,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,6 +87,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/correlations/go.sum b/apps/correlations/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/correlations/go.sum +++ b/apps/correlations/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index e5a1352e7d8..56d8b7715a1 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -5,8 +5,8 @@ go 1.25.3 require ( cuelang.org/go v0.11.1 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/prometheus/client_golang v1.23.2 @@ -30,7 +30,7 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect @@ -92,7 +92,7 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect @@ -106,7 +106,6 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect @@ -119,9 +118,9 @@ require ( golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -130,6 +129,6 @@ require ( k8s.io/klog/v2 v2.130.1 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 135eec59afc..17fbe2222d2 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -39,8 +39,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -85,10 +85,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= @@ -239,8 +239,8 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -280,8 +280,6 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -337,12 +335,12 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -375,7 +373,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/example/go.mod b/apps/example/go.mod index 66b3b93f6df..61574462d8a 100644 --- a/apps/example/go.mod +++ b/apps/example/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/example go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 @@ -21,7 +21,7 @@ require ( github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect @@ -62,7 +62,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -73,7 +73,6 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect @@ -82,9 +81,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -95,6 +94,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/example/go.sum b/apps/example/go.sum index aaf74c34d44..3211e083601 100644 --- a/apps/example/go.sum +++ b/apps/example/go.sum @@ -18,8 +18,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -56,10 +56,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe h1:pPoFj2bQKDBg5EyEdOU+Jn+0hQN+M775Qihk73RbdSs= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe/go.mod h1:zn/yoxKpWA2KUsxOhQbSbL8OCkF2JNLpSEHs+hQYvdM= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= @@ -140,8 +140,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -169,8 +169,6 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -213,12 +211,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -250,7 +248,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/folder/go.mod b/apps/folder/go.mod index 1c9249745a9..4e336935537 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/folder go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -24,7 +24,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -64,6 +64,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/folder/go.sum b/apps/folder/go.sum index 944e0d69721..ab70319f432 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -33,10 +33,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -181,7 +181,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 894e28c7a61..0821652b3d8 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -52,8 +52,8 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/apps/folder v0.0.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/prometheus/client_golang v1.23.2 @@ -68,7 +68,7 @@ require ( cloud.google.com/go v0.121.4 // indirect cloud.google.com/go/auth v0.16.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect cloud.google.com/go/storage v1.55.0 // indirect @@ -84,7 +84,7 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect @@ -139,7 +139,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheekybits/genny v1.0.0 // indirect github.com/cloudflare/circl v1.6.1 // indirect - github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect + github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect @@ -159,7 +159,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -169,7 +169,7 @@ require ( github.com/gchaincl/sqlhooks v1.3.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-ldap/ldap/v3 v3.4.4 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect @@ -362,7 +362,7 @@ require ( github.com/spf13/cobra v1.10.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect - github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect @@ -375,15 +375,14 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/zeebo/errs v1.4.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.etcd.io/etcd/api/v3 v3.6.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect @@ -432,9 +431,9 @@ require ( gonum.org/v1/gonum v0.16.0 // indirect google.golang.org/api v0.242.0 // indirect google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect @@ -461,7 +460,7 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect xorm.io/builder v0.3.13 // indirect ) diff --git a/apps/iam/go.sum b/apps/iam/go.sum index c91c571fa7d..5bab810e8d0 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -53,8 +53,8 @@ cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJW cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= @@ -153,8 +153,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/FZambia/eagle v0.2.0 h1:1kQaZpJvbkvAXFRE/9K2ucBMuVqo+E29EMLYB74hIis= github.com/FZambia/eagle v0.2.0/go.mod h1:LKMYBwGYhao5sJI0TppvQ4SvvldFj9gITxrl8NvGwG0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= @@ -405,8 +405,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= @@ -519,10 +519,10 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -565,8 +565,8 @@ github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkPro github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -833,10 +833,10 @@ github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f h1:5xkjl5Y/j2QefJKO github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f/go.mod h1:+O5QxOwwgP10jedZHapzXY+IPKTnzHBtIs5UUb9G+kI= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe h1:q+QaVANzNZxvTovycpQvDTfsNZ2rHh4XIIaccMnrIR4= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= @@ -1452,8 +1452,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ= github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= @@ -1543,8 +1543,6 @@ github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= @@ -1577,8 +1575,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/collector/featuregate v1.44.0 h1:/GeGhTD8f+FNWS7C4w1Dj0Ui9Jp4v2WAdlXyW1p3uG8= go.opentelemetry.io/collector/featuregate v1.44.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= go.opentelemetry.io/collector/pdata v1.44.0 h1:q/EfWDDKrSaf4hjTIzyPeg1ZcCRg1Uj7VTFnGfNVdk8= @@ -1587,8 +1585,8 @@ go.opentelemetry.io/collector/semconv v0.124.0 h1:YTdo3UFwNyDQCh9DiSm2rbzAgBuwn/ go.opentelemetry.io/collector/semconv v0.124.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= @@ -2153,10 +2151,10 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -2190,8 +2188,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -2315,8 +2313,8 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index d4f3685a2aa..6741017390f 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/investigations go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 k8s.io/apimachinery v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -31,7 +31,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -53,11 +53,10 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -76,9 +75,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,6 +87,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/logsdrilldown/go.mod b/apps/logsdrilldown/go.mod index 712d99405d0..f7d81af4b9d 100644 --- a/apps/logsdrilldown/go.mod +++ b/apps/logsdrilldown/go.mod @@ -5,8 +5,8 @@ go 1.24.0 toolchain go1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -54,11 +54,10 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -77,9 +76,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -90,6 +89,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/logsdrilldown/go.sum b/apps/logsdrilldown/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/logsdrilldown/go.sum +++ b/apps/logsdrilldown/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 8a254e980d1..bdc831e87ba 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/playlist go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 k8s.io/apimachinery v0.34.2 k8s.io/client-go v0.34.2 k8s.io/klog/v2 v2.130.1 @@ -32,7 +32,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -54,11 +54,10 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -77,9 +76,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,6 +87,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index ef1b58517a3..cebf6594566 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -11,8 +11,8 @@ replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver require ( github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 @@ -46,7 +46,7 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -146,7 +146,7 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect @@ -175,9 +175,9 @@ require ( golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect @@ -190,6 +190,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index 06683624e65..de829f9be57 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -71,8 +71,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -161,10 +161,10 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= @@ -412,8 +412,8 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -536,12 +536,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuB gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -594,7 +594,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/preferences/go.mod b/apps/preferences/go.mod index cd702672de6..18bc0d81e19 100644 --- a/apps/preferences/go.mod +++ b/apps/preferences/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/preferences go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -24,7 +24,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -64,6 +64,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/preferences/go.sum b/apps/preferences/go.sum index 8d576bfaa8b..eb2916cb29a 100644 --- a/apps/preferences/go.sum +++ b/apps/preferences/go.sum @@ -33,10 +33,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -181,7 +181,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index 0fb19b230c8..65293ff0f33 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/go-github/v70 v70.0.0 github.com/google/uuid v1.6.0 github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 github.com/grafana/nanogit v0.3.0 @@ -18,7 +18,7 @@ require ( k8s.io/apiserver v0.34.2 k8s.io/client-go v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 ) require ( @@ -29,7 +29,7 @@ require ( github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect @@ -44,7 +44,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect - github.com/grafana/grafana-app-sdk v0.48.2 // indirect + github.com/grafana/grafana-app-sdk v0.48.4 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect @@ -62,7 +62,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect @@ -75,8 +75,8 @@ require ( golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index db6bd3cf414..76238334303 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -16,8 +16,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -62,10 +62,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f h1:f+Z5Xpfp1WNYjUe23ginerWsHWUsRgOWrr3WGu3SlWs= github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f/go.mod h1:RA8mP8KVIwKXBx3Ssqa/uEBABib5LvUWYPVMxrNvnP0= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= @@ -133,8 +133,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= @@ -224,10 +224,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -260,7 +260,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/scope/go.mod b/apps/scope/go.mod index 9b739eb3d0b..c7d21d96f4a 100644 --- a/apps/scope/go.mod +++ b/apps/scope/go.mod @@ -39,5 +39,5 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/apps/scope/go.sum b/apps/scope/go.sum index 1796d287d60..3e1f8bc7d54 100644 --- a/apps/scope/go.sum +++ b/apps/scope/go.sum @@ -114,7 +114,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/sdk.mk b/apps/sdk.mk index 6956168a7df..62d89ed8ed3 100644 --- a/apps/sdk.mk +++ b/apps/sdk.mk @@ -1,4 +1,4 @@ -APP_SDK_VERSION = v0.48.2 +APP_SDK_VERSION = v0.48.4 APP_SDK_DIR = $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) APP_SDK_BIN = $(APP_SDK_DIR)/grafana-app-sdk diff --git a/apps/secret/go.mod b/apps/secret/go.mod index 2ac03ea3f8b..d8de93c5cff 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -3,11 +3,11 @@ module github.com/grafana/grafana/apps/secret go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf github.com/stretchr/testify v1.11.1 go.yaml.in/yaml/v3 v3.0.4 - google.golang.org/grpc v1.76.0 + google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -29,7 +29,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -51,7 +51,6 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/net v0.47.0 // indirect @@ -60,13 +59,13 @@ require ( golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/client-go v0.34.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/secret/go.sum b/apps/secret/go.sum index 74024f8df34..a18c6f39a89 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -37,10 +37,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf/go.mod h1:eAlOam2uWhrsEZlOoAr7XZ9hbBP7SyYGYn31/aQAPs8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -111,8 +111,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= @@ -168,10 +168,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -199,7 +199,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index 740aea32a72..eb9936ab643 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/shorturl go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 @@ -22,7 +22,7 @@ require ( github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect @@ -63,7 +63,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -74,7 +74,6 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect @@ -83,9 +82,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -95,6 +94,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/shorturl/go.sum b/apps/shorturl/go.sum index f8b394c0f37..4508fbd82a6 100644 --- a/apps/shorturl/go.sum +++ b/apps/shorturl/go.sum @@ -18,8 +18,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -56,10 +56,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba h1:Qam8QzVRsyZN39zgZ9Vj6e8PEfswvv2McnqCZ/v5NcI= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba/go.mod h1:rlJ/mmE0RQolOB2+HV3+bw+ZifHyPDQurBwZEus+Wm0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= @@ -140,8 +140,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -169,8 +169,6 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -213,12 +211,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -250,7 +248,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/go.mod b/go.mod index 3d277125a1f..371cd742c72 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,7 @@ require ( github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group github.com/gchaincl/sqlhooks v1.3.0 // @grafana/grafana-search-and-storage github.com/getkin/kin-openapi v0.133.0 // @grafana/grafana-app-platform-squad - github.com/go-jose/go-jose/v4 v4.1.2 // @grafana/identity-access-team + github.com/go-jose/go-jose/v4 v4.1.3 // @grafana/identity-access-team github.com/go-kit/log v0.2.1 // @grafana/grafana-backend-group github.com/go-ldap/ldap/v3 v3.4.4 // @grafana/identity-access-team github.com/go-logfmt/logfmt v0.6.1 // @grafana/oss-big-tent @@ -97,8 +97,8 @@ require ( github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend - github.com/grafana/grafana-app-sdk v0.48.2 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-app-sdk/logging v0.48.1 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk v0.48.4 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk/logging v0.48.3 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-aws-sdk v1.3.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad @@ -211,7 +211,7 @@ require ( golang.org/x/tools v0.39.0 // indirect; @grafana/grafana-as-code gonum.org/v1/gonum v0.16.0 // @grafana/oss-big-tent google.golang.org/api v0.242.0 // @grafana/grafana-backend-group - google.golang.org/grpc v1.76.0 // @grafana/plugins-platform-backend + google.golang.org/grpc v1.77.0 // @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.10 // @grafana/plugins-platform-backend gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group @@ -227,7 +227,7 @@ require ( modernc.org/sqlite v1.39.1 // @grafana/grafana-backend-group pgregory.net/rapid v1.2.0 // @grafana/grafana-operator-experience-squad sigs.k8s.io/randfill v1.0.0 // @grafana/grafana-app-platform-squad - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // @grafana/grafana-app-platform-squad + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // @grafana/grafana-app-platform-squad xorm.io/builder v0.3.13 // @grafana/grafana-backend-group ) @@ -298,7 +298,7 @@ require ( cloud.google.com/go v0.121.4 // indirect cloud.google.com/go/auth v0.16.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect @@ -316,7 +316,7 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect github.com/FZambia/eagle v0.2.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -391,7 +391,7 @@ require ( github.com/cheekybits/genny v1.0.0 // indirect github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d // indirect github.com/cloudflare/circl v1.6.1 // indirect - github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect + github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -414,7 +414,7 @@ require ( github.com/elazarl/goproxy v1.7.2 // indirect github.com/emicklei/proto v1.13.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -583,7 +583,7 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/viper v1.21.0 // indirect - github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect @@ -601,18 +601,17 @@ require ( github.com/yudai/pp v2.0.1+incompatible // indirect github.com/yuin/gopher-lua v1.1.1 // indirect github.com/zclconf/go-cty v1.16.3 // indirect - github.com/zeebo/errs v1.4.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.etcd.io/bbolt v1.4.2 // indirect go.etcd.io/etcd/api/v3 v3.6.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/collector/featuregate v1.44.0 // indirect go.opentelemetry.io/collector/semconv v0.124.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect @@ -638,8 +637,8 @@ require ( golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect diff --git a/go.sum b/go.sum index 971b7b06f74..ff568126e42 100644 --- a/go.sum +++ b/go.sum @@ -194,8 +194,8 @@ cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= @@ -725,8 +725,8 @@ github.com/FZambia/eagle v0.2.0 h1:1kQaZpJvbkvAXFRE/9K2ucBMuVqo+E29EMLYB74hIis= github.com/FZambia/eagle v0.2.0/go.mod h1:LKMYBwGYhao5sJI0TppvQ4SvvldFj9gITxrl8NvGwG0= github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc= github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= @@ -1040,8 +1040,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= @@ -1169,10 +1169,10 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -1232,8 +1232,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -1633,10 +1633,10 @@ github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8= github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= @@ -2454,8 +2454,8 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ= github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= @@ -2589,8 +2589,6 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= @@ -2627,8 +2625,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/collector v0.124.0 h1:g/dfdGFhBcQI0ggGxTmGlJnJ6Yl6T2gVxQoIj4UfXCc= go.opentelemetry.io/collector/featuregate v1.44.0 h1:/GeGhTD8f+FNWS7C4w1Dj0Ui9Jp4v2WAdlXyW1p3uG8= go.opentelemetry.io/collector/featuregate v1.44.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= @@ -2640,8 +2638,8 @@ go.opentelemetry.io/collector/semconv v0.124.0 h1:YTdo3UFwNyDQCh9DiSm2rbzAgBuwn/ go.opentelemetry.io/collector/semconv v0.124.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= @@ -3517,15 +3515,15 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go. google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -3572,8 +3570,8 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -3763,8 +3761,8 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= diff --git a/go.work.sum b/go.work.sum index c4b732eaadb..3bc86d10781 100644 --- a/go.work.sum +++ b/go.work.sum @@ -72,6 +72,7 @@ cloud.google.com/go/cloudtasks v1.13.6/go.mod h1:/IDaQqGKMixD+ayM43CfsvWF2k36Geo cloud.google.com/go/compute v1.40.0 h1:dlEzKo/BtyEGNc+SflXwwoBh52dNl/A5BaSYurT0k0k= cloud.google.com/go/compute v1.40.0/go.mod h1:P1doTJnlwurJDzIQFMp4mgU+vyCe9HU2NWTlqTfq3MY= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= cloud.google.com/go/contactcenterinsights v1.17.3 h1:lenyU3uzHwKDveCwmpfNxHYvLS3uEBWdn+O7+rSxy+Q= cloud.google.com/go/contactcenterinsights v1.17.3/go.mod h1:7Uu2CpxS3f6XxhRdlEzYAkrChpR5P5QfcdGAFEdHOG8= cloud.google.com/go/container v1.43.0 h1:A6J92FJPfxTvyX7MHF+w4t2W9WCqvHOi9UB5SAeSy3w= @@ -1535,6 +1536,8 @@ github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+ github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= @@ -1917,6 +1920,7 @@ golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -1932,6 +1936,7 @@ golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2059,6 +2064,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= @@ -2066,6 +2072,7 @@ google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFL google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= @@ -2079,6 +2086,8 @@ google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= +google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds= +google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index 24f0d25efae..264fb259202 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -17,7 +17,7 @@ require ( k8s.io/component-base v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 ) require ( @@ -101,7 +101,7 @@ require ( go.etcd.io/etcd/api/v3 v3.6.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect @@ -130,9 +130,9 @@ require ( golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 313c0b7c200..1c52723a0ac 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -269,8 +269,8 @@ go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -382,13 +382,13 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -426,7 +426,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index fdcf1562f1f..3a111a9028b 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/apimachinery go 1.25.3 require ( - github.com/go-jose/go-jose/v4 v4.1.2 + github.com/go-jose/go-jose/v4 v4.1.3 github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team github.com/stretchr/testify v1.11.1 @@ -34,27 +34,25 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 3b59ab0b8b9..42e2598d7f5 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -6,8 +6,8 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -77,8 +77,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= @@ -96,8 +96,6 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -130,10 +128,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -157,7 +155,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 4a3a9d7a84b..08a647a145b 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -5,7 +5,7 @@ go 1.25.3 require ( github.com/google/go-cmp v0.7.0 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 @@ -17,7 +17,7 @@ require ( k8s.io/component-base v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 ) require ( @@ -31,7 +31,7 @@ require ( github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect @@ -71,7 +71,7 @@ require ( go.etcd.io/etcd/api/v3 v3.6.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect @@ -92,9 +92,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.39.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 52c046f42a0..ff63cdc69ca 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -25,8 +25,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -71,8 +71,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= @@ -170,8 +170,8 @@ go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= @@ -260,13 +260,13 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -303,7 +303,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 80952661b2b..e08dfce7d74 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -16,7 +16,7 @@ require ( golang.org/x/net v0.47.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources golang.org/x/sync v0.18.0 // @grafana/alerting-backend golang.org/x/text v0.31.0 // indirect; @grafana/grafana-backend-group - google.golang.org/grpc v1.76.0 // indirect; @grafana/plugins-platform-backend + google.golang.org/grpc v1.77.0 // indirect; @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.10 // indirect; @grafana/plugins-platform-backend ) @@ -30,8 +30,8 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect golang.org/x/sys v0.38.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect ) require ( @@ -51,7 +51,7 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sosodev/duration v1.3.1 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect diff --git a/pkg/build/go.sum b/pkg/build/go.sum index b5a55f1d488..3d794bfccf8 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -59,8 +59,8 @@ github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTd github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= @@ -105,12 +105,12 @@ golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 314d19580a4..6aea33c1df8 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -86,7 +86,7 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect @@ -111,9 +111,9 @@ require ( golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.242.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/client-go v0.34.2 // indirect @@ -122,5 +122,5 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 85d212bc772..eaa9d903015 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -3,8 +3,8 @@ cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 h1:wL5IEG5zb7BVv1Kv0Xm92orq+5hB5Nipn3B5tn4Rqfk= @@ -278,8 +278,8 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -378,12 +378,12 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -410,7 +410,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From 1c3795d97c278d19842820717bfa166b7653520d Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 28 Nov 2025 21:22:36 +0100 Subject: [PATCH 202/378] Alerting: Add spec.title selector to the receivers endpoint (#114599) --- .../alerting/notifications/kinds/receiver.cue | 8 +- .../v0alpha1/receiver_schema_gen.go | 15 +- .../v0alpha1/zz_openapi_gen.go | 904 ++++++++++++++++++ .../apis/alertingnotifications_manifest.go | 3 + 4 files changed, 925 insertions(+), 5 deletions(-) create mode 100644 apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go diff --git a/apps/alerting/notifications/kinds/receiver.cue b/apps/alerting/notifications/kinds/receiver.cue index 719dd8ad699..ed87ad8aec1 100644 --- a/apps/alerting/notifications/kinds/receiver.cue +++ b/apps/alerting/notifications/kinds/receiver.cue @@ -13,7 +13,7 @@ receiverv0alpha1: receiverKind & { schema: { spec: v0alpha1.ReceiverSpec } -// selectableFields: [ // TODO revisit when custom field selectors are supported -// "spec.title", -// ] -} \ No newline at end of file + selectableFields: [ + "spec.title", + ] +} diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go index 0987b4504bc..ea4e3b8e363 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go @@ -5,13 +5,26 @@ package v0alpha1 import ( + "errors" + "github.com/grafana/grafana-app-sdk/resource" ) // schema is unexported to prevent accidental overwrites var ( schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &Receiver{}, &ReceiverList{}, resource.WithKind("Receiver"), - resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope)) + resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ + FieldSelector: "spec.title", + FieldValueFunc: func(o resource.Object) (string, error) { + cast, ok := o.(*Receiver) + if !ok { + return "", errors.New("provided object must be of type *Receiver") + } + + return cast.Spec.Title, nil + }, + }, + })) kindReceiver = resource.Kind{ Schema: schemaReceiver, Codecs: map[resource.KindEncoding]resource.Codec{ diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go new file mode 100644 index 00000000000..33b088eca5c --- /dev/null +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go @@ -0,0 +1,904 @@ +package v0alpha1 + +import ( + common "k8s.io/kube-openapi/pkg/common" + spec "k8s.io/kube-openapi/pkg/validation/spec" +) + +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.Receiver": schema_pkg_apis_alertingnotifications_v0alpha1_Receiver(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverIntegration": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverIntegration(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverList": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverList(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverSpec": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverSpec(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTree": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTree(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeList": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeList(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeMatcher": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeMatcher(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRoute(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRouteDefaults": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRouteDefaults(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeSpec": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeSpec(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroup": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroup(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupList": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupList(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupSpec": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupSpec(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeInterval": schema_pkg_apis_alertingnotifications_v0alpha1_TimeInterval(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalInterval": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalInterval(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalList": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalList(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalSpec": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalSpec(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalTimeRange": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalTimeRange(ref), + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_Receiver(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the Receiver", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverIntegration(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "uid": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "disableResolveMessage": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "settings": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + "secureFields": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"type", "version", "settings"}, + }, + }, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.Receiver"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.Receiver", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "integrations": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverIntegration"), + }, + }, + }, + }, + }, + }, + Required: []string{"title", "integrations"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverIntegration"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTree(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the RoutingTree", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTree"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTree", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeMatcher(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type", "label", "value"}, + }, + }, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRoute(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "receiver": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "matchers": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeMatcher"), + }, + }, + }, + }, + }, + "continue": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "group_by": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "mute_time_intervals": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "active_time_intervals": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "routes": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute"), + }, + }, + }, + }, + }, + "group_wait": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "group_interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "repeat_interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"continue"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeMatcher", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRouteDefaults(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "receiver": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "group_by": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "group_wait": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "group_interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "repeat_interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"receiver"}, + }, + }, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "defaults": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRouteDefaults"), + }, + }, + "routes": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute"), + }, + }, + }, + }, + }, + }, + Required: []string{"defaults", "routes"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRouteDefaults"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroup(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the TemplateGroup", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroup"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroup", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "content": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"title", "content"}, + }, + }, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the TimeInterval", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "times": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalTimeRange"), + }, + }, + }, + }, + }, + "weekdays": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "days_of_month": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "months": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "years": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "location": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalTimeRange"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeInterval"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeInterval", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "time_intervals": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalInterval"), + }, + }, + }, + }, + }, + }, + Required: []string{"name", "time_intervals"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalInterval"}, + } +} + +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalTimeRange(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "start_time": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "end_time": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"start_time", "end_time"}, + }, + }, + } +} diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go index cebfbec12f1..e16f443a4f6 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go @@ -49,6 +49,9 @@ var appManifestData = app.ManifestData{ Scope: "Namespaced", Conversion: false, Schema: &versionSchemaReceiverv0alpha1, + SelectableFields: []string{ + "spec.title", + }, }, { From 66b72b659f87fff8a2531c02a0dde0450e467429 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 00:39:42 +0000 Subject: [PATCH 203/378] I18n: Download translations from Crowdin (#114604) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 12 ++++++++++++ public/locales/de-DE/grafana.json | 12 ++++++++++++ public/locales/es-ES/grafana.json | 12 ++++++++++++ public/locales/fr-FR/grafana.json | 12 ++++++++++++ public/locales/hu-HU/grafana.json | 12 ++++++++++++ public/locales/id-ID/grafana.json | 12 ++++++++++++ public/locales/it-IT/grafana.json | 12 ++++++++++++ public/locales/ja-JP/grafana.json | 12 ++++++++++++ public/locales/ko-KR/grafana.json | 12 ++++++++++++ public/locales/nl-NL/grafana.json | 12 ++++++++++++ public/locales/pl-PL/grafana.json | 12 ++++++++++++ public/locales/pt-BR/grafana.json | 12 ++++++++++++ public/locales/pt-PT/grafana.json | 12 ++++++++++++ public/locales/ru-RU/grafana.json | 12 ++++++++++++ public/locales/sv-SE/grafana.json | 12 ++++++++++++ public/locales/tr-TR/grafana.json | 12 ++++++++++++ public/locales/zh-Hans/grafana.json | 12 ++++++++++++ public/locales/zh-Hant/grafana.json | 12 ++++++++++++ 18 files changed, 216 insertions(+) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 73a8378bd20..0f54e455b41 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -5437,6 +5437,10 @@ "loading-initializing-dashboard": "Načítání a inicializace nástěnky", "title-not-found": "Panel s ID {{panelId}} nebyl nalezen" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Proměnné šablony" }, @@ -11184,6 +11188,12 @@ "could-anything-matching-query": "Nenašly jsme žádnou shodu s vaším dotazem" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Vymazat typy", "select-aria-label": "Filtr typu panelu", @@ -13387,6 +13397,7 @@ "create-team": { "create": "Vytvořit", "description-email": "Toto je volitelné a používá se především k povolení vlastních týmových avatarů", + "failed-to-create": "", "label-email": "E-mail", "label-name": "Název", "label-role": "Role" @@ -13418,6 +13429,7 @@ "title-edit-team": "Upravit tým", "tooltip-edit-team": "Upravit tým" }, + "loading-teams": "", "new-team": "Nový tým", "placeholder-search-teams": "Hledat týmy" }, diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 469a0ddfef8..075df04b5d0 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Laden und Initialisieren des Dashboards", "title-not-found": "Das Panel mit der ID {{panelId}} wurde nicht gefunden" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Vorlagenvariablen" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Es konnte nichts gefunden werden, was Ihrer Abfrage entspricht" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Typen zurücksetzen", "select-aria-label": "Panel-Typ-Filter", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Erstellen", "description-email": "Dies ist optional und wird hauptsächlich genutzt, um individuelle Team-Avatare zu ermöglichen", + "failed-to-create": "", "label-email": "E-Mail-Adresse", "label-name": "Name", "label-role": "Rolle" @@ -13304,6 +13315,7 @@ "title-edit-team": "Team bearbeiten", "tooltip-edit-team": "Team bearbeiten" }, + "loading-teams": "", "new-team": "Neues Team", "placeholder-search-teams": "Teams suchen" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index db55d7dbe2b..d5ef82955ed 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Cargando e iniciando el dashboard", "title-not-found": "Panel con ID {{panelId}} no encontrado" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Variables de plantilla" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "No se ha encontrado nada que coincida con tu consulta" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Borrar tipos", "select-aria-label": "Filtro de tipo de panel", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Crear", "description-email": "Esto es opcional y se utiliza principalmente para permitir avatares de equipo personalizados", + "failed-to-create": "", "label-email": "Correo electrónico", "label-name": "Nombre", "label-role": "Rol" @@ -13304,6 +13315,7 @@ "title-edit-team": "Editar equipo", "tooltip-edit-team": "Editar equipo" }, + "loading-teams": "", "new-team": "Nuevo equipo", "placeholder-search-teams": "Buscar equipos" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index e0b0cd18363..7849934975c 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Chargement et initialisation du tableau de bord", "title-not-found": "Panneau avec l’ID {{panelId}} introuvable" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Variables de modèle" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Impossible de trouver quoi que ce soit correspondant à votre requête" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Effacer les types", "select-aria-label": "Filtre par type de panneau", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Créer", "description-email": "Ceci est facultatif et est principalement utilisé pour autoriser les avatars d’équipe personnalisés", + "failed-to-create": "", "label-email": "Adresse e-mail", "label-name": "Nom", "label-role": "Rôle" @@ -13304,6 +13315,7 @@ "title-edit-team": "Modifier l’équipe", "tooltip-edit-team": "Modifier l’équipe" }, + "loading-teams": "", "new-team": "Nouvelle équipe", "placeholder-search-teams": "Rechercher les équipes" }, diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 3ff7e30485b..800a0de556e 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Irányítópult betöltése és inicializálása", "title-not-found": "A(z) {{panelId}} azonosítójú panel nem található" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Sablonváltozók" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Nincs találat a lekérdezésre" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Típusok törlése", "select-aria-label": "Paneltípusszűrő", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Létrehozás", "description-email": "Ez nem kötelező, és elsősorban egyéni csapatavatárok engedélyezésére szolgál", + "failed-to-create": "", "label-email": "E-mail", "label-name": "Név", "label-role": "Szerepkör" @@ -13304,6 +13315,7 @@ "title-edit-team": "Csapat szerkesztése", "tooltip-edit-team": "Csapat szerkesztése" }, + "loading-teams": "", "new-team": "Új csapat", "placeholder-search-teams": "Csapatok keresése" }, diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index a6708a09472..edf158ffbd1 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -5374,6 +5374,10 @@ "loading-initializing-dashboard": "Memuat & menginisialisasi dasbor", "title-not-found": "Panel dengan id {{panelId}} tidak ditemukan" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Variabel templat" }, @@ -11043,6 +11047,12 @@ "could-anything-matching-query": "Tidak dapat menemukan sesuatu yang cocok dengan kueri Anda" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Hapus jenis", "select-aria-label": "Filter jenis panel", @@ -13216,6 +13226,7 @@ "create-team": { "create": "Buat", "description-email": "Hal ini opsional dan secara spesifik digunakan untuk mengizinkan avatar tim kustom", + "failed-to-create": "", "label-email": "Email", "label-name": "Nama", "label-role": "Peran" @@ -13247,6 +13258,7 @@ "title-edit-team": "Edit tim", "tooltip-edit-team": "Edit tim" }, + "loading-teams": "", "new-team": "Tim Baru", "placeholder-search-teams": "Cari tim" }, diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index e5fc8647a20..c0c2e9cfe0d 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Caricamento e inizializzazione della dashboard", "title-not-found": "Pannello con ID {{panelId}} non trovato" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Variabili del modello" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Non è stato possibile trovare nulla che corrispondesse alla tua query" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Cancella tipi", "select-aria-label": "Filtro del tipo di pannello", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Crea", "description-email": "Questo è facoltativo e viene utilizzato principalmente per consentire avatar di team personalizzati", + "failed-to-create": "", "label-email": "Email", "label-name": "Nome", "label-role": "Ruolo" @@ -13304,6 +13315,7 @@ "title-edit-team": "Modifica team", "tooltip-edit-team": "Modifica team" }, + "loading-teams": "", "new-team": "Nuovo team", "placeholder-search-teams": "Cerca team" }, diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index fa3f87eabf4..6736b3e5fb0 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -5374,6 +5374,10 @@ "loading-initializing-dashboard": "ダッシュボードの読み込みと初期化", "title-not-found": "ID{{panelId}}のパネルが見つかりません" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "テンプレートの変数" }, @@ -11043,6 +11047,12 @@ "could-anything-matching-query": "クエリに一致するものが見つかりませんでした" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "タイプをクリア", "select-aria-label": "パネルタイプフィルター", @@ -13216,6 +13226,7 @@ "create-team": { "create": "作成", "description-email": "これは任意であり、主にカスタムチームのアバターに使用されます", + "failed-to-create": "", "label-email": "メール", "label-name": "名前", "label-role": "ロール" @@ -13247,6 +13258,7 @@ "title-edit-team": "チームを編集", "tooltip-edit-team": "チームを編集" }, + "loading-teams": "", "new-team": "新しいチーム", "placeholder-search-teams": "チームを検索" }, diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index cf9773184cd..d17b3191e98 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -5374,6 +5374,10 @@ "loading-initializing-dashboard": "대시보드 로딩 및 초기화", "title-not-found": "ID가 {{panelId}}인 패널을 찾을 수 없음" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "템플릿 변수" }, @@ -11043,6 +11047,12 @@ "could-anything-matching-query": "쿼리와 일치하는 항목을 찾을 수 없습니다" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "유형 초기화", "select-aria-label": "패널 유형 필터", @@ -13216,6 +13226,7 @@ "create-team": { "create": "생성", "description-email": "이 항목은 선택 사항이며 주로 사용자 지정 팀 아바타를 허용하는 데 사용됩니다", + "failed-to-create": "", "label-email": "이메일", "label-name": "이름", "label-role": "역할" @@ -13247,6 +13258,7 @@ "title-edit-team": "팀 편집", "tooltip-edit-team": "팀 편집" }, + "loading-teams": "", "new-team": "새 팀", "placeholder-search-teams": "팀 검색" }, diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 8a7dcc3b2d0..5339e9cc1ba 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Dashboard laden en initialiseren", "title-not-found": "Paneel met id {{panelId}} niet gevonden" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Sjabloonvariabelen" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Kon niets vinden dat overeenkomt met je query" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Types wissen", "select-aria-label": "Paneeltypefilter", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Aanmaken", "description-email": "Dit is optioneel en wordt voornamelijk gebruikt voor het toestaan van aangepaste teamavatars", + "failed-to-create": "", "label-email": "E-mailadres", "label-name": "Naam", "label-role": "Rol" @@ -13304,6 +13315,7 @@ "title-edit-team": "Team bewerken", "tooltip-edit-team": "Team bewerken" }, + "loading-teams": "", "new-team": "Nieuw team", "placeholder-search-teams": "Teams zoeken" }, diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index d988de6fdee..9a90219b85f 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -5437,6 +5437,10 @@ "loading-initializing-dashboard": "Wczytywanie i inicjowanie pulpitu", "title-not-found": "Nie znaleziono panelu o identyfikatorze {{panelId}}" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Zmienne szablonu" }, @@ -11184,6 +11188,12 @@ "could-anything-matching-query": "Nie znaleziono wyników pasujących do Twojego zapytania" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Wyczyść typy", "select-aria-label": "Filtr typu panelu", @@ -13387,6 +13397,7 @@ "create-team": { "create": "Utwórz", "description-email": "Jest to ustawienie opcjonalne i służy przede wszystkim do zezwalania na niestandardowe awatary zespołu", + "failed-to-create": "", "label-email": "E-mail", "label-name": "Nazwa", "label-role": "Rola" @@ -13418,6 +13429,7 @@ "title-edit-team": "Edytuj zespół", "tooltip-edit-team": "Edytuj zespół" }, + "loading-teams": "", "new-team": "Nowy zespół", "placeholder-search-teams": "Szukaj zespołów" }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index e17ed3dade4..b3d6aa4c02f 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Carregando e inicializando o painel", "title-not-found": "Painel com ID {{panelId}} não encontrado" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Variáveis do modelo" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Não foi possível encontrar nada que corresponda à sua consulta" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Limpar tipos", "select-aria-label": "Filtro de tipo de painel", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Criar", "description-email": "Isso é opcional e é usado principalmente para permitir avatares de equipe personalizados", + "failed-to-create": "", "label-email": "E-mail", "label-name": "Nome", "label-role": "Função" @@ -13304,6 +13315,7 @@ "title-edit-team": "Editar equipe", "tooltip-edit-team": "Editar equipe" }, + "loading-teams": "", "new-team": "Nova equipe", "placeholder-search-teams": "Pesquisar equipes" }, diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 34cc8be7717..744661bf91c 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "A carregar e inicializar o painel de controlo", "title-not-found": "Painel com ID {{panelId}} não encontrado" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Variáveis do modelo" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Não foi possível encontrar nada que corresponda à sua consulta" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Limpar tipos", "select-aria-label": "Filtro do tipo de painel", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Criar", "description-email": "Isto é opcional e é utilizado principalmente para permitir avatares de equipa personalizados", + "failed-to-create": "", "label-email": "E-mail", "label-name": "Nome", "label-role": "Função" @@ -13304,6 +13315,7 @@ "title-edit-team": "Editar equipa", "tooltip-edit-team": "Editar equipa" }, + "loading-teams": "", "new-team": "Nova equipa", "placeholder-search-teams": "Pesquisar equipas" }, diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 3a8724d3098..0a2820e3e6c 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -5437,6 +5437,10 @@ "loading-initializing-dashboard": "Загрузка и инициализация дашборда", "title-not-found": "Панель с идентификатором {{panelId}} не найдена" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Переменные шаблона" }, @@ -11184,6 +11188,12 @@ "could-anything-matching-query": "Не удалось найти данные, соответствующие вашему запросу" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Очистить типы", "select-aria-label": "Фильтр типов панелей", @@ -13387,6 +13397,7 @@ "create-team": { "create": "Создать", "description-email": "Это необязательное действие, которое используется в основном для создания пользовательских аватаров команд.", + "failed-to-create": "", "label-email": "Адрес электронной почты", "label-name": "Имя", "label-role": "Роль" @@ -13418,6 +13429,7 @@ "title-edit-team": "Редактирование команды", "tooltip-edit-team": "Редактировать команду" }, + "loading-teams": "", "new-team": "Новая команда", "placeholder-search-teams": "Поиск команд" }, diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index c0bd748a996..b7b1cdf6cfa 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Laddar och initierar instrumentpanel", "title-not-found": "Panel med ID {{panelId}} hittades inte" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Mallvariabler" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Kunde inte hitta något som matchar din fråga" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Rensa typer", "select-aria-label": "Filter för paneltyp", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Skapa", "description-email": "Detta är valfritt och används främst för att tillåta anpassade teamavatarer", + "failed-to-create": "", "label-email": "E-post", "label-name": "Namn", "label-role": "Roll" @@ -13304,6 +13315,7 @@ "title-edit-team": "Redigera team", "tooltip-edit-team": "Redigera team" }, + "loading-teams": "", "new-team": "Nytt team", "placeholder-search-teams": "Sök team" }, diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index cfeb21bd5aa..6c49826ac02 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -5395,6 +5395,10 @@ "loading-initializing-dashboard": "Pano yükleniyor ve başlatılıyor", "title-not-found": "{{panelId}} kimliğine sahip panel bulunamadı" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "Şablon değişkenleri" }, @@ -11090,6 +11094,12 @@ "could-anything-matching-query": "Sorgunuzla eşleşen hiçbir şey bulunamadı" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "Türleri temizle", "select-aria-label": "Panel tipi filtresi", @@ -13273,6 +13283,7 @@ "create-team": { "create": "Oluştur", "description-email": "Bu isteğe bağlıdır ve esas olarak özel takım avatarlarına izin vermek için kullanılır", + "failed-to-create": "", "label-email": "E-posta", "label-name": "Ad", "label-role": "Rol" @@ -13304,6 +13315,7 @@ "title-edit-team": "Ekibi düzenle", "tooltip-edit-team": "Ekibi düzenle" }, + "loading-teams": "", "new-team": "Yeni ekip", "placeholder-search-teams": "Ekip ara" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index e96f271b082..7168af3ce09 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -5374,6 +5374,10 @@ "loading-initializing-dashboard": "正在加载和初始化数据面板", "title-not-found": "未找到 ID 为 {{panelId}} 的面板" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "模板变量" }, @@ -11043,6 +11047,12 @@ "could-anything-matching-query": "找不到与您的查询匹配的内容" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "清除类型", "select-aria-label": "面板类型筛选器", @@ -13216,6 +13226,7 @@ "create-team": { "create": "创建", "description-email": "这是可选项,主要用于允许自定义团队头像", + "failed-to-create": "", "label-email": "电子邮箱", "label-name": "名称", "label-role": "角色" @@ -13247,6 +13258,7 @@ "title-edit-team": "编辑团队", "tooltip-edit-team": "编辑团队" }, + "loading-teams": "", "new-team": "新建团队", "placeholder-search-teams": "搜索团队" }, diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 561d6b53c3a..dddcce4ef55 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -5374,6 +5374,10 @@ "loading-initializing-dashboard": "正在載入並初始化儀表板", "title-not-found": "未找到 ID 為 {{panelId}} 的面板" }, + "source-link": { + "title": "", + "tooltip": "" + }, "sub-menu-un-connected": { "aria-label-template-variables": "範本變數" }, @@ -11043,6 +11047,12 @@ "could-anything-matching-query": "找不到符合您查詢的內容" } }, + "panel-group-by": { + "button": "", + "loading": "", + "no-options": "", + "search-placeholder": "" + }, "panel-type-filter": { "clear-button": "清除類型", "select-aria-label": "面板類型篩選器", @@ -13216,6 +13226,7 @@ "create-team": { "create": "建立", "description-email": "這是可選項目,主要用於允許自訂團隊頭像", + "failed-to-create": "", "label-email": "電子郵件", "label-name": "名稱", "label-role": "角色" @@ -13247,6 +13258,7 @@ "title-edit-team": "編輯團隊", "tooltip-edit-team": "編輯團隊" }, + "loading-teams": "", "new-team": "新團隊", "placeholder-search-teams": "搜尋團隊" }, From 3b481f8687afa842508d77595d1c4c0baaeacb3d Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Sun, 30 Nov 2025 07:53:19 -0600 Subject: [PATCH 204/378] Docs: Add dashboard dto information (#114459) --------- Co-authored-by: Anna Urbiztondo --- .../api-reference/http-api/dashboard.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/sources/developer-resources/api-reference/http-api/dashboard.md b/docs/sources/developer-resources/api-reference/http-api/dashboard.md index 2cc7aa84bb3..abef6a8f5e6 100644 --- a/docs/sources/developer-resources/api-reference/http-api/dashboard.md +++ b/docs/sources/developer-resources/api-reference/http-api/dashboard.md @@ -628,6 +628,14 @@ Status Codes: - **403** – Access denied - **404** – Not Found +### Retrieve additional access information + +`GET /apis/dashboard.grafana.app/v1beta1/namespaces/:namespace/dashboards/:uid/dto` + +Retrieves a dashboard with additional access information. + +The `GET` response includes an additional `access` section with data such as if it's a public dashboard, or the dashboard permissions (admin, editor) of the user who made the request. + ## List Dashboards `GET /apis/dashboard.grafana.app/v1beta1/namespaces/:namespace/dashboards` From db9afe31e46c52cfbc93f80042c7fa0cb7a2396b Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Sun, 30 Nov 2025 23:24:03 -0600 Subject: [PATCH 205/378] Provisioning: Fix panic on watcher when channel is closed (#114439) --- .../pkg/repository/local/watch.go | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/provisioning/pkg/repository/local/watch.go b/apps/provisioning/pkg/repository/local/watch.go index 91caed9a1d3..6ad3fdbaa31 100644 --- a/apps/provisioning/pkg/repository/local/watch.go +++ b/apps/provisioning/pkg/repository/local/watch.go @@ -28,6 +28,7 @@ type fileWatcher struct { timers map[string]*time.Timer watcher *fsnotify.Watcher logger logging.Logger + closed bool } // File watcher that buffers events for 100ms before actually firing them @@ -77,22 +78,21 @@ func NewFileWatcher(path string, accept func(string) bool) (FileWatcher, error) // Keep watching for changes until the context is done func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) { + defer f.cleanup(events) + for { select { case <-ctx.Done(): - close(events) return case _, ok := <-f.watcher.Errors: if !ok { // Channel was closed (i.e. Watcher.Close() was called). - close(events) return } // Read from Events. case e, ok := <-f.watcher.Events: if !ok { // Channel was closed (i.e. Watcher.Close() was called). - close(events) return } name := filepath.Base(e.Name) @@ -114,6 +114,11 @@ func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) { if !ok { nameCopy := e.Name t = time.AfterFunc(math.MaxInt64, func() { + // before sending the event, check if the watcher has been closed + if f.closed { + return + } + path, _ := strings.CutPrefix(nameCopy, f.prefix) events <- path @@ -128,3 +133,17 @@ func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) { } } } + +// stop all pending timers and close the event channel +func (f *fileWatcher) cleanup(events chan<- string) { + f.timersMu.Lock() + defer f.timersMu.Unlock() + + for _, timer := range f.timers { + timer.Stop() + } + f.timers = make(map[string]*time.Timer) + + close(events) + f.closed = true +} From aaa5d02a3e45f89605b1223cf6dc8f349fbce442 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Mon, 1 Dec 2025 01:29:04 -0700 Subject: [PATCH 206/378] AuthZ: Set span errors (#114460) --- pkg/services/authz/zanzana/server/server_batch_check.go | 9 +++++++++ pkg/services/authz/zanzana/server/server_check.go | 3 +++ pkg/services/authz/zanzana/server/server_list.go | 3 +++ pkg/services/authz/zanzana/server/server_mutate.go | 3 +++ pkg/services/authz/zanzana/server/server_query.go | 3 +++ pkg/services/authz/zanzana/server/server_read.go | 3 +++ pkg/services/authz/zanzana/server/server_write.go | 3 +++ 7 files changed, 27 insertions(+) diff --git a/pkg/services/authz/zanzana/server/server_batch_check.go b/pkg/services/authz/zanzana/server/server_batch_check.go index ecc86712950..62994c58973 100644 --- a/pkg/services/authz/zanzana/server/server_batch_check.go +++ b/pkg/services/authz/zanzana/server/server_batch_check.go @@ -5,6 +5,7 @@ import ( authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" + "go.opentelemetry.io/otel/codes" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" @@ -15,6 +16,8 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest defer span.End() if err := authorize(ctx, r.GetNamespace(), s.cfg); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return nil, err } @@ -24,11 +27,15 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest store, err := s.getStoreInfo(ctx, r.GetNamespace()) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return nil, err } contextuals, err := s.getContextuals(r.GetSubject()) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return nil, err } @@ -37,6 +44,8 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest for _, item := range r.GetItems() { res, err := s.batchCheckItem(ctx, r, item, contextuals, store, groupResourceAccess) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return nil, err } diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index e9712b6fe21..d8c1a2ec0c9 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -9,6 +9,7 @@ import ( authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "google.golang.org/protobuf/types/known/structpb" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" @@ -25,6 +26,8 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C res, err := s.check(ctx, r) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) s.logger.Error("failed to perform check request", "error", err, "namespace", r.GetNamespace()) return nil, errors.New("failed to perform check request") } diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index a5fa19e3896..46f7a9167ad 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -13,6 +13,7 @@ import ( authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" ) @@ -28,6 +29,8 @@ func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.Lis res, err := s.list(ctx, r) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) s.logger.Error("failed to perform list request", "error", err, "namespace", r.GetNamespace()) return nil, errors.New("failed to perform list request") } diff --git a/pkg/services/authz/zanzana/server/server_mutate.go b/pkg/services/authz/zanzana/server/server_mutate.go index cdd8c5b2f50..bd339534fe5 100644 --- a/pkg/services/authz/zanzana/server/server_mutate.go +++ b/pkg/services/authz/zanzana/server/server_mutate.go @@ -7,6 +7,7 @@ import ( "time" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "go.opentelemetry.io/otel/codes" ) type OperationGroup string @@ -30,6 +31,8 @@ func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au res, err := s.mutate(ctx, req) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) s.logger.Error("failed to perform mutate request", "error", err, "namespace", req.GetNamespace()) return nil, errors.New("failed to perform mutate request") } diff --git a/pkg/services/authz/zanzana/server/server_query.go b/pkg/services/authz/zanzana/server/server_query.go index 596c3805a5a..585c64f8ddd 100644 --- a/pkg/services/authz/zanzana/server/server_query.go +++ b/pkg/services/authz/zanzana/server/server_query.go @@ -8,6 +8,7 @@ import ( "time" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "go.opentelemetry.io/otel/codes" ) func (s *Server) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) { @@ -20,6 +21,8 @@ func (s *Server) Query(ctx context.Context, req *authzextv1.QueryRequest) (*auth res, err := s.query(ctx, req) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) s.logger.Error("failed to perform query request", "error", err, "namespace", req.GetNamespace()) return nil, errors.New("failed to perform query request") } diff --git a/pkg/services/authz/zanzana/server/server_read.go b/pkg/services/authz/zanzana/server/server_read.go index d78d2fcbc0b..07cb7902b1a 100644 --- a/pkg/services/authz/zanzana/server/server_read.go +++ b/pkg/services/authz/zanzana/server/server_read.go @@ -7,6 +7,7 @@ import ( "time" openfgav1 "github.com/openfga/api/proto/openfga/v1" + "go.opentelemetry.io/otel/codes" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" @@ -22,6 +23,8 @@ func (s *Server) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authze res, err := s.read(ctx, req) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) s.logger.Error("failed to perform read request", "error", err, "namespace", req.GetNamespace()) return nil, errors.New("failed to perform read request") } diff --git a/pkg/services/authz/zanzana/server/server_write.go b/pkg/services/authz/zanzana/server/server_write.go index 2f7df49d688..d16d706a320 100644 --- a/pkg/services/authz/zanzana/server/server_write.go +++ b/pkg/services/authz/zanzana/server/server_write.go @@ -7,6 +7,7 @@ import ( "time" openfgav1 "github.com/openfga/api/proto/openfga/v1" + "go.opentelemetry.io/otel/codes" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana/common" @@ -22,6 +23,8 @@ func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*auth res, err := s.write(ctx, req) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) s.logger.Error("failed to perform write request", "error", err, "namespace", req.GetNamespace()) return nil, errors.New("failed to perform write request") } From dd77107ed4007816f85a84fbf8ff6ecdc217e1e6 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Mon, 1 Dec 2025 09:25:57 +0000 Subject: [PATCH 207/378] Folders: Add additional query param to legacy API for tracking purposes (#114505) --- public/app/api/clients/folder/v1beta1/hooks.ts | 17 +++++++++++++---- .../api/browseDashboardsAPI.ts | 14 +++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/public/app/api/clients/folder/v1beta1/hooks.ts b/public/app/api/clients/folder/v1beta1/hooks.ts index 1793818bdd6..1fea62c40d0 100644 --- a/public/app/api/clients/folder/v1beta1/hooks.ts +++ b/public/app/api/clients/folder/v1beta1/hooks.ts @@ -103,10 +103,17 @@ const combineFolderResponses = ( export async function getFolderByUidFacade(uid: string): Promise { const isVirtualFolder = uid && [GENERAL_FOLDER_UID, config.sharedWithMeFolderUID].includes(uid); - // We need the legacy API call regardless, for now - const legacyApiCall = dispatch(browseDashboardsAPI.endpoints.getFolder.initiate(uid)); - const shouldUseAppPlatformAPI = Boolean(config.featureToggles.foldersAppPlatformAPI); + + // We need the legacy API call regardless, for now + const legacyApiCall = dispatch( + browseDashboardsAPI.endpoints.getFolder.initiate({ + folderUID: uid, + accesscontrol: true, + isLegacyCall: shouldUseAppPlatformAPI, + }) + ); + if (shouldUseAppPlatformAPI) { let virtualFolderResponse; if (isVirtualFolder) { @@ -165,7 +172,9 @@ export function useGetFolderQueryFacade(uid?: string) { // This may look weird that we call the legacy folder anyway all the time, but the issue is we don't have good API // for the access control metadata yet, and so we still take it from the old api. // see https://github.com/grafana/identity-access-team/issues/1103 - const legacyFolderResult = useGetFolderQueryLegacy(uid || skipToken); + const legacyFolderResult = useGetFolderQueryLegacy( + uid ? { folderUID: uid, accesscontrol: true, isLegacyCall: true } : skipToken + ); let resultFolder = useGetFolderQuery(shouldUseAppPlatformAPI && !isVirtualFolder ? params : skipToken); // We get parents and folders for virtual folders too. Parents should just return empty array but it's easier to // stitch the responses this way and access can actually return different response based on the grafana setup. diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 51ff4423bc8..90d255cead9 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -94,9 +94,17 @@ export const browseDashboardsAPI = createApi({ }), // get folder info (e.g. title, parents) but *not* children - getFolder: builder.query({ - providesTags: (_result, _error, folderUID) => [{ type: 'getFolder', id: folderUID }], - query: (folderUID) => ({ url: `/folders/${folderUID}`, params: { accesscontrol: true } }), + getFolder: builder.query({ + providesTags: (_result, _error, { folderUID }) => [{ type: 'getFolder', id: folderUID }], + query: ({ folderUID, accesscontrol, isLegacyCall }) => ({ + url: `/folders/${folderUID}`, + params: { + accesscontrol, + // Add additional query param so we can tell when + // this was called for app platform compatibility purposes vs. actually needing to use the legacy API + isLegacyCall, + }, + }), }), // create a new folder From 2da171595a17ce21067da4d419013d05e1cf3264 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 1 Dec 2025 09:51:11 +0000 Subject: [PATCH 208/378] FieldColor: Add accessible color palettes (#114424) * add viridis and others, allow passing interpolator directly to FieldColorSchemeMode * fix bug * be more defensive when getting the fieldcolor * backend changes * add d3-scale-chromatic to list of esModules --- .../kinds/v2alpha1/dashboard_spec.cue | 13 ++-- .../kinds/v2beta1/dashboard_spec.cue | 15 ++-- .../dashboard/v0alpha1/dashboard_kind.cue | 17 +++-- .../apis/dashboard/v1beta1/dashboard_kind.cue | 17 +++-- .../dashboard/v2alpha1/dashboard_spec.cue | 13 ++-- .../dashboard/v2alpha1/dashboard_spec_gen.go | 12 +++- .../apis/dashboard/v2beta1/dashboard_spec.cue | 15 ++-- .../dashboard/v2beta1/dashboard_spec_gen.go | 12 +++- kinds/dashboard/dashboard_kind.cue | 17 +++-- packages/grafana-data/package.json | 1 + packages/grafana-data/src/field/fieldColor.ts | 70 +++++++++++++++++-- packages/grafana-data/src/types/fieldColor.ts | 5 ++ packages/grafana-plugin-configs/jest/utils.js | 1 + .../raw/dashboard/x/dashboard_types.gen.ts | 12 +++- .../dashboard/v2alpha0/dashboard.schema.cue | 9 ++- .../schema/dashboard/v2alpha0/types.gen.ts | 11 ++- .../dashboard/v2alpha1/types.spec.gen.ts | 9 ++- .../dashboard/v2beta1/types.spec.gen.ts | 9 ++- pkg/kinds/dashboard/dashboard_spec_gen.go | 12 +++- yarn.lock | 1 + 20 files changed, 217 insertions(+), 54 deletions(-) diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 5768c358c24..df99f88581c 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -340,7 +340,12 @@ ValueMappingResult: { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -352,7 +357,7 @@ ValueMappingResult: { // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. FieldColorSeriesByMode: "min" | "max" | "last" @@ -377,7 +382,7 @@ FetchOptions: { url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -387,7 +392,7 @@ InfinityOptions: FetchOptions & { datasourceUid: string } -HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" +HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" // Action variable type ActionVariableType: "string" diff --git a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue index 426fb179fd8..b81175db77d 100644 --- a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue @@ -113,7 +113,7 @@ DashboardLink: { placement?: DashboardLinkPlacement } -// Dashboard Link placement. Defines where the link should be displayed. +// Dashboard Link placement. Defines where the link should be displayed. // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu DashboardLinkPlacement: "inControlsMenu" @@ -342,7 +342,12 @@ ValueMappingResult: { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -354,7 +359,7 @@ ValueMappingResult: { // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. FieldColorSeriesByMode: "min" | "max" | "last" @@ -379,7 +384,7 @@ FetchOptions: { url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -389,7 +394,7 @@ InfinityOptions: FetchOptions & { datasourceUid: string } -HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" +HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" // Action variable type ActionVariableType: "string" diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue index 9222dad2bc7..d2a65bbbf24 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue @@ -301,8 +301,8 @@ lineage: schemas: [{ // Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) #DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type") - // Dashboard Link placement. Defines where the link should be displayed. - // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu + // Dashboard Link placement. Defines where the link should be displayed. + // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu #DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type") // Annotation Query placement. Defines where the annotation query should be displayed. @@ -318,7 +318,7 @@ lineage: schemas: [{ url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining this way because we can't generate a go struct that + // We are defining this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -330,7 +330,7 @@ lineage: schemas: [{ url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -381,7 +381,12 @@ lineage: schemas: [{ // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations - // `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode + // `continuous-viridis`: Continuous Viridis palette mode + // `continuous-magma`: Continuous Magma palette mode + // `continuous-plasma`: Continuous Plasma palette mode + // `continuous-inferno`: Continuous Inferno palette mode + // `continuous-cividis`: Continuous Cividis palette mode + // `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -393,7 +398,7 @@ lineage: schemas: [{ // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. - #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) + #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousViridis|ContinuousMagma|ContinuousPlasma|ContinuousInferno|ContinuousCividis|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue index 9222dad2bc7..d2a65bbbf24 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue @@ -301,8 +301,8 @@ lineage: schemas: [{ // Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) #DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type") - // Dashboard Link placement. Defines where the link should be displayed. - // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu + // Dashboard Link placement. Defines where the link should be displayed. + // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu #DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type") // Annotation Query placement. Defines where the annotation query should be displayed. @@ -318,7 +318,7 @@ lineage: schemas: [{ url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining this way because we can't generate a go struct that + // We are defining this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -330,7 +330,7 @@ lineage: schemas: [{ url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -381,7 +381,12 @@ lineage: schemas: [{ // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations - // `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode + // `continuous-viridis`: Continuous Viridis palette mode + // `continuous-magma`: Continuous Magma palette mode + // `continuous-plasma`: Continuous Plasma palette mode + // `continuous-inferno`: Continuous Inferno palette mode + // `continuous-cividis`: Continuous Cividis palette mode + // `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -393,7 +398,7 @@ lineage: schemas: [{ // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. - #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) + #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousViridis|ContinuousMagma|ContinuousPlasma|ContinuousInferno|ContinuousCividis|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index 9a8a621345f..b9e8420d2bb 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -344,7 +344,12 @@ ValueMappingResult: { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -356,7 +361,7 @@ ValueMappingResult: { // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. FieldColorSeriesByMode: "min" | "max" | "last" @@ -381,7 +386,7 @@ FetchOptions: { url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -391,7 +396,7 @@ InfinityOptions: FetchOptions & { datasourceUid: string } -HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" +HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" // Action variable type ActionVariableType: "string" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index b83cbbd6fb3..e095d7f6c7b 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -583,7 +583,12 @@ func NewDashboardFieldColor() *DashboardFieldColor { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -602,6 +607,11 @@ const ( DashboardFieldColorModeIdThresholds DashboardFieldColorModeId = "thresholds" DashboardFieldColorModeIdPaletteClassic DashboardFieldColorModeId = "palette-classic" DashboardFieldColorModeIdPaletteClassicByName DashboardFieldColorModeId = "palette-classic-by-name" + DashboardFieldColorModeIdContinuousViridis DashboardFieldColorModeId = "continuous-viridis" + DashboardFieldColorModeIdContinuousMagma DashboardFieldColorModeId = "continuous-magma" + DashboardFieldColorModeIdContinuousPlasma DashboardFieldColorModeId = "continuous-plasma" + DashboardFieldColorModeIdContinuousInferno DashboardFieldColorModeId = "continuous-inferno" + DashboardFieldColorModeIdContinuousCividis DashboardFieldColorModeId = "continuous-cividis" DashboardFieldColorModeIdContinuousGrYlRd DashboardFieldColorModeId = "continuous-GrYlRd" DashboardFieldColorModeIdContinuousRdYlGr DashboardFieldColorModeId = "continuous-RdYlGr" DashboardFieldColorModeIdContinuousBlYlRd DashboardFieldColorModeId = "continuous-BlYlRd" diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue index d9df0dd3ee3..b2340d5c22e 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue @@ -117,7 +117,7 @@ DashboardLink: { placement?: DashboardLinkPlacement } -// Dashboard Link placement. Defines where the link should be displayed. +// Dashboard Link placement. Defines where the link should be displayed. // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu DashboardLinkPlacement: "inControlsMenu" @@ -346,7 +346,12 @@ ValueMappingResult: { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -358,7 +363,7 @@ ValueMappingResult: { // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. FieldColorSeriesByMode: "min" | "max" | "last" @@ -383,7 +388,7 @@ FetchOptions: { url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -393,7 +398,7 @@ InfinityOptions: FetchOptions & { datasourceUid: string } -HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" +HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" // Action variable type ActionVariableType: "string" diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go index fc0a8fcf26f..ffdad0045c6 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go @@ -587,7 +587,12 @@ func NewDashboardFieldColor() *DashboardFieldColor { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -606,6 +611,11 @@ const ( DashboardFieldColorModeIdThresholds DashboardFieldColorModeId = "thresholds" DashboardFieldColorModeIdPaletteClassic DashboardFieldColorModeId = "palette-classic" DashboardFieldColorModeIdPaletteClassicByName DashboardFieldColorModeId = "palette-classic-by-name" + DashboardFieldColorModeIdContinuousViridis DashboardFieldColorModeId = "continuous-viridis" + DashboardFieldColorModeIdContinuousMagma DashboardFieldColorModeId = "continuous-magma" + DashboardFieldColorModeIdContinuousPlasma DashboardFieldColorModeId = "continuous-plasma" + DashboardFieldColorModeIdContinuousInferno DashboardFieldColorModeId = "continuous-inferno" + DashboardFieldColorModeIdContinuousCividis DashboardFieldColorModeId = "continuous-cividis" DashboardFieldColorModeIdContinuousGrYlRd DashboardFieldColorModeId = "continuous-GrYlRd" DashboardFieldColorModeIdContinuousRdYlGr DashboardFieldColorModeId = "continuous-RdYlGr" DashboardFieldColorModeIdContinuousBlYlRd DashboardFieldColorModeId = "continuous-BlYlRd" diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index ce2510d2cfe..346edbb4753 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -297,8 +297,8 @@ lineage: schemas: [{ // Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) #DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type") - // Dashboard Link placement. Defines where the link should be displayed. - // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu + // Dashboard Link placement. Defines where the link should be displayed. + // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu #DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type") // Annotation Query placement. Defines where the annotation query should be displayed. @@ -314,7 +314,7 @@ lineage: schemas: [{ url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining this way because we can't generate a go struct that + // We are defining this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -326,7 +326,7 @@ lineage: schemas: [{ url: string body?: string // These are 2D arrays of strings, each representing a key-value pair - // We are defining them this way because we can't generate a go struct that + // We are defining them this way because we can't generate a go struct that // that would have exactly two strings in each sub-array queryParams?: [...[...string]] headers?: [...[...string]] @@ -377,7 +377,12 @@ lineage: schemas: [{ // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations - // `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode + // `continuous-viridis`: Continuous Viridis palette mode + // `continuous-magma`: Continuous Magma palette mode + // `continuous-plasma`: Continuous Plasma palette mode + // `continuous-inferno`: Continuous Inferno palette mode + // `continuous-cividis`: Continuous Cividis palette mode + // `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -389,7 +394,7 @@ lineage: schemas: [{ // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. - #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) + #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousViridis|ContinuousMagma|ContinuousPlasma|ContinuousInferno|ContinuousCividis|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 59d3c58a042..ff90b2c295d 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -63,6 +63,7 @@ "@types/string-hash": "1.1.3", "@types/systemjs": "6.15.3", "d3-interpolate": "3.0.1", + "d3-scale-chromatic": "3.1.0", "date-fns": "4.1.0", "dompurify": "3.3.0", "eventemitter3": "5.0.1", diff --git a/packages/grafana-data/src/field/fieldColor.ts b/packages/grafana-data/src/field/fieldColor.ts index a7957f9f44e..7016c208994 100644 --- a/packages/grafana-data/src/field/fieldColor.ts +++ b/packages/grafana-data/src/field/fieldColor.ts @@ -1,4 +1,11 @@ import { interpolateRgbBasis } from 'd3-interpolate'; +import { + interpolateViridis, + interpolateMagma, + interpolatePlasma, + interpolateInferno, + interpolateCividis, +} from 'd3-scale-chromatic'; import stringHash from 'string-hash'; import tinycolor from 'tinycolor2'; @@ -75,6 +82,41 @@ export const fieldColorModeRegistry = new Registry(() => { ); }, }), + new FieldColorSchemeMode({ + id: FieldColorModeId.ContinuousViridis, + name: 'Viridis', + isContinuous: true, + isByValue: true, + interpolator: interpolateViridis, + }), + new FieldColorSchemeMode({ + id: FieldColorModeId.ContinuousMagma, + name: 'Magma', + isContinuous: true, + isByValue: true, + interpolator: interpolateMagma, + }), + new FieldColorSchemeMode({ + id: FieldColorModeId.ContinuousPlasma, + name: 'Plasma', + isContinuous: true, + isByValue: true, + interpolator: interpolatePlasma, + }), + new FieldColorSchemeMode({ + id: FieldColorModeId.ContinuousInferno, + name: 'Inferno', + isContinuous: true, + isByValue: true, + interpolator: interpolateInferno, + }), + new FieldColorSchemeMode({ + id: FieldColorModeId.ContinuousCividis, + name: 'Cividis', + isContinuous: true, + isByValue: true, + interpolator: interpolateCividis, + }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousGrYlRd, name: 'Green-Yellow-Red', @@ -148,16 +190,27 @@ export const fieldColorModeRegistry = new Registry(() => { ]; }); -interface FieldColorSchemeModeOptions { +interface BaseFieldColorSchemeModeOptions { id: FieldColorModeId; name: string; description?: string; - getColors: (theme: GrafanaTheme2) => string[]; isContinuous: boolean; isByValue: boolean; useSeriesName?: boolean; } +interface FieldColorSchemeModeInterpolator extends BaseFieldColorSchemeModeOptions { + interpolator: (value: number) => string; + getColors?: never; +} + +interface FieldColorSchemeModeGetColors extends BaseFieldColorSchemeModeOptions { + getColors: (theme: GrafanaTheme2) => string[]; + interpolator?: never; +} + +type FieldColorSchemeModeOptions = FieldColorSchemeModeGetColors | FieldColorSchemeModeInterpolator; + export class FieldColorSchemeMode implements FieldColorMode { id: FieldColorModeId; name: string; @@ -178,11 +231,15 @@ export class FieldColorSchemeMode implements FieldColorMode { this.isContinuous = options.isContinuous; this.isByValue = options.isByValue; this.useSeriesName = options.useSeriesName; + this.interpolator = options.interpolator; } getColors(theme: GrafanaTheme2): string[] { if (!this.getNamedColors) { - return []; + if (!this.interpolator) { + return []; + } + this.getNamedColors = () => new Array(9).fill(0).map((_, i) => this.getInterpolator()(i / 8)); } if (this.colorCache && this.colorCacheTheme === theme) { @@ -231,12 +288,15 @@ export class FieldColorSchemeMode implements FieldColorMode { /** @beta */ export function getFieldColorModeForField(field: Field): FieldColorMode { - return fieldColorModeRegistry.get(field.config.color?.mode ?? FieldColorModeId.Thresholds); + return ( + fieldColorModeRegistry.getIfExists(field.config.color?.mode) ?? + fieldColorModeRegistry.get(FieldColorModeId.Thresholds) + ); } /** @beta */ export function getFieldColorMode(mode?: FieldColorModeId | string): FieldColorMode { - return fieldColorModeRegistry.get(mode ?? FieldColorModeId.Thresholds); + return fieldColorModeRegistry.getIfExists(mode) ?? fieldColorModeRegistry.get(FieldColorModeId.Thresholds); } /** diff --git a/packages/grafana-data/src/types/fieldColor.ts b/packages/grafana-data/src/types/fieldColor.ts index 26d5cc2a662..7b07abfb337 100644 --- a/packages/grafana-data/src/types/fieldColor.ts +++ b/packages/grafana-data/src/types/fieldColor.ts @@ -16,6 +16,11 @@ export enum FieldColorModeId { ContinuousReds = 'continuous-reds', ContinuousGreens = 'continuous-greens', ContinuousPurples = 'continuous-purples', + ContinuousViridis = 'continuous-viridis', + ContinuousMagma = 'continuous-magma', + ContinuousPlasma = 'continuous-plasma', + ContinuousInferno = 'continuous-inferno', + ContinuousCividis = 'continuous-cividis', Fixed = 'fixed', Shades = 'shades', } diff --git a/packages/grafana-plugin-configs/jest/utils.js b/packages/grafana-plugin-configs/jest/utils.js index 439617819bb..1d9da1c50ce 100644 --- a/packages/grafana-plugin-configs/jest/utils.js +++ b/packages/grafana-plugin-configs/jest/utils.js @@ -8,6 +8,7 @@ export const grafanaESModules = [ 'd3', 'd3-color', 'd3-interpolate', + 'd3-scale-chromatic', 'delaunator', 'get-user-locale', 'internmap', diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index 3466df584b8..c6a722cbae7 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -475,7 +475,12 @@ export type VariableType = ('query' | 'adhoc' | 'groupby' | 'constant' | 'dataso * `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold * `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations * `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations - * `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode + * `continuous-viridis`: Continuous Viridis palette mode + * `continuous-magma`: Continuous Magma palette mode + * `continuous-plasma`: Continuous Plasma palette mode + * `continuous-inferno`: Continuous Inferno palette mode + * `continuous-cividis`: Continuous Cividis palette mode + * `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode * `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode * `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode * `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -492,11 +497,16 @@ export enum FieldColorModeId { ContinuousBlPu = 'continuous-BlPu', ContinuousBlYlRd = 'continuous-BlYlRd', ContinuousBlues = 'continuous-blues', + ContinuousCividis = 'continuous-cividis', ContinuousGrYlRd = 'continuous-GrYlRd', ContinuousGreens = 'continuous-greens', + ContinuousInferno = 'continuous-inferno', + ContinuousMagma = 'continuous-magma', + ContinuousPlasma = 'continuous-plasma', ContinuousPurples = 'continuous-purples', ContinuousRdYlGr = 'continuous-RdYlGr', ContinuousReds = 'continuous-reds', + ContinuousViridis = 'continuous-viridis', ContinuousYlBl = 'continuous-YlBl', ContinuousYlRd = 'continuous-YlRd', Fixed = 'fixed', diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index ba637656ed8..052e626cdfc 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -334,7 +334,12 @@ ValueMappingResult: { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -346,7 +351,7 @@ ValueMappingResult: { // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. FieldColorSeriesByMode: "min" | "max" | "last" diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index d38a6693ac9..34af3332332 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -327,7 +327,7 @@ export interface FieldConfig { description?: string; // An explicit path to the field in the datasource. When the frame meta includes a path, // This will default to `${frame.meta.path}/${field.name} - // + // // When defined, this value can be used as an identifier within the datasource scope, and // may be used to update the results path?: string; @@ -529,7 +529,12 @@ export const defaultFieldColor = (): FieldColor => ({ // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -541,7 +546,7 @@ export const defaultFieldColor = (): FieldColor => ({ // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; +export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds"); diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 060a0e3945f..b264bce9e18 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -489,7 +489,12 @@ export const defaultFieldColor = (): FieldColor => ({ // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -501,7 +506,7 @@ export const defaultFieldColor = (): FieldColor => ({ // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; +export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds"); diff --git a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts index 00e9111453a..95c0dac4230 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts @@ -496,7 +496,12 @@ export const defaultFieldColor = (): FieldColor => ({ // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -508,7 +513,7 @@ export const defaultFieldColor = (): FieldColor => ({ // `continuous-purples`: Continuous Purple palette mode // `shades`: Shades of a single color. Specify a single color, useful in an override rule. // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. -export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; +export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds"); diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index 786fabd8de8..fd8dbae7b77 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -619,7 +619,12 @@ func NewFieldColor() *FieldColor { // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations -// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-viridis`: Continuous Viridis palette mode +// `continuous-magma`: Continuous Magma palette mode +// `continuous-plasma`: Continuous Plasma palette mode +// `continuous-inferno`: Continuous Inferno palette mode +// `continuous-cividis`: Continuous Cividis palette mode +// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode // `continuous-YlRd`: Continuous Yellow-Red palette mode @@ -637,6 +642,11 @@ const ( FieldColorModeIdThresholds FieldColorModeId = "thresholds" FieldColorModeIdPaletteClassic FieldColorModeId = "palette-classic" FieldColorModeIdPaletteClassicByName FieldColorModeId = "palette-classic-by-name" + FieldColorModeIdContinuousViridis FieldColorModeId = "continuous-viridis" + FieldColorModeIdContinuousMagma FieldColorModeId = "continuous-magma" + FieldColorModeIdContinuousPlasma FieldColorModeId = "continuous-plasma" + FieldColorModeIdContinuousInferno FieldColorModeId = "continuous-inferno" + FieldColorModeIdContinuousCividis FieldColorModeId = "continuous-cividis" FieldColorModeIdContinuousGrYlRd FieldColorModeId = "continuous-GrYlRd" FieldColorModeIdContinuousRdYlGr FieldColorModeId = "continuous-RdYlGr" FieldColorModeIdContinuousBlYlRd FieldColorModeId = "continuous-BlYlRd" diff --git a/yarn.lock b/yarn.lock index abe69dda8b7..356d0eaacf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3122,6 +3122,7 @@ __metadata: "@types/systemjs": "npm:6.15.3" "@types/tinycolor2": "npm:1.4.6" d3-interpolate: "npm:3.0.1" + d3-scale-chromatic: "npm:3.1.0" date-fns: "npm:4.1.0" dompurify: "npm:3.3.0" esbuild: "npm:0.25.8" From 7fce2d9516c59b410d1862e370f8f0ed27dd38c6 Mon Sep 17 00:00:00 2001 From: Rafael Bortolon Paulovic Date: Mon, 1 Dec 2025 11:30:22 +0100 Subject: [PATCH 209/378] fix(unified): set SQLite path in cfg for reusing shared DB (#114580) --- pkg/tests/testinfra/testinfra.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index 25b2d4b177b..cb38d0cd768 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -107,6 +107,9 @@ func StartGrafanaEnvWithDB(t *testing.T, grafDir, cfgPath string) (string, *serv dbCfg.Key("user").SetValue(testDB.User) dbCfg.Key("password").SetValue(testDB.Password) dbCfg.Key("name").SetValue(testDB.Database) + if testDB.Path != "" { + dbCfg.Key("path").SetValue(testDB.Path) + } t.Log("Using test database", "type", testDB.DriverName, "host", testDB.Host, "port", testDB.Port, "user", testDB.User, "name", testDB.Database, "path", testDB.Path) From 32b9bebc75f4f5aca9956d12f7d6fa8596dbdcd7 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Mon, 1 Dec 2025 11:17:28 +0000 Subject: [PATCH 210/378] Swagger: Load embedded icons from CDN (#114632) --- pkg/api/swagger.go | 6 ++---- public/swagger/SwaggerPage.tsx | 5 ++--- public/views/swagger.html | 10 +++++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pkg/api/swagger.go b/pkg/api/swagger.go index 6e34efdf82a..1cc1ba4447d 100644 --- a/pkg/api/swagger.go +++ b/pkg/api/swagger.go @@ -30,10 +30,8 @@ func (hs *HTTPServer) registerSwaggerUI(r routing.RouteRegister) { } data := map[string]any{ - "Nonce": c.RequestNonce, - "Assets": assets, - "FavIcon": "public/img/fav32.png", - "AppleTouchIcon": "public/img/apple-touch-icon.png", + "Nonce": c.RequestNonce, + "Assets": assets, } if hs.Cfg.CSPEnabled { data["CSPEnabled"] = true diff --git a/public/swagger/SwaggerPage.tsx b/public/swagger/SwaggerPage.tsx index 6c322e3884b..f97aafedc36 100644 --- a/public/swagger/SwaggerPage.tsx +++ b/public/swagger/SwaggerPage.tsx @@ -5,10 +5,9 @@ import SwaggerUI from 'swagger-ui-react'; import { createTheme, monacoLanguageRegistry, SelectableValue } from '@grafana/data'; import { Trans } from '@grafana/i18n'; -import { Stack, Select, UserIcon, UserView, Button } from '@grafana/ui'; +import { Icon, Stack, Select, UserIcon, UserView, Button } from '@grafana/ui'; import { setMonacoEnv } from 'app/core/monacoEnv'; import { ThemeProvider } from 'app/core/utils/ConfigProvider'; -import grafanaIconSvg from 'img/grafana_icon.svg'; import { NamespaceContext, WrappedPlugins } from './plugins'; @@ -85,7 +84,7 @@ export const Page = () => {
- Grafana + - - - - - {!ds?.meta.annotations && ( - - - The selected data source does not support annotations. Please select a different data source. - - - )} - - - - - - - - - - - - - <> - - {panelFilter !== PanelFilterType.AllPanels && ( - annotation.filter?.ids.includes(panel.value!))} - onChange={onAddFilterPanelID} - isClearable={true} - placeholder={t('dashboard-scene.annotation-settings-edit.placeholder-choose-panels', 'Choose panels')} - width={100} - closeMenuOnSelect={false} - className={styles.select} - data-testid={selectors.components.Annotations.annotationsChoosePanelInput} - /> + + + {/* Data source */} + + + + {!ds?.meta.annotations && ( + + + The selected data source does not support annotations. Please select a different data source. + + + )} + + {/* Enabled */} + - + > + + + + {/* Color */} + + + + + + + {/* Annotation controls display */} + + + + + {/* Show in */} + + <> + - - - + + + {/* Type */} + + - - - - - - + + + {/* Tooltip */} + + + + + {/* Icon */} + + updateValue('target', target)} placeholder="events.eventname" /> -
-
+ + Show Global Annotations? updateIsGlobal(isGlobal)} /> -
-
+ + ); }; From 8a0fa93aecd8e85c82305b834b726572102ab691 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 5 Dec 2025 13:55:56 +0100 Subject: [PATCH 329/378] Zanzana: Fix duplicated writes in one request (#114900) * Zanzana: Fix duplicated writes * add tests --- .../authz/zanzana/server/server_mutate.go | 66 ++++++++++++++++++- .../zanzana/server/server_mutate_folder.go | 19 +----- .../zanzana/server/server_mutate_org_role.go | 19 +----- .../server_mutate_resourcepermissions.go | 19 +----- .../server/server_mutate_rolebindings.go | 19 +----- .../zanzana/server/server_mutate_roles.go | 19 +----- .../server/server_mutate_teambindings.go | 19 +----- .../zanzana/server/server_mutate_test.go | 64 ++++++++++++++++++ 8 files changed, 135 insertions(+), 109 deletions(-) diff --git a/pkg/services/authz/zanzana/server/server_mutate.go b/pkg/services/authz/zanzana/server/server_mutate.go index bd339534fe5..15a57404bbc 100644 --- a/pkg/services/authz/zanzana/server/server_mutate.go +++ b/pkg/services/authz/zanzana/server/server_mutate.go @@ -6,8 +6,10 @@ import ( "fmt" "time" - authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + openfgav1 "github.com/openfga/api/proto/openfga/v1" "go.opentelemetry.io/otel/codes" + + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" ) type OperationGroup string @@ -119,3 +121,65 @@ func groupByOperation(operations []*authzextv1.MutateOperation) (map[OperationGr return grouped, nil } + +func deduplicateTupleKeys(writeTuples []*openfgav1.TupleKey, deleteTuples []*openfgav1.TupleKeyWithoutCondition) ([]*openfgav1.TupleKey, []*openfgav1.TupleKeyWithoutCondition) { + deduplicatedWriteTuples := make([]*openfgav1.TupleKey, 0) + deduplicatedDeleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + writeTupleMap := make(map[string]bool) + + for _, writeTuple := range writeTuples { + id := getTupleKeyID(writeTuple) + if !writeTupleMap[id] { + writeTupleMap[id] = true + deduplicatedWriteTuples = append(deduplicatedWriteTuples, writeTuple) + } + } + + // Prioritize writes over deletes. Deletes do not have a condition, so we don't know if write tuple is different from delete one. + for _, deleteTuple := range deleteTuples { + id := getTupleKeyID(deleteTuple) + if !writeTupleMap[id] { + writeTupleMap[id] = true + deduplicatedDeleteTuples = append(deduplicatedDeleteTuples, deleteTuple) + } + } + + return deduplicatedWriteTuples, deduplicatedDeleteTuples +} + +func (s *Server) writeTuples(ctx context.Context, store *storeInfo, writeTuples []*openfgav1.TupleKey, deleteTuples []*openfgav1.TupleKeyWithoutCondition) error { + writeReq := &openfgav1.WriteRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + } + + writeTuples, deleteTuples = deduplicateTupleKeys(writeTuples, deleteTuples) + + if len(writeTuples) > 0 { + writeReq.Writes = &openfgav1.WriteRequestWrites{ + TupleKeys: writeTuples, + OnDuplicate: "ignore", + } + } + + if len(deleteTuples) > 0 { + writeReq.Deletes = &openfgav1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + OnMissing: "ignore", + } + } + + _, err := s.openfga.Write(ctx, writeReq) + return err +} + +type TupleKey interface { + GetUser() string + GetRelation() string + GetObject() string +} + +func getTupleKeyID(t TupleKey) string { + return fmt.Sprintf("%s:%s:%s", t.GetUser(), t.GetRelation(), t.GetObject()) +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_folder.go b/pkg/services/authz/zanzana/server/server_mutate_folder.go index 3d92347f404..6d07492b788 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_folder.go +++ b/pkg/services/authz/zanzana/server/server_mutate_folder.go @@ -52,24 +52,7 @@ func (s *Server) mutateFolders(ctx context.Context, store *storeInfo, operations return nil } - writeReq := &openfgav1.WriteRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - } - if len(writeTuples) > 0 { - writeReq.Writes = &openfgav1.WriteRequestWrites{ - TupleKeys: writeTuples, - OnDuplicate: "ignore", - } - } - if len(deleteTuples) > 0 { - writeReq.Deletes = &openfgav1.WriteRequestDeletes{ - TupleKeys: deleteTuples, - OnMissing: "ignore", - } - } - - _, err := s.openfga.Write(ctx, writeReq) + err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write folder tuples", "error", err) return err diff --git a/pkg/services/authz/zanzana/server/server_mutate_org_role.go b/pkg/services/authz/zanzana/server/server_mutate_org_role.go index bda9decb3d5..843c62859dd 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_org_role.go +++ b/pkg/services/authz/zanzana/server/server_mutate_org_role.go @@ -50,24 +50,7 @@ func (s *Server) mutateOrgRoles(ctx context.Context, store *storeInfo, operation return nil } - writeReq := &openfgav1.WriteRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - } - if len(writeTuples) > 0 { - writeReq.Writes = &openfgav1.WriteRequestWrites{ - TupleKeys: writeTuples, - OnDuplicate: "ignore", - } - } - if len(deleteTuples) > 0 { - writeReq.Deletes = &openfgav1.WriteRequestDeletes{ - TupleKeys: deleteTuples, - OnMissing: "ignore", - } - } - - _, err := s.openfga.Write(ctx, writeReq) + err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write user org role tuples", "error", err) return err diff --git a/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go index fa8b5467235..f85f31900f6 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go +++ b/pkg/services/authz/zanzana/server/server_mutate_resourcepermissions.go @@ -47,24 +47,7 @@ func (s *Server) mutateResourcePermissions(ctx context.Context, store *storeInfo } } - writeReq := &openfgav1.WriteRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - } - if len(writeTuples) > 0 { - writeReq.Writes = &openfgav1.WriteRequestWrites{ - TupleKeys: writeTuples, - OnDuplicate: "ignore", - } - } - if len(deleteTuples) > 0 { - writeReq.Deletes = &openfgav1.WriteRequestDeletes{ - TupleKeys: deleteTuples, - OnMissing: "ignore", - } - } - - _, err := s.openfga.Write(ctx, writeReq) + err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write resource permission tuples", "error", err) return err diff --git a/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go b/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go index 3b18566bee2..faf23d1f1ed 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go +++ b/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go @@ -44,24 +44,7 @@ func (s *Server) mutateRoleBindings(ctx context.Context, store *storeInfo, opera } } - writeReq := &openfgav1.WriteRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - } - if len(writeTuples) > 0 { - writeReq.Writes = &openfgav1.WriteRequestWrites{ - TupleKeys: writeTuples, - OnDuplicate: "ignore", - } - } - if len(deleteTuples) > 0 { - writeReq.Deletes = &openfgav1.WriteRequestDeletes{ - TupleKeys: deleteTuples, - OnMissing: "ignore", - } - } - - _, err := s.openfga.Write(ctx, writeReq) + err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write resource role binding tuples", "error", err) return err diff --git a/pkg/services/authz/zanzana/server/server_mutate_roles.go b/pkg/services/authz/zanzana/server/server_mutate_roles.go index 4c19b1fd288..c0471fdbf17 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_roles.go +++ b/pkg/services/authz/zanzana/server/server_mutate_roles.go @@ -41,24 +41,7 @@ func (s *Server) mutateRoles(ctx context.Context, store *storeInfo, operations [ } } - writeReq := &openfgav1.WriteRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - } - if len(writeTuples) > 0 { - writeReq.Writes = &openfgav1.WriteRequestWrites{ - TupleKeys: writeTuples, - OnDuplicate: "ignore", - } - } - if len(deleteTuples) > 0 { - writeReq.Deletes = &openfgav1.WriteRequestDeletes{ - TupleKeys: deleteTuples, - OnMissing: "ignore", - } - } - - _, err := s.openfga.Write(ctx, writeReq) + err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write resource role binding tuples", "error", err) return err diff --git a/pkg/services/authz/zanzana/server/server_mutate_teambindings.go b/pkg/services/authz/zanzana/server/server_mutate_teambindings.go index 81e1c9cb437..96690bb96d8 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_teambindings.go +++ b/pkg/services/authz/zanzana/server/server_mutate_teambindings.go @@ -43,24 +43,7 @@ func (s *Server) mutateTeamBindings(ctx context.Context, store *storeInfo, opera } } - writeReq := &openfgav1.WriteRequest{ - StoreId: store.ID, - AuthorizationModelId: store.ModelID, - } - if len(writeTuples) > 0 { - writeReq.Writes = &openfgav1.WriteRequestWrites{ - TupleKeys: writeTuples, - OnDuplicate: "ignore", - } - } - if len(deleteTuples) > 0 { - writeReq.Deletes = &openfgav1.WriteRequestDeletes{ - TupleKeys: deleteTuples, - OnMissing: "ignore", - } - } - - _, err := s.openfga.Write(ctx, writeReq) + err := s.writeTuples(ctx, store, writeTuples, deleteTuples) if err != nil { s.logger.Error("failed to write resource role binding tuples", "error", err) return err diff --git a/pkg/services/authz/zanzana/server/server_mutate_test.go b/pkg/services/authz/zanzana/server/server_mutate_test.go index 70dc1ea2fb8..c1fcfabbe43 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_test.go +++ b/pkg/services/authz/zanzana/server/server_mutate_test.go @@ -5,6 +5,7 @@ import ( openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" @@ -133,3 +134,66 @@ func testMutate(t *testing.T, srv *Server) { require.Len(t, res.Tuples, 0) }) } + +func TestDeduplicateTupleKeys(t *testing.T) { + t.Run("should deduplicate write tuples", func(t *testing.T) { + writeTuples := []*openfgav1.TupleKey{ + {User: "user:1", Relation: "get", Object: "object:1"}, + {User: "user:1", Relation: "get", Object: "object:2"}, + } + deleteTuples := []*openfgav1.TupleKeyWithoutCondition{ + {User: "user:1", Relation: "get", Object: "object:1"}, + {User: "user:2", Relation: "get", Object: "object:2"}, + } + + deduplicatedWriteTuples, deduplicatedDeleteTuples := deduplicateTupleKeys(writeTuples, deleteTuples) + require.Len(t, deduplicatedWriteTuples, 2) + require.ElementsMatch(t, deduplicatedWriteTuples, []*openfgav1.TupleKey{ + {User: "user:1", Relation: "get", Object: "object:1"}, + {User: "user:1", Relation: "get", Object: "object:2"}, + }) + + require.Len(t, deduplicatedDeleteTuples, 1) + require.ElementsMatch(t, deduplicatedDeleteTuples, []*openfgav1.TupleKeyWithoutCondition{ + {User: "user:2", Relation: "get", Object: "object:2"}, + }) + }) + + t.Run("should deduplicate write tuples with conditions", func(t *testing.T) { + writeTuples := []*openfgav1.TupleKey{ + {User: "user:1", Relation: "get", Object: "object:1", Condition: &openfgav1.RelationshipCondition{Name: "condition:1", Context: &structpb.Struct{Fields: map[string]*structpb.Value{ + "field:1": structpb.NewStringValue("value:1"), + }}}}, + {User: "user:1", Relation: "get", Object: "object:2"}, + } + deleteTuples := []*openfgav1.TupleKeyWithoutCondition{ + {User: "user:1", Relation: "get", Object: "object:1"}, + } + + deduplicatedWriteTuples, deduplicatedDeleteTuples := deduplicateTupleKeys(writeTuples, deleteTuples) + require.Len(t, deduplicatedWriteTuples, 2) + require.ElementsMatch(t, deduplicatedWriteTuples, []*openfgav1.TupleKey{ + {User: "user:1", Relation: "get", Object: "object:1", Condition: &openfgav1.RelationshipCondition{Name: "condition:1", Context: &structpb.Struct{Fields: map[string]*structpb.Value{ + "field:1": structpb.NewStringValue("value:1"), + }}}}, + {User: "user:1", Relation: "get", Object: "object:2"}, + }) + + require.Len(t, deduplicatedDeleteTuples, 0) + }) + + t.Run("should do nothing for no duplicates", func(t *testing.T) { + writeTuples := []*openfgav1.TupleKey{ + {User: "user:1", Relation: "get", Object: "object:1"}, + } + deleteTuples := []*openfgav1.TupleKeyWithoutCondition{ + {User: "user:2", Relation: "get", Object: "object:2"}, + } + + deduplicatedWriteTuples, deduplicatedDeleteTuples := deduplicateTupleKeys(writeTuples, deleteTuples) + require.Len(t, deduplicatedWriteTuples, 1) + require.ElementsMatch(t, deduplicatedWriteTuples, writeTuples) + require.Len(t, deduplicatedDeleteTuples, 1) + require.ElementsMatch(t, deduplicatedDeleteTuples, deleteTuples) + }) +} From 5ac702a4c16b17ac08f067622ffe98ca4d855f1b Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 5 Dec 2025 17:21:30 +0300 Subject: [PATCH 330/378] Dashboards: update manifest to avoid useoldmanifestkinds (#114910) --- apps/dashboard/Makefile | 3 +- apps/dashboard/kinds/dashboard.cue | 54 ------------ apps/dashboard/kinds/manifest.cue | 84 +++++++++++++++++-- apps/dashboard/kinds/snapshot.cue | 62 +++++--------- apps/dashboard/pkg/apis/dashboard/utils.go | 2 + .../conversion/v1beta1_to_v2alpha1.go | 4 +- .../conversion/v1beta1_to_v2alpha1_test.go | 4 +- pkg/tsdb/grafanads/grafana.go | 3 +- 8 files changed, 110 insertions(+), 106 deletions(-) diff --git a/apps/dashboard/Makefile b/apps/dashboard/Makefile index fa75513d964..3d5c7060199 100644 --- a/apps/dashboard/Makefile +++ b/apps/dashboard/Makefile @@ -12,8 +12,7 @@ do-generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generati --grouping=group \ --defencoding=none \ --genoperatorstate=false \ - --noschemasinmanifest \ - --useoldmanifestkinds + --noschemasinmanifest .PHONY: post-generate-cleanup post-generate-cleanup: ## Clean up the generated code diff --git a/apps/dashboard/kinds/dashboard.cue b/apps/dashboard/kinds/dashboard.cue index e8dfea3bf98..5d04cf1e331 100644 --- a/apps/dashboard/kinds/dashboard.cue +++ b/apps/dashboard/kinds/dashboard.cue @@ -1,12 +1,5 @@ package kinds -import ( - v0 "github.com/grafana/grafana/sdkkinds/dashboard/v0alpha1" - v1 "github.com/grafana/grafana/sdkkinds/dashboard/v1beta1" - v2alpha1 "github.com/grafana/grafana/sdkkinds/dashboard/v2alpha1" - v2beta1 "github.com/grafana/grafana/sdkkinds/dashboard/v2beta1" -) - // Status is the shared status of all dashboard versions. DashboardStatus: { // Optional conversion status. @@ -31,50 +24,3 @@ ConversionStatus: { // The original value map[string]any source?: _ } - -dashboard: { - kind: "Dashboard" - pluralName: "Dashboards" - current: "v1beta1" - codegen: { - ts: { - enabled: true - config: { - enumsAsUnionTypes: true - } - } - go: { - enabled: true - config: { - allowMarshalEmptyDisjunctions: true - } - } - } - - versions: { - "v0alpha1": { - schema: { - spec: v0.DashboardSpec - status: DashboardStatus - } - } - "v1beta1": { - schema: { - spec: v1.DashboardSpec - status: DashboardStatus - } - } - "v2alpha1": { - schema: { - spec: v2alpha1.DashboardSpec - status: DashboardStatus - } - } - "v2beta1": { - schema: { - spec: v2beta1.DashboardSpec - status: DashboardStatus - } - } - } -} diff --git a/apps/dashboard/kinds/manifest.cue b/apps/dashboard/kinds/manifest.cue index f1044a39e24..9fb17910664 100644 --- a/apps/dashboard/kinds/manifest.cue +++ b/apps/dashboard/kinds/manifest.cue @@ -1,10 +1,82 @@ package kinds +import ( + v0 "github.com/grafana/grafana/sdkkinds/dashboard/v0alpha1" + v1 "github.com/grafana/grafana/sdkkinds/dashboard/v1beta1" + v2alpha1 "github.com/grafana/grafana/sdkkinds/dashboard/v2alpha1" + v2beta1 "github.com/grafana/grafana/sdkkinds/dashboard/v2beta1" +) + manifest: { - appName: "dashboard" - groupOverride: "dashboard.grafana.app" - kinds: [ - dashboard, - snapshot, - ] + appName: "dashboard" + groupOverride: "dashboard.grafana.app" + preferredVersion: "v1beta1" + + versions: { + "v0alpha1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + { + kind: "Dashboard" + pluralName: "Dashboards" + schema: { + spec: v0.DashboardSpec + status: DashboardStatus + } + }, + snapshotV0alpha1, // Only exists in v0alpha (for now) + ] + } + "v1beta1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + { + kind: "Dashboard" + pluralName: "Dashboards" + schema: { + spec: v1.DashboardSpec + status: DashboardStatus + } + } + ] + } + "v2alpha1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + { + kind: "Dashboard" + pluralName: "Dashboards" + schema: { + spec: v2alpha1.DashboardSpec + status: DashboardStatus + } + } + ] + } + "v2beta1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + { + kind: "Dashboard" + pluralName: "Dashboards" + schema: { + spec: v2beta1.DashboardSpec + status: DashboardStatus + } + } + ] + } + } } diff --git a/apps/dashboard/kinds/snapshot.cue b/apps/dashboard/kinds/snapshot.cue index c224daf8492..00f445881b5 100644 --- a/apps/dashboard/kinds/snapshot.cue +++ b/apps/dashboard/kinds/snapshot.cue @@ -1,46 +1,30 @@ package kinds -snapshot: { +snapshotV0alpha1: { kind: "Snapshot" pluralName: "Snapshots" - scope: "Namespaced" - current: "v0alpha1" - - codegen: { - ts: { - enabled: true - } - go: { - enabled: true - } - } - - versions: { - "v0alpha1": { - schema: { - spec: { - // Snapshot title - title?: string - - // Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds) - expires?: int64 | *0 - - // When set to true, the snapshot exists in a remote server - external?: bool | *false - - // The external URL where the snapshot can be seen - externalUrl?: string - - // The URL that created the dashboard originally - originalUrl?: string - - // Snapshot creation timestamp - timestamp?: string + schema: { + spec: { + // Snapshot title + title?: string + + // Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds) + expires?: int64 | *0 + + // When set to true, the snapshot exists in a remote server + external?: bool | *false + + // The external URL where the snapshot can be seen + externalUrl?: string + + // The URL that created the dashboard originally + originalUrl?: string + + // Snapshot creation timestamp + timestamp?: string - // The raw dashboard (unstructured for now) - dashboard?: [string]: _ - } - } + // The raw dashboard (unstructured for now) + dashboard?: [string]: _ } } -} +} \ No newline at end of file diff --git a/apps/dashboard/pkg/apis/dashboard/utils.go b/apps/dashboard/pkg/apis/dashboard/utils.go index a5979151a90..f1453d548a0 100644 --- a/apps/dashboard/pkg/apis/dashboard/utils.go +++ b/apps/dashboard/pkg/apis/dashboard/utils.go @@ -6,6 +6,8 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" ) +const GrafanaDatasourceUID = "grafana" + // SetPluginIDMeta sets the repo name to "plugin" and the path to the plugin ID func SetPluginIDMeta(obj *unstructured.Unstructured, pluginID string) { if pluginID == "" { diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index e6599b1d8eb..231c0ad4131 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -12,11 +12,11 @@ import ( "k8s.io/apiserver/pkg/endpoints/request" "github.com/grafana/authlib/types" + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" schemaversion "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/tsdb/grafanads" ) // getDefaultDatasourceType gets the default datasource type using the datasource provider @@ -58,7 +58,7 @@ func getDatasourceTypeByUID(ctx context.Context, uid string, provider schemavers // datasource: { type: "datasource" } with no UID, it should resolve to uid: "grafana". func resolveGrafanaDatasourceUID(dsType, dsUID string) string { if dsType == "datasource" && dsUID == "" { - return grafanads.DatasourceUID + return dashboard.GrafanaDatasourceUID } return dsUID } diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go index 6bbdf1ca214..3dad9188fe7 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go @@ -7,11 +7,11 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" "github.com/grafana/grafana/apps/dashboard/pkg/migration" migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" - "github.com/grafana/grafana/pkg/tsdb/grafanads" ) // TestV1beta1ToV2alpha1 tests conversion from v1beta1 to v2alpha1 with various datasource scenarios @@ -77,7 +77,7 @@ func TestV1beta1ToV2alpha1(t *testing.T) { // Verify datasource UID is resolved to "grafana" assert.NotNil(t, query.Spec.Datasource.Uid) - assert.Equal(t, grafanads.DatasourceUID, *query.Spec.Datasource.Uid, "type: 'datasource' with no UID should resolve to uid: 'grafana'") + assert.Equal(t, dashboard.GrafanaDatasourceUID, *query.Spec.Datasource.Uid, "type: 'datasource' with no UID should resolve to uid: 'grafana'") // Verify query kind matches datasource type assert.Equal(t, "datasource", query.Spec.Query.Kind) diff --git a/pkg/tsdb/grafanads/grafana.go b/pkg/tsdb/grafanads/grafana.go index 06254b7f0b8..68dc792342c 100644 --- a/pkg/tsdb/grafanads/grafana.go +++ b/pkg/tsdb/grafanads/grafana.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/datasources" @@ -31,7 +32,7 @@ const DatasourceID = -1 // DatasourceUID is the fake datasource uid used in requests to identify it as a // Grafana DS command. -const DatasourceUID = "grafana" +const DatasourceUID = dashboard.GrafanaDatasourceUID // Make sure Service implements required interfaces. // This is important to do since otherwise we will only get a From b19e5462545c80705e26a1d0489bae2ccac1b702 Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 5 Dec 2025 16:04:42 +0100 Subject: [PATCH 331/378] Remote Alertmanager: Remove X-Remote-Alertmanager header (#114917) Remote Alertmanager: Remove X-Remote-Alertmanager haeder --- pkg/services/ngalert/remote/alertmanager_test.go | 7 ------- .../ngalert/remote/client/mimir_auth_round_tripper.go | 4 +--- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 953ef3ef2d6..d6f66756454 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -153,7 +153,6 @@ func TestGetRemoteState(t *testing.T) { getOkHandler := func(state string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) res := map[string]any{ "status": "success", @@ -268,7 +267,6 @@ func TestIntegrationApplyConfig(t *testing.T) { errorHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) w.Header().Add("content-type", "application/json") w.WriteHeader(http.StatusInternalServerError) require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"status": "error"})) @@ -278,7 +276,6 @@ func TestIntegrationApplyConfig(t *testing.T) { var configSyncs, stateSyncs int okHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) res := map[string]any{"status": "success"} if r.Method == http.MethodPost { @@ -432,7 +429,6 @@ func TestCompareAndSendConfiguration(t *testing.T) { var got string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) w.Header().Add("content-type", "application/json") b, err := io.ReadAll(r.Body) @@ -639,7 +635,6 @@ func Test_TestReceiversDecryptsSecureSettings(t *testing.T) { var got apimodels.TestReceiversConfigBodyParams server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) w.Header().Add("Content-Type", "application/json") require.NoError(t, json.NewDecoder(r.Body).Decode(&got)) require.NoError(t, r.Body.Close()) @@ -746,7 +741,6 @@ func TestApplyConfigWithExtraConfigs(t *testing.T) { var configSent client.UserGrafanaConfig server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/config") { require.NoError(t, json.NewDecoder(r.Body).Decode(&configSent)) @@ -828,7 +822,6 @@ func TestCompareAndSendConfigurationWithExtraConfigs(t *testing.T) { var configSent client.UserGrafanaConfig server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) - require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/config") { require.NoError(t, json.NewDecoder(r.Body).Decode(&configSent)) diff --git a/pkg/services/ngalert/remote/client/mimir_auth_round_tripper.go b/pkg/services/ngalert/remote/client/mimir_auth_round_tripper.go index 3a9ff94f4a6..2a7a6314e1d 100644 --- a/pkg/services/ngalert/remote/client/mimir_auth_round_tripper.go +++ b/pkg/services/ngalert/remote/client/mimir_auth_round_tripper.go @@ -5,8 +5,7 @@ import ( ) const ( - MimirTenantHeader = "X-Scope-OrgID" - RemoteAlertmanagerHeader = "X-Remote-Alertmanager" + MimirTenantHeader = "X-Scope-OrgID" ) type MimirAuthRoundTripper struct { @@ -19,7 +18,6 @@ type MimirAuthRoundTripper struct { // It adds an `X-Scope-OrgID` header with the TenantID if only provided with a tenantID or sets HTTP Basic Authentication if both // a tenantID and a password are provided. func (r *MimirAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Set(RemoteAlertmanagerHeader, "true") if r.TenantID != "" && r.Password == "" { req.Header.Set(MimirTenantHeader, r.TenantID) } From bf042afa9878fcbe91a14da88fb6373f0946c0eb Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Fri, 5 Dec 2025 17:12:12 +0200 Subject: [PATCH 332/378] Dashboard: Fix dropping panels in tabs and rows (#114893) --- .../dashboard-scene/scene/layout-rows/RowItemRenderer.tsx | 3 ++- .../dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx index 4c74ee35d99..80c16b2f2a1 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx @@ -13,6 +13,7 @@ import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { useDashboardState, useInterpolatedTitle } from '../../utils/utils'; import { DashboardScene } from '../DashboardScene'; import { useSoloPanelContext } from '../SoloPanelContext'; +import { isDashboardLayoutGrid } from '../types/DashboardLayoutGrid'; import { RowItem } from './RowItem'; @@ -83,7 +84,7 @@ export function RowItemRenderer({ model }: SceneComponentProps) { dragProvided.innerRef(ref); model.containerRef.current = ref; }} - data-dashboard-drop-target-key={model.state.key} + data-dashboard-drop-target-key={isDashboardLayoutGrid(layout) ? model.state.key : undefined} className={cx( styles.wrapper, !isCollapsed && styles.wrapperNotCollapsed, diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx index 0fa22e9d305..e618fb21c7e 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx @@ -11,6 +11,7 @@ import { useIsConditionallyHidden } from '../../conditional-rendering/hooks/useI import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { useDashboardState } from '../../utils/utils'; import { useSoloPanelContext } from '../SoloPanelContext'; +import { isDashboardLayoutGrid } from '../types/DashboardLayoutGrid'; import { TabItem } from './TabItem'; @@ -91,7 +92,7 @@ export function TabItemRenderer({ model }: SceneComponentProps) { onSelect?.(evt); }} label={titleInterpolated} - data-dashboard-drop-target-key={model.state.key} + data-dashboard-drop-target-key={isDashboardLayoutGrid(layout) ? model.state.key : undefined} {...titleCollisionProps} />
From 7cd10aa49ed22435b0aef9fc04d471818b7108dc Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Fri, 5 Dec 2025 10:14:02 -0500 Subject: [PATCH 333/378] SQL Expressions: Fix alerts with sql expressions that have a cte (#114852) Fix for #114377 - fix alerts with sql expressions that have a cte --- .../components/rule-editor/dag.test.ts | 55 +++++++++++++++++++ .../unified/components/rule-editor/dag.ts | 44 +++++++++++++-- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/dag.test.ts b/public/app/features/alerting/unified/components/rule-editor/dag.test.ts index 189bcb6f25e..423f035ddc9 100644 --- a/public/app/features/alerting/unified/components/rule-editor/dag.test.ts +++ b/public/app/features/alerting/unified/components/rule-editor/dag.test.ts @@ -293,6 +293,61 @@ SELECT * FROM table1`) expect(parseRefsFromSqlExpression('SELECT * FROM\ntable1')).toEqual(['table1']); }); }); + + describe('CTE (Common Table Expression) handling', () => { + it('should exclude single CTE name from results', () => { + const query = 'WITH my_cte AS (SELECT * FROM table1) SELECT * FROM my_cte'; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1']); + }); + + it('should exclude multiple CTE names from results', () => { + const query = ` + WITH cte1 AS (SELECT * FROM table1), + cte2 AS (SELECT * FROM table2) + SELECT * FROM cte1 JOIN cte2 ON cte1.id = cte2.id + `; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1', 'table2']); + }); + + it('should handle CTEs with external table references in main query', () => { + const query = ` + WITH summary AS (SELECT id, count FROM table1) + SELECT * FROM summary JOIN table2 ON summary.id = table2.id + `; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1', 'table2']); + }); + + it('should handle CTE names case-insensitively', () => { + const query = 'WITH MyCte AS (SELECT * FROM table1) SELECT * FROM mycte'; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1']); + }); + + it('should handle RECURSIVE CTEs', () => { + const query = ` + WITH RECURSIVE cte AS ( + SELECT * FROM table1 + UNION ALL + SELECT * FROM cte WHERE depth < 10 + ) + SELECT * FROM cte + `; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1']); + }); + + it('should handle queries without CTEs normally', () => { + const query = 'SELECT * FROM table1 JOIN table2 ON table1.id = table2.id'; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1', 'table2']); + }); + + it('should handle CTE that references another CTE', () => { + const query = ` + WITH cte1 AS (SELECT * FROM table1), + cte2 AS (SELECT * FROM cte1) + SELECT * FROM cte2 + `; + expect(parseRefsFromSqlExpression(query)).toEqual(['table1']); + }); + }); }); describe('fingerprints', () => { diff --git a/public/app/features/alerting/unified/components/rule-editor/dag.ts b/public/app/features/alerting/unified/components/rule-editor/dag.ts index 9666186f039..c7078ca7517 100644 --- a/public/app/features/alerting/unified/components/rule-editor/dag.ts +++ b/public/app/features/alerting/unified/components/rule-editor/dag.ts @@ -132,10 +132,15 @@ export function parseRefsFromSqlExpression(input: string): string[] { .replace(/\s+/g, ' ') // Remove any potential multi line comments .replace(/\/\*[\s\S]*?\*\//g, ''); + + // Extract CTE names to exclude them from table references + const cteNames = parseCteNames(query); + const tableMatches = []; // Extract tables after FROM - case insensitive with /i flag - const fromRegex = /from\s+([^;]*?)(?:\s+(?:join|where|group|having|order|limit)|\s*$)/gi; + // Terminate on: SQL keywords, closing paren (for CTEs/subqueries), or end of string + const fromRegex = /from\s+([^;)]*?)(?:\s+(?:join|where|group|having|order|limit|on|select)|\)|$)/gi; for (const match of query.matchAll(fromRegex)) { const fromClause = match[1].trim(); @@ -153,13 +158,44 @@ export function parseRefsFromSqlExpression(input: string): string[] { tableMatches.push(cleanTableName(match[1])); } - return compact(uniq(tableMatches)); + // Filter out CTE names - they're local definitions, not external references + const externalRefs = tableMatches.filter((table) => !cteNames.has(table.toLowerCase())); + + return compact(uniq(externalRefs)); +} + +/** + * Parse CTE (Common Table Expression) names from a SQL query. + * CTEs are defined with: WITH cte_name AS (...), another_cte AS (...) + */ +function parseCteNames(query: string): Set { + const cteNames = new Set(); + + // Match the WITH clause - handles both regular and RECURSIVE CTEs + const withMatch = query.match(/^\s*with\s+(?:recursive\s+)?(.*?)(?:\s+select\s)/i); + + if (!withMatch) { + return cteNames; + } + + const withClause = withMatch[1]; + + // Match CTE names - they appear before "AS" keyword followed by opening paren + // This handles: cte_name AS (, "quoted_name" AS ( + const cteNameRegex = /([a-zA-Z0-9_]+|"[^"]+"|'[^']+')\s+as\s*\(/gi; + + for (const match of withClause.matchAll(cteNameRegex)) { + const cteName = match[1].replace(/['"]/g, '').toLowerCase(); + cteNames.add(cteName); + } + + return cteNames; } // Helper function to clean table names function cleanTableName(tableName: string): string { - // Remove quotes - let name = tableName.replace(/['"]/g, ''); + // Remove quotes and parentheses + let name = tableName.replace(/['"()]/g, ''); // Remove alias if present (both "AS alias" and "alias" forms) if (name.includes(' as ')) { From 0adb2461e9bdd21c56412a2b9d3695fad6e06902 Mon Sep 17 00:00:00 2001 From: "Marc M." <146180665+grafakus@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:48:34 +0100 Subject: [PATCH 334/378] Dashboards: Improve custom variable editor and undo/redo (#114559) --- .../dashboards-edit-custom-variables.spec.ts | 6 +- .../src/selectors/pages.ts | 3 + .../components/VariableValuesPreview.tsx | 2 +- .../CustomVariableEditor/ModalEditor.tsx | 92 +++++++++++++++---- .../editors/CustomVariableEditor/PaneItem.tsx | 2 +- .../CustomVariableEditor/ValuesBuilder.tsx | 52 ----------- .../CustomVariableEditor/ValuesPreview.tsx | 13 --- public/locales/en-US/grafana.json | 4 +- 8 files changed, 85 insertions(+), 89 deletions(-) delete mode 100644 public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesBuilder.tsx delete mode 100644 public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesPreview.tsx diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts index 4715dfc7128..e11f2dd099a 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts @@ -84,9 +84,9 @@ test.describe( refetchItems(dashboardPage, selectors); }; - const closeModal = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { + const applyAndcloseModal = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { await dashboardPage - .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.closeButton) + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.applyButton) .click(); }; @@ -149,7 +149,7 @@ test.describe( await removeItem(dashboardPage, selectors, 2); await checkRows(3); await checkPreview(dashboardPage, selectors, ['first value', 'second label', 'fourth value']); - await closeModal(dashboardPage, selectors); + await applyAndcloseModal(dashboardPage, selectors); // assert variable is visible and has the correct values const variableLabel = dashboardPage.getByGrafanaSelector( diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 47a5573b00d..1fa640a2563 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -567,6 +567,9 @@ export const versionedPages = { closeButton: { [MIN_GRAFANA_VERSION]: 'data-testid custom-variable-close-button', }, + applyButton: { + [MIN_GRAFANA_VERSION]: 'data-testid custom-variable-apply-button', + }, }, IntervalVariable: { intervalsValueInput: { diff --git a/public/app/features/dashboard-scene/settings/variables/components/VariableValuesPreview.tsx b/public/app/features/dashboard-scene/settings/variables/components/VariableValuesPreview.tsx index 73d57bd811a..ac59419cda7 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/VariableValuesPreview.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/VariableValuesPreview.tsx @@ -37,7 +37,7 @@ export const VariableValuesPreview = ({ options }: VariableValuesPreviewProps) = {previewOptions.map((o, index) => ( -
{o.label}
+
{o.label || String(o.value)}
))} diff --git a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx index 3e8a8aa57b1..aed926a6809 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx @@ -1,47 +1,103 @@ -import { useCallback, useRef } from 'react'; +import { useRef, useState } from 'react'; +import { lastValueFrom } from 'rxjs'; import { selectors } from '@grafana/e2e-selectors'; import { t, Trans } from '@grafana/i18n'; -import { CustomVariable } from '@grafana/scenes'; +import { CustomVariable, VariableValueOption, VariableValueSingle } from '@grafana/scenes'; import { Button, Modal, Stack } from '@grafana/ui'; -import { VariableStaticOptionsFormRef } from '../../components/VariableStaticOptionsForm'; +import { dashboardEditActions } from '../../../../edit-pane/shared'; +import { VariableStaticOptionsForm, VariableStaticOptionsFormRef } from '../../components/VariableStaticOptionsForm'; import { VariableStaticOptionsFormAddButton } from '../../components/VariableStaticOptionsFormAddButton'; - -import { ValuesBuilder } from './ValuesBuilder'; -import { ValuesPreview } from './ValuesPreview'; +import { VariableValuesPreview } from '../../components/VariableValuesPreview'; interface ModalEditorProps { variable: CustomVariable; - isOpen: boolean; onClose: () => void; } -export function ModalEditor({ variable, isOpen, onClose }: ModalEditorProps) { - const formRef = useRef(null); - - const handleOnAdd = useCallback(() => formRef.current?.addItem(), []); +export function ModalEditor(props: ModalEditorProps) { + const { formRef, onCloseModal, options, onChangeOptions, onAddNewOption, onSaveOptions } = useModalEditor(props); return ( - - + + - }> + }> + ); } + +function useModalEditor({ variable, onClose }: ModalEditorProps) { + const { query } = variable.state; + const [options, setOptions] = useState(() => transformQueryToOptions(variable, query)); + const initialQueryRef = useRef(query); + const formRef = useRef(null); + + return { + formRef, + onCloseModal: onClose, + options, + onChangeOptions: setOptions, + onAddNewOption() { + formRef.current?.addItem(); + }, + onSaveOptions() { + dashboardEditActions.edit({ + source: variable, + description: t('dashboard.edit-pane.variable.custom-options.change-value', 'Change variable value'), + perform: () => { + variable.setState({ query: transformOptionsToQuery(options) }); + lastValueFrom(variable.validateAndUpdate!()); + }, + undo: () => { + variable.setState({ query: initialQueryRef.current }); + lastValueFrom(variable.validateAndUpdate!()); + }, + }); + + onClose(); + }, + }; +} + +const transformQueryToOptions = (variable: ModalEditorProps['variable'], query: string) => + variable.transformCsvStringToOptions(query, false).map(({ label, value }) => ({ + value, + label: value === label ? '' : label, + })); + +const formatOption = (option: VariableValueOption) => { + if (!option.label || option.label === option.value) { + return escapeEntities(option.value); + } + return `${escapeEntities(option.label)} : ${escapeEntities(String(option.value))}`; +}; + +const escapeEntities = (text: VariableValueSingle) => String(text).trim().replaceAll(',', '\\,'); + +const transformOptionsToQuery = (options: VariableValueOption[]) => options.map(formatOption).join(', '); diff --git a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/PaneItem.tsx b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/PaneItem.tsx index e453fc6b8b8..d1dab1e554f 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/PaneItem.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/PaneItem.tsx @@ -31,7 +31,7 @@ export function PaneItem({ variable }: PaneItemProps) { Open variable editor - setIsOpen(false)} /> + {isOpen && setIsOpen(false)} />} ); } diff --git a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesBuilder.tsx b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesBuilder.tsx deleted file mode 100644 index e2eceea5fd3..00000000000 --- a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesBuilder.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { forwardRef, useCallback } from 'react'; -import { lastValueFrom } from 'rxjs'; - -import { CustomVariable, VariableValueOption, VariableValueSingle } from '@grafana/scenes'; - -import { VariableStaticOptionsForm, VariableStaticOptionsFormRef } from '../../components/VariableStaticOptionsForm'; - -interface ValuesBuilderProps { - variable: CustomVariable; -} - -export const ValuesBuilder = forwardRef(function ( - { variable }: ValuesBuilderProps, - ref -) { - const { query } = variable.useState(); - - const options = variable.transformCsvStringToOptions(query, false).map(({ label, value }) => ({ - value, - label: value === label ? '' : label, - })); - - const escapeEntities = useCallback((text: VariableValueSingle) => String(text).trim().replaceAll(',', '\\,'), []); - - const formatOption = useCallback( - (option: VariableValueOption) => { - if (!option.label || option.label === option.value) { - return escapeEntities(option.value); - } - - return `${escapeEntities(option.label)} : ${escapeEntities(String(option.value))}`; - }, - [escapeEntities] - ); - - const generateQuery = useCallback( - (options: VariableValueOption[]) => options.map(formatOption).join(', '), - [formatOption] - ); - - const handleOptionsChange = useCallback( - async (options: VariableValueOption[]) => { - variable.setState({ query: generateQuery(options) }); - await lastValueFrom(variable.validateAndUpdate!()); - }, - [variable, generateQuery] - ); - - return ; -}); - -ValuesBuilder.displayName = 'ValuesBuilder'; diff --git a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesPreview.tsx b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesPreview.tsx deleted file mode 100644 index 49a3e8dd55b..00000000000 --- a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ValuesPreview.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { CustomVariable } from '@grafana/scenes'; - -import { VariableValuesPreview } from '../../components/VariableValuesPreview'; -import { hasVariableOptions } from '../../utils'; - -export function ValuesPreview({ variable }: { variable: CustomVariable }) { - // Workaround to toggle a component refresh when values change so that the preview is updated - variable.useState(); - - const isHasVariableOptions = hasVariableOptions(variable); - - return isHasVariableOptions ? : null; -} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6ee3b9e50e6..cc056e38526 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "Close", + "apply": "Apply", + "change-value": "Change variable value", + "discard": "Discard", "modal-title": "Custom Variable", "values": "Values separated by comma" }, From 74c7b5a29220301d8809d2fc0caad2efe9f4e853 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 5 Dec 2025 18:02:11 +0100 Subject: [PATCH 335/378] Alerting: Fix creating a new alert rule vesion when only keep_firing_for changes (#114926) Alerting: Create alert rule vesion when keep_firing_for changes --- pkg/services/ngalert/store/models.go | 1 + pkg/services/ngalert/store/models_test.go | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/pkg/services/ngalert/store/models.go b/pkg/services/ngalert/store/models.go index 24167a225dd..25b81053e66 100644 --- a/pkg/services/ngalert/store/models.go +++ b/pkg/services/ngalert/store/models.go @@ -88,6 +88,7 @@ func (a alertRuleVersion) EqualSpec(b alertRuleVersion) bool { a.NoDataState == b.NoDataState && a.ExecErrState == b.ExecErrState && a.For == b.For && + a.KeepFiringFor == b.KeepFiringFor && a.Annotations == b.Annotations && a.Labels == b.Labels && a.IsPaused == b.IsPaused && diff --git a/pkg/services/ngalert/store/models_test.go b/pkg/services/ngalert/store/models_test.go index ddbc34b4036..98703609007 100644 --- a/pkg/services/ngalert/store/models_test.go +++ b/pkg/services/ngalert/store/models_test.go @@ -21,6 +21,7 @@ func TestAlertRuleVersion_EqualSpec(t *testing.T) { NoDataState: "state1", ExecErrState: "state2", For: time.Minute, + KeepFiringFor: 2 * time.Minute, Annotations: `{ "test": "annotation" }`, Labels: `{ "test": "label" }`, IsPaused: true, @@ -119,6 +120,12 @@ func TestAlertRuleVersion_EqualSpec(t *testing.T) { b: func() alertRuleVersion { v := baseVersion; v.For = 2 * time.Minute; return v }(), expect: false, }, + { + name: "different KeepFiringFor durations", + a: baseVersion, + b: func() alertRuleVersion { v := baseVersion; v.KeepFiringFor = 5 * time.Minute; return v }(), + expect: false, + }, { name: "exact match including bools and other types", a: func() alertRuleVersion { From 5b89d3b807d06836a4d7e281bb37cbd1d5dae715 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:56:01 -0500 Subject: [PATCH 336/378] Plugins App: Add access control (#114869) --- pkg/registry/apps/plugins/accesscontrol.go | 127 ++++++++++++++++++ pkg/registry/apps/plugins/register.go | 21 ++- pkg/server/wire_gen.go | 4 +- pkg/services/accesscontrol/permreg/permreg.go | 2 + 4 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 pkg/registry/apps/plugins/accesscontrol.go diff --git a/pkg/registry/apps/plugins/accesscontrol.go b/pkg/registry/apps/plugins/accesscontrol.go new file mode 100644 index 00000000000..d41efa86f97 --- /dev/null +++ b/pkg/registry/apps/plugins/accesscontrol.go @@ -0,0 +1,127 @@ +package plugins + +import ( + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/org" +) + +const ( + // Plugins + ActionPluginsPluginsCreate = "plugins.plugins:create" // CREATE. + ActionPluginsPluginsWrite = "plugins.plugins:write" // UPDATE. + ActionPluginsPluginsRead = "plugins.plugins:read" // GET + LIST. + ActionPluginsPluginsDelete = "plugins.plugins:delete" // DELETE. + + // PluginMetas + ActionPluginsPluginsMetaCreate = "plugins.pluginsmeta:create" // CREATE. + ActionPluginsPluginsMetaWrite = "plugins.pluginsmeta:write" // UPDATE. + ActionPluginsPluginsMetaRead = "plugins.pluginsmeta:read" // GET + LIST. + ActionPluginsPluginsMetaDelete = "plugins.pluginsmeta:delete" // DELETE. +) + +var ( + ScopeProviderPluginsPlugins = accesscontrol.NewScopeProvider("plugins.plugins") + ScopeProviderPluginsPluginsMeta = accesscontrol.NewScopeProvider("plugins.pluginsmeta") + + ScopeAllPluginsPlugins = ScopeProviderPluginsPlugins.GetResourceAllScope() + ScopeAllPluginsPluginsMeta = ScopeProviderPluginsPluginsMeta.GetResourceAllScope() +) + +func registerAccessControlRoles(service accesscontrol.Service) error { + // Plugins + pluginsReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:plugins.plugins:reader", + DisplayName: "Plugins Reader", + Description: "Read and list plugins.", + Group: "Plugins", + Permissions: []accesscontrol.Permission{ + { + Action: ActionPluginsPluginsRead, + Scope: ScopeAllPluginsPlugins, + }, + }, + }, + Grants: []string{string(org.RoleViewer), string(org.RoleEditor), string(org.RoleAdmin)}, + } + + pluginsWriter := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:plugins.plugins:writer", + DisplayName: "Plugins Writer", + Description: "Create, update and delete plugins.", + Group: "Plugins", + Permissions: []accesscontrol.Permission{ + { + Action: ActionPluginsPluginsCreate, + Scope: ScopeAllPluginsPlugins, + }, + { + Action: ActionPluginsPluginsRead, + Scope: ScopeAllPluginsPlugins, + }, + { + Action: ActionPluginsPluginsWrite, + Scope: ScopeAllPluginsPlugins, + }, + { + Action: ActionPluginsPluginsDelete, + Scope: ScopeAllPluginsPlugins, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + // PluginMetas + pluginsMetaReader := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:plugins.pluginsmeta:reader", + DisplayName: "Plugin Metas Reader", + Description: "Read and list plugin metadata.", + Group: "Plugins", + Permissions: []accesscontrol.Permission{ + { + Action: ActionPluginsPluginsMetaRead, + Scope: ScopeAllPluginsPluginsMeta, + }, + }, + }, + Grants: []string{string(org.RoleViewer), string(org.RoleEditor), string(org.RoleAdmin)}, + } + + pluginsMetaWriter := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:plugins.pluginsmeta:writer", + DisplayName: "Plugin Metas Writer", + Description: "Create, update and delete plugin metadata.", + Group: "Plugins", + Permissions: []accesscontrol.Permission{ + { + Action: ActionPluginsPluginsMetaCreate, + Scope: ScopeAllPluginsPluginsMeta, + }, + { + Action: ActionPluginsPluginsMetaRead, + Scope: ScopeAllPluginsPluginsMeta, + }, + { + Action: ActionPluginsPluginsMetaWrite, + Scope: ScopeAllPluginsPluginsMeta, + }, + { + Action: ActionPluginsPluginsMetaDelete, + Scope: ScopeAllPluginsPluginsMeta, + }, + }, + }, + Grants: []string{string(org.RoleAdmin)}, + } + + return service.DeclareFixedRoles( + pluginsReader, + pluginsWriter, + pluginsMetaReader, + pluginsMetaWriter, + ) +} diff --git a/pkg/registry/apps/plugins/register.go b/pkg/registry/apps/plugins/register.go index 5d452cbe67c..6831d31ef9b 100644 --- a/pkg/registry/apps/plugins/register.go +++ b/pkg/registry/apps/plugins/register.go @@ -1,14 +1,16 @@ package plugins import ( + "fmt" "os" - "k8s.io/apiserver/pkg/authorization/authorizer" - + authlib "github.com/grafana/authlib/types" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" + "k8s.io/apiserver/pkg/authorization/authorizer" pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app" "github.com/grafana/grafana/apps/plugins/pkg/app/meta" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" ) @@ -18,10 +20,14 @@ var ( ) type AppInstaller struct { - appsdkapiserver.AppInstaller + *pluginsapp.PluginAppInstaller } -func ProvideAppInstaller() (*AppInstaller, error) { +func ProvideAppInstaller(accessControlService accesscontrol.Service, accessClient authlib.AccessClient) (*AppInstaller, error) { + if err := registerAccessControlRoles(accessControlService); err != nil { + return nil, fmt.Errorf("registering access control roles: %w", err) + } + grafanaComAPIURL := os.Getenv("GRAFANA_COM_API_URL") if grafanaComAPIURL == "" { grafanaComAPIURL = "https://grafana.com/api/plugins" @@ -36,12 +42,13 @@ func ProvideAppInstaller() (*AppInstaller, error) { return nil, err } + i.WithAccessChecker(accessClient) + return &AppInstaller{ - AppInstaller: i, + PluginAppInstaller: i, }, nil } -// GetAuthorizer returns the authorizer for the plugins app. -func (p *AppInstaller) GetAuthorizer() authorizer.Authorizer { +func (a *AppInstaller) GetAuthorizer() authorizer.Authorizer { return pluginsapp.GetAuthorizer() } diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index e920bdbec61..d1a2ecee64f 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -783,7 +783,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - appInstaller, err := plugins.ProvideAppInstaller() + appInstaller, err := plugins.ProvideAppInstaller(acimplService, accessClient) if err != nil { return nil, err } @@ -1436,7 +1436,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - appInstaller, err := plugins.ProvideAppInstaller() + appInstaller, err := plugins.ProvideAppInstaller(acimplService, accessClient) if err != nil { return nil, err } diff --git a/pkg/services/accesscontrol/permreg/permreg.go b/pkg/services/accesscontrol/permreg/permreg.go index 5d025a1258b..c9f010c2909 100644 --- a/pkg/services/accesscontrol/permreg/permreg.go +++ b/pkg/services/accesscontrol/permreg/permreg.go @@ -84,6 +84,8 @@ func newPermissionRegistry() *permissionRegistry { "annotations": "annotations:type:", "orgs": "orgs:id:", "plugins": "plugins:id:", + "plugins.plugins": "plugins.plugins:uid:", + "plugins.pluginsmeta": "plugins.pluginsmeta:uid:", "provisioners": "provisioners:", "reports": "reports:id:", "permissions": "permissions:type:", From d1cbef9157ef70282e56b83b1a2f91ac418c6caa Mon Sep 17 00:00:00 2001 From: Charandas <542168+charandas@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:53:31 -0800 Subject: [PATCH 337/378] K8s: use runtime config for API Builders (#114601) * Reapply "K8s: read resource configs from API Enablement for API Builders" (#114475) This reverts commit 4130bd9cd300ec7ce0fb492b3a7229968c9167b4. * revert part that broke things * FF service changes are gonna come later --- pkg/services/apiserver/builder/helper.go | 85 +++++++++++++------ pkg/services/apiserver/builder/openapi.go | 19 ++++- .../apiserver/builder/request_handler.go | 10 ++- pkg/services/apiserver/service.go | 8 +- 4 files changed, 90 insertions(+), 32 deletions(-) diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index 603c49f3cb2..a76a01dffba 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -22,6 +22,7 @@ import ( k8srequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/generic" genericapiserver "k8s.io/apiserver/pkg/server" + serverstorage "k8s.io/apiserver/pkg/server/storage" "k8s.io/apiserver/pkg/util/openapi" k8sscheme "k8s.io/client-go/kubernetes/scheme" k8stracing "k8s.io/component-base/tracing" @@ -78,7 +79,9 @@ func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus. delegateHandler, c.LoopbackClientConfig, builders, - reg) + reg, + c.MergedResourceConfig, + ) if err != nil { panic(fmt.Sprintf("could not build the request handler for specified API builders: %s", err.Error())) } @@ -105,6 +108,8 @@ func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus. } } +// SetupConfig sets up the server config for the API server +// specify isAggregator=true, if the chain is being constructed for kube-aggregator func SetupConfig( scheme *runtime.Scheme, serverConfig *genericapiserver.RecommendedConfig, @@ -114,6 +119,7 @@ func SetupConfig( gvs []schema.GroupVersion, additionalOpenAPIDefGetters []common.GetOpenAPIDefinitions, reg prometheus.Registerer, + apiResourceConfig *serverstorage.ResourceConfig, ) error { serverConfig.AdmissionControl = NewAdmissionFromBuilders(builders) defsGetter := GetOpenAPIDefinitions(builders, additionalOpenAPIDefGetters...) @@ -126,7 +132,7 @@ func SetupConfig( openapinamer.NewDefinitionNamer(scheme, k8sscheme.Scheme)) // Add the custom routes to service discovery - serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs) + serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs, apiResourceConfig) serverConfig.OpenAPIV3Config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) { meta := r.Metadata() kind := "" @@ -287,6 +293,7 @@ func InstallAPIs( features featuremgmt.FeatureToggles, dualWriterMetrics *grafanarest.DualWriterMetrics, builderMetrics *BuilderMetrics, + apiResourceConfig *serverstorage.ResourceConfig, ) error { // dual writing is only enabled when the storage type is not legacy. // this is needed to support setting a default RESTOptionsGetter for new APIs that don't @@ -401,34 +408,9 @@ func InstallAPIs( for group, buildersForGroup := range buildersGroupMap { g := genericapiserver.NewDefaultAPIGroupInfo(group, scheme, metav1.ParameterCodec, codecs) for _, b := range buildersForGroup { - if err := b.UpdateAPIGroupInfo(&g, APIGroupOptions{ - Scheme: scheme, - OptsGetter: optsGetter, - DualWriteBuilder: dualWrite, - MetricsRegister: reg, - StorageOptsRegister: optsregister, - StorageOpts: storageOpts, - }); err != nil { + if err := installAPIGroupsForBuilder(&g, group, b, apiResourceConfig, scheme, optsGetter, dualWrite, reg, optsregister, storageOpts, features); err != nil { return err } - if len(g.PrioritizedVersions) < 1 { - continue - } - - // if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed - //nolint:staticcheck // not yet migrated to OpenFeature - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok { - for name := range resources { - if !allowRegisteringResourceByInfo(b.AllowedV0Alpha1Resources(), name) { - delete(resources, name) - } - } - if len(resources) == 0 { - delete(g.VersionedResourcesStorageMap, "v0alpha1") - } - } - } } // skip installing the group if there are no resources left after filtering @@ -445,6 +427,53 @@ func InstallAPIs( return nil } +func installAPIGroupsForBuilder(g *genericapiserver.APIGroupInfo, group string, b APIGroupBuilder, apiResourceConfig *serverstorage.ResourceConfig, scheme *runtime.Scheme, + optsGetter generic.RESTOptionsGetter, dualWrite grafanarest.DualWriteBuilder, reg prometheus.Registerer, optsregister apistore.StorageOptionsRegister, + storageOpts *options.StorageOptions, features featuremgmt.FeatureToggles) error { + if err := b.UpdateAPIGroupInfo(g, APIGroupOptions{ + Scheme: scheme, + OptsGetter: optsGetter, + DualWriteBuilder: dualWrite, + MetricsRegister: reg, + StorageOptsRegister: optsregister, + StorageOpts: storageOpts, + }); err != nil { + return err + } + if len(g.PrioritizedVersions) < 1 { + return nil + } + + // filter out api groups that are disabled in APIEnablementOptions + for version := range g.VersionedResourcesStorageMap { + gvr := schema.GroupVersionResource{ + Group: group, + Version: version, + } + if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) { + klog.InfoS("Skipping storage for disabled resource", "gvr", gvr.String()) + delete(g.VersionedResourcesStorageMap, version) + } + } + + // if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed + //nolint:staticcheck // not yet migrated to OpenFeature + if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { + if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok { + for name := range resources { + if !allowRegisteringResourceByInfo(b.AllowedV0Alpha1Resources(), name) { + delete(resources, name) + } + } + if len(resources) == 0 { + delete(g.VersionedResourcesStorageMap, "v0alpha1") + } + } + } + + return nil +} + // AddPostStartHooks adds post start hooks to a generic API server config func AddPostStartHooks( config *genericapiserver.RecommendedConfig, diff --git a/pkg/services/apiserver/builder/openapi.go b/pkg/services/apiserver/builder/openapi.go index 9cb7cc17f7c..6d3b33f4baf 100644 --- a/pkg/services/apiserver/builder/openapi.go +++ b/pkg/services/apiserver/builder/openapi.go @@ -9,6 +9,8 @@ import ( apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime/schema" + serverstorage "k8s.io/apiserver/pkg/server/storage" + "k8s.io/klog/v2" openapi "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" spec "k8s.io/kube-openapi/pkg/validation/spec" @@ -76,6 +78,7 @@ func addBuilderRoutes( targetGroupVersion schema.GroupVersion, openAPISpec *spec3.OpenAPI, apiGroupBuilders []APIGroupBuilder, + apiResourceConfig *serverstorage.ResourceConfig, ) (*spec3.OpenAPI, error) { for _, apiGroupBuilder := range apiGroupBuilders { // Optionally include raw http handlers for all builders @@ -107,12 +110,24 @@ func addBuilderRoutes( } } } + + // filter out api groups that are disabled in APIEnablementOptions + for path := range openAPISpec.Paths.Paths { + if strings.HasPrefix(path, "/apis/"+targetGroupVersion.String()+"/") { + gv := targetGroupVersion.WithResource("") + if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gv) { + klog.InfoS("removing openapi routes for disabled resource", "gv", gv.String()) + delete(openAPISpec.Paths.Paths, path) + } + } + } + return openAPISpec, nil } // Modify the OpenAPI spec to include the additional routes. // nolint:gocyclo -func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) { +func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion, apiResourceConfig *serverstorage.ResourceConfig) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) { return func(s *spec3.OpenAPI) (*spec3.OpenAPI, error) { if s.Paths == nil { return s, nil @@ -227,7 +242,7 @@ func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []s } } } - return addBuilderRoutes(gv, ©, builders) + return addBuilderRoutes(gv, ©, builders, apiResourceConfig) } } return s, nil diff --git a/pkg/services/apiserver/builder/request_handler.go b/pkg/services/apiserver/builder/request_handler.go index 80fbf6d0eaa..50761f1d42c 100644 --- a/pkg/services/apiserver/builder/request_handler.go +++ b/pkg/services/apiserver/builder/request_handler.go @@ -6,7 +6,9 @@ import ( "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" + serverstorage "k8s.io/apiserver/pkg/server/storage" restclient "k8s.io/client-go/rest" + klog "k8s.io/klog/v2" "k8s.io/kube-openapi/pkg/spec3" ) @@ -14,7 +16,7 @@ type requestHandler struct { router *mux.Router } -func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer) (http.Handler, error) { +func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer, apiResourceConfig *serverstorage.ResourceConfig) (http.Handler, error) { useful := false // only true if any routes exist anywhere router := mux.NewRouter() @@ -27,6 +29,12 @@ func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient } for _, gv := range GetGroupVersions(builder) { + // filter out api groups that are disabled in APIEnablementOptions + gvr := gv.WithResource("") + if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) { + klog.InfoS("Skipping custom route handler for disabled group version", "gv", gv.String()) + continue + } routes := provider.GetAPIRoutes(gv) if routes == nil { continue diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 28da416536c..605016c112d 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -316,7 +316,11 @@ func (s *service) start(ctx context.Context) error { s.cfg.BuildBranch, ) - if err := o.APIEnablementOptions.ApplyTo(&serverConfig.Config, appinstaller.NewAPIResourceConfig(s.appInstallers), s.scheme); err != nil { + apiResourceConfig := appinstaller.NewAPIResourceConfig(s.appInstallers) + // add the builder group versions to the api resource config + apiResourceConfig.EnableVersions(groupVersions...) + + if err := o.APIEnablementOptions.ApplyTo(&serverConfig.Config, apiResourceConfig, s.scheme); err != nil { return err } @@ -359,6 +363,7 @@ func (s *service) start(ctx context.Context) error { groupVersions, defGetters, s.metrics, + apiResourceConfig, ) if err != nil { return err @@ -400,6 +405,7 @@ func (s *service) start(ctx context.Context) error { s.features, s.dualWriterMetrics, s.builderMetrics, + apiResourceConfig, ) if err != nil { return err From 0f9d0317dc2a2e2b362e48e25acbd992633029bd Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Sat, 6 Dec 2025 00:40:23 +0000 Subject: [PATCH 338/378] I18n: Download translations from Crowdin (#114938) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 7 +++++-- public/locales/de-DE/grafana.json | 7 +++++-- public/locales/es-ES/grafana.json | 7 +++++-- public/locales/fr-FR/grafana.json | 7 +++++-- public/locales/hu-HU/grafana.json | 7 +++++-- public/locales/id-ID/grafana.json | 7 +++++-- public/locales/it-IT/grafana.json | 7 +++++-- public/locales/ja-JP/grafana.json | 7 +++++-- public/locales/ko-KR/grafana.json | 7 +++++-- public/locales/nl-NL/grafana.json | 7 +++++-- public/locales/pl-PL/grafana.json | 7 +++++-- public/locales/pt-BR/grafana.json | 7 +++++-- public/locales/pt-PT/grafana.json | 7 +++++-- public/locales/ru-RU/grafana.json | 7 +++++-- public/locales/sv-SE/grafana.json | 7 +++++-- public/locales/tr-TR/grafana.json | 7 +++++-- public/locales/zh-Hans/grafana.json | 7 +++++-- public/locales/zh-Hant/grafana.json | 7 +++++-- 18 files changed, 90 insertions(+), 36 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 99f381a244e..e90a588053c 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -4851,7 +4851,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Hodnoty oddělené čárkou" }, @@ -12502,7 +12504,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index f84c77dd97f..e0a149baab7 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Werte werden durch Komma getrennt" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 70b7097bcc7..ff65f840041 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Valores separados por coma" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 098050412e8..01bbb43fff9 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Valeurs séparées par une virgule" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 28130cb2ec2..146b2a78d04 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Értékek vesszővel elválasztva" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 72598112d52..7b8e043e947 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -4791,7 +4791,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Nilai dipisahkan dengan koma" }, @@ -12343,7 +12345,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 5b8c69dff0b..68364a78d6f 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Valori separati da virgola" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 14c46a83f7f..c023581e3cf 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -4791,7 +4791,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "カンマで区切った値" }, @@ -12343,7 +12345,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index df4cad85150..e9cb03b8350 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -4791,7 +4791,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "쉼표로 구분된 값" }, @@ -12343,7 +12345,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 126ad3f53f9..7872baf058a 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Waarden gescheiden door komma" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 0f441a67b12..acd3b23e77f 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -4851,7 +4851,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Wartości rozdzielone przecinkami" }, @@ -12502,7 +12504,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index a96c531b03c..e091960845a 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Valores separados por vírgula" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index dc7e3c11c34..3a69ffa3194 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Valores separados por vírgulas" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index cb1af71f6ed..a85c8b00dd5 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -4851,7 +4851,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Значения, разделенные запятыми" }, @@ -12502,7 +12504,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 883cac40ddf..733bfdd230b 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Värden åtskilda med kommatecken" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 2037475ec8a..4b26d75fcb5 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -4811,7 +4811,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "Virgülle ayrılmış değerler" }, @@ -12396,7 +12398,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 8d34dcd8286..45cbea59c45 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -4791,7 +4791,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "以逗号分隔的值" }, @@ -12343,7 +12345,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 97699c7bdd8..5548f9f3d56 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -4791,7 +4791,9 @@ }, "variable": { "custom-options": { - "close": "", + "apply": "", + "change-value": "", + "discard": "", "modal-title": "", "values": "以逗號分隔的值" }, @@ -12343,7 +12345,8 @@ "bar-glow": "", "center-glow": "", "rounded-bars": "", - "spotlight": "" + "spotlight": "", + "spotlight-tooltip": "" }, "gradient": "", "gradient-auto": "", From e9ba45ca4fd7f28d34cd2590762b75c6a7afd7c6 Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Sat, 6 Dec 2025 08:34:18 +0100 Subject: [PATCH 339/378] Update grafana-app-sdk to v0.48.5 (#114810) Co-authored-by: Owen Smallwood Co-authored-by: Ryan McKinley --- apps/advisor/go.mod | 2 +- apps/advisor/go.sum | 4 +- apps/alerting/alertenrichment/go.mod | 2 +- apps/alerting/alertenrichment/go.sum | 4 +- apps/alerting/historian/go.mod | 2 +- apps/alerting/historian/go.sum | 4 +- apps/alerting/notifications/go.mod | 2 +- apps/alerting/notifications/go.sum | 4 +- .../v0alpha1/receiver_object_gen.go | 6 + .../v0alpha1/receiver_schema_gen.go | 2 +- .../v0alpha1/routingtree_object_gen.go | 6 + .../v0alpha1/routingtree_schema_gen.go | 2 +- .../v0alpha1/templategroup_object_gen.go | 6 + .../v0alpha1/templategroup_schema_gen.go | 2 +- .../v0alpha1/timeinterval_object_gen.go | 6 + .../v0alpha1/timeinterval_schema_gen.go | 2 +- apps/alerting/rules/go.mod | 2 +- apps/alerting/rules/go.sum | 4 +- apps/annotation/go.mod | 2 +- apps/annotation/go.sum | 4 +- apps/collections/go.mod | 2 +- apps/collections/go.sum | 4 +- apps/correlations/go.mod | 2 +- apps/correlations/go.sum | 4 +- apps/dashboard/go.mod | 2 +- apps/dashboard/go.sum | 4 +- .../v0alpha1/dashboard_object_gen.go | 7 ++ .../v0alpha1/dashboard_schema_gen.go | 2 +- .../dashboard/v0alpha1/snapshot_object_gen.go | 6 + .../dashboard/v0alpha1/snapshot_schema_gen.go | 2 +- .../dashboard/v1beta1/dashboard_object_gen.go | 7 ++ .../dashboard/v1beta1/dashboard_schema_gen.go | 2 +- .../v2alpha1/dashboard_object_gen.go | 7 ++ .../v2alpha1/dashboard_schema_gen.go | 2 +- .../dashboard/v2beta1/dashboard_object_gen.go | 7 ++ .../dashboard/v2beta1/dashboard_schema_gen.go | 2 +- apps/example/go.mod | 2 +- apps/example/go.sum | 4 +- apps/folder/go.mod | 27 +++- apps/folder/go.sum | 69 ++++++++++- .../apis/folder/v1beta1/folder_object_gen.go | 6 + .../apis/folder/v1beta1/folder_schema_gen.go | 2 +- .../pkg/apis/manifestdata/folder_manifest.go | 116 ++++++++++++++++++ apps/iam/go.mod | 2 +- apps/iam/go.sum | 4 +- apps/investigations/go.mod | 2 +- apps/investigations/go.sum | 4 +- apps/logsdrilldown/go.mod | 2 +- apps/logsdrilldown/go.sum | 4 +- apps/playlist/go.mod | 2 +- apps/playlist/go.sum | 4 +- apps/plugins/go.mod | 17 +-- apps/plugins/go.sum | 24 ++-- apps/preferences/go.mod | 2 +- apps/preferences/go.sum | 4 +- apps/provisioning/go.mod | 2 +- apps/provisioning/go.sum | 4 +- apps/sdk.mk | 2 +- apps/secret/go.mod | 2 +- apps/secret/go.sum | 4 +- apps/shorturl/go.mod | 2 +- apps/shorturl/go.sum | 4 +- go.mod | 2 +- go.sum | 4 +- go.work.sum | 3 +- 65 files changed, 365 insertions(+), 91 deletions(-) create mode 100644 apps/folder/pkg/apis/manifestdata/folder_manifest.go diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 2318df205ce..941d8b9bc0f 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -8,7 +8,7 @@ require ( github.com/google/go-github/v70 v70.0.0 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 76208d30349..4695785bbd1 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -618,8 +618,8 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= diff --git a/apps/alerting/alertenrichment/go.mod b/apps/alerting/alertenrichment/go.mod index 95466bad6bb..bb020e52bff 100644 --- a/apps/alerting/alertenrichment/go.mod +++ b/apps/alerting/alertenrichment/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/alertenrichment go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 diff --git a/apps/alerting/alertenrichment/go.sum b/apps/alerting/alertenrichment/go.sum index 5dff965c88b..ef36568611c 100644 --- a/apps/alerting/alertenrichment/go.sum +++ b/apps/alerting/alertenrichment/go.sum @@ -23,8 +23,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 h1:PgMfX4OPENz/iXmtDDIW9+poZY4UD0hhmXm7flVclDo= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28/go.mod h1:av5N0Naq+8VV9MLF7zAkihy/mVq5UbS2EvRSJukDHlY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/apps/alerting/historian/go.mod b/apps/alerting/historian/go.mod index 5963ac8139c..21ad42c90af 100644 --- a/apps/alerting/historian/go.mod +++ b/apps/alerting/historian/go.mod @@ -6,7 +6,7 @@ require ( github.com/go-kit/log v0.2.1 github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/prometheus/client_golang v1.23.2 github.com/spf13/pflag v1.0.10 diff --git a/apps/alerting/historian/go.sum b/apps/alerting/historian/go.sum index 9e44308979b..b4d2d1dc2e2 100644 --- a/apps/alerting/historian/go.sum +++ b/apps/alerting/historian/go.sum @@ -216,14 +216,14 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmFAlqnWsXoRyUwSa2GHNEMSEDKGKfQ4= github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLmAhwA4m6iGUD4w1YkydEWWjazn9qxCFT8W0= diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 80dd39f9ca7..f72c4c9775b 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/notifications go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 6ab34248ef8..57b099ab2f1 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -71,8 +71,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go index 00bb1a34745..de0a0d5320f 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go @@ -23,6 +23,12 @@ type Receiver struct { Spec ReceiverSpec `json:"spec" yaml:"spec"` } +func NewReceiver() *Receiver { + return &Receiver{ + Spec: *NewReceiverSpec(), + } +} + func (o *Receiver) GetSpec() any { return o.Spec } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go index ea4e3b8e363..27047b00601 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go @@ -12,7 +12,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &Receiver{}, &ReceiverList{}, resource.WithKind("Receiver"), + schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewReceiver(), &ReceiverList{}, resource.WithKind("Receiver"), resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ FieldSelector: "spec.title", FieldValueFunc: func(o resource.Object) (string, error) { diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go index e59f0dada5c..354e009d77f 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go @@ -23,6 +23,12 @@ type RoutingTree struct { Spec RoutingTreeSpec `json:"spec" yaml:"spec"` } +func NewRoutingTree() *RoutingTree { + return &RoutingTree{ + Spec: *NewRoutingTreeSpec(), + } +} + func (o *RoutingTree) GetSpec() any { return o.Spec } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_schema_gen.go index 6838c9cdebd..2a1812a2846 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaRoutingTree = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &RoutingTree{}, &RoutingTreeList{}, resource.WithKind("RoutingTree"), + schemaRoutingTree = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewRoutingTree(), &RoutingTreeList{}, resource.WithKind("RoutingTree"), resource.WithPlural("routingtrees"), resource.WithScope(resource.NamespacedScope)) kindRoutingTree = resource.Kind{ Schema: schemaRoutingTree, diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go index 0866bcb258c..d755a887a3f 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go @@ -23,6 +23,12 @@ type TemplateGroup struct { Spec TemplateGroupSpec `json:"spec" yaml:"spec"` } +func NewTemplateGroup() *TemplateGroup { + return &TemplateGroup{ + Spec: *NewTemplateGroupSpec(), + } +} + func (o *TemplateGroup) GetSpec() any { return o.Spec } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_schema_gen.go index 0be8cb1c6de..ba92e2c4c4c 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaTemplateGroup = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TemplateGroup{}, &TemplateGroupList{}, resource.WithKind("TemplateGroup"), + schemaTemplateGroup = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewTemplateGroup(), &TemplateGroupList{}, resource.WithKind("TemplateGroup"), resource.WithPlural("templategroups"), resource.WithScope(resource.NamespacedScope)) kindTemplateGroup = resource.Kind{ Schema: schemaTemplateGroup, diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go index 0ef813ee40c..e87b49dc958 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go @@ -23,6 +23,12 @@ type TimeInterval struct { Spec TimeIntervalSpec `json:"spec" yaml:"spec"` } +func NewTimeInterval() *TimeInterval { + return &TimeInterval{ + Spec: *NewTimeIntervalSpec(), + } +} + func (o *TimeInterval) GetSpec() any { return o.Spec } diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_schema_gen.go index 715bfbc0fe7..d342cb90637 100644 --- a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaTimeInterval = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TimeInterval{}, &TimeIntervalList{}, resource.WithKind("TimeInterval"), + schemaTimeInterval = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewTimeInterval(), &TimeIntervalList{}, resource.WithKind("TimeInterval"), resource.WithPlural("timeintervals"), resource.WithScope(resource.NamespacedScope)) kindTimeInterval = resource.Kind{ Schema: schemaTimeInterval, diff --git a/apps/alerting/rules/go.mod b/apps/alerting/rules/go.mod index 7286ccf5376..63da00536a6 100644 --- a/apps/alerting/rules/go.mod +++ b/apps/alerting/rules/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/rules go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/prometheus/common v0.67.3 k8s.io/apimachinery v0.34.2 diff --git a/apps/alerting/rules/go.sum b/apps/alerting/rules/go.sum index c447da4e325..3df8e8b5ebe 100644 --- a/apps/alerting/rules/go.sum +++ b/apps/alerting/rules/go.sum @@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= diff --git a/apps/annotation/go.mod b/apps/annotation/go.mod index a042d852413..946c42fe38f 100644 --- a/apps/annotation/go.mod +++ b/apps/annotation/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/annotation go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 diff --git a/apps/annotation/go.sum b/apps/annotation/go.sum index c447da4e325..3df8e8b5ebe 100644 --- a/apps/annotation/go.sum +++ b/apps/annotation/go.sum @@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= diff --git a/apps/collections/go.mod b/apps/collections/go.mod index 81872ad4505..00575d7e6d4 100644 --- a/apps/collections/go.mod +++ b/apps/collections/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/collections go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 diff --git a/apps/collections/go.sum b/apps/collections/go.sum index 22bf6a8cbcd..75a19848d73 100644 --- a/apps/collections/go.sum +++ b/apps/collections/go.sum @@ -33,8 +33,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= diff --git a/apps/correlations/go.mod b/apps/correlations/go.mod index e1c9242aa14..29bc91e70a6 100644 --- a/apps/correlations/go.mod +++ b/apps/correlations/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/correlations go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 diff --git a/apps/correlations/go.sum b/apps/correlations/go.sum index c447da4e325..3df8e8b5ebe 100644 --- a/apps/correlations/go.sum +++ b/apps/correlations/go.sum @@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 4baf37a3657..0a19120fe80 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -5,7 +5,7 @@ go 1.25.5 require ( cuelang.org/go v0.11.1 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 9c8cde10af6..0faeaaf78ba 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -85,8 +85,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_object_gen.go index a267e0c8df8..ac8a0a61685 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_object_gen.go @@ -25,6 +25,13 @@ type Dashboard struct { Status DashboardStatus `json:"status" yaml:"status"` } +func NewDashboard() *Dashboard { + return &Dashboard{ + Spec: *NewDashboardSpec(), + Status: *NewDashboardStatus(), + } +} + func (o *Dashboard) GetSpec() any { return o.Spec } diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_schema_gen.go index 5b2da44ec05..1ec0884202f 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_schema_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"), + schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", NewDashboard(), &DashboardList{}, resource.WithKind("Dashboard"), resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope)) kindDashboard = resource.Kind{ Schema: schemaDashboard, diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_object_gen.go index d917cebc0bf..64924eac264 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_object_gen.go @@ -23,6 +23,12 @@ type Snapshot struct { Spec SnapshotSpec `json:"spec" yaml:"spec"` } +func NewSnapshot() *Snapshot { + return &Snapshot{ + Spec: *NewSnapshotSpec(), + } +} + func (o *Snapshot) GetSpec() any { return o.Spec } diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_schema_gen.go index b6086c5fd1f..596c5bb2890 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_schema_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaSnapshot = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", &Snapshot{}, &SnapshotList{}, resource.WithKind("Snapshot"), + schemaSnapshot = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", NewSnapshot(), &SnapshotList{}, resource.WithKind("Snapshot"), resource.WithPlural("snapshots"), resource.WithScope(resource.NamespacedScope)) kindSnapshot = resource.Kind{ Schema: schemaSnapshot, diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_object_gen.go index be021b5f003..35bb8900ab0 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_object_gen.go @@ -25,6 +25,13 @@ type Dashboard struct { Status DashboardStatus `json:"status" yaml:"status"` } +func NewDashboard() *Dashboard { + return &Dashboard{ + Spec: *NewDashboardSpec(), + Status: *NewDashboardStatus(), + } +} + func (o *Dashboard) GetSpec() any { return o.Spec } diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_schema_gen.go index e944e0afc33..006312837e0 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_schema_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v1beta1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"), + schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v1beta1", NewDashboard(), &DashboardList{}, resource.WithKind("Dashboard"), resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope)) kindDashboard = resource.Kind{ Schema: schemaDashboard, diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_object_gen.go index 99cf7df0da9..6a06594656e 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_object_gen.go @@ -25,6 +25,13 @@ type Dashboard struct { Status DashboardStatus `json:"status" yaml:"status"` } +func NewDashboard() *Dashboard { + return &Dashboard{ + Spec: *NewDashboardSpec(), + Status: *NewDashboardStatus(), + } +} + func (o *Dashboard) GetSpec() any { return o.Spec } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_schema_gen.go index 136698cf70f..1a5f27d0fb5 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_schema_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2alpha1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"), + schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2alpha1", NewDashboard(), &DashboardList{}, resource.WithKind("Dashboard"), resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope)) kindDashboard = resource.Kind{ Schema: schemaDashboard, diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_object_gen.go index 5076b7d9b0c..bb64a321a1d 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_object_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_object_gen.go @@ -25,6 +25,13 @@ type Dashboard struct { Status DashboardStatus `json:"status" yaml:"status"` } +func NewDashboard() *Dashboard { + return &Dashboard{ + Spec: *NewDashboardSpec(), + Status: *NewDashboardStatus(), + } +} + func (o *Dashboard) GetSpec() any { return o.Spec } diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_schema_gen.go index 30c2237ca31..35d87fd07f7 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_schema_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2beta1", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"), + schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2beta1", NewDashboard(), &DashboardList{}, resource.WithKind("Dashboard"), resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope)) kindDashboard = resource.Kind{ Schema: schemaDashboard, diff --git a/apps/example/go.mod b/apps/example/go.mod index deb8763474c..d63d23be302 100644 --- a/apps/example/go.mod +++ b/apps/example/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/example go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe k8s.io/apimachinery v0.34.2 diff --git a/apps/example/go.sum b/apps/example/go.sum index 4a43a44d46c..70c83a9fe37 100644 --- a/apps/example/go.sum +++ b/apps/example/go.sum @@ -56,8 +56,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe h1:pPoFj2bQKDBg5EyEdOU+Jn+0hQN+M775Qihk73RbdSs= diff --git a/apps/folder/go.mod b/apps/folder/go.mod index c40b11f7add..476c6949c73 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -3,42 +3,67 @@ module github.com/grafana/grafana/apps/folder go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect github.com/go-openapi/jsonreference v0.21.2 // indirect github.com/go-openapi/swag v0.23.1 // indirect github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/client-go v0.34.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/folder/go.sum b/apps/folder/go.sum index 286d9142c77..d8454185267 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -1,3 +1,7 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -6,6 +10,8 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= @@ -16,6 +22,8 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -23,20 +31,33 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -45,9 +66,29 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= @@ -58,10 +99,20 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -77,16 +128,24 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -100,12 +159,18 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= +k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= +k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= diff --git a/apps/folder/pkg/apis/folder/v1beta1/folder_object_gen.go b/apps/folder/pkg/apis/folder/v1beta1/folder_object_gen.go index 226af606f6f..6fabb8f8958 100644 --- a/apps/folder/pkg/apis/folder/v1beta1/folder_object_gen.go +++ b/apps/folder/pkg/apis/folder/v1beta1/folder_object_gen.go @@ -23,6 +23,12 @@ type Folder struct { Spec FolderSpec `json:"spec" yaml:"spec"` } +func NewFolder() *Folder { + return &Folder{ + Spec: *NewFolderSpec(), + } +} + func (o *Folder) GetSpec() any { return o.Spec } diff --git a/apps/folder/pkg/apis/folder/v1beta1/folder_schema_gen.go b/apps/folder/pkg/apis/folder/v1beta1/folder_schema_gen.go index f0d4fffe6b0..e626e4773ee 100644 --- a/apps/folder/pkg/apis/folder/v1beta1/folder_schema_gen.go +++ b/apps/folder/pkg/apis/folder/v1beta1/folder_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaFolder = resource.NewSimpleSchema("folder.grafana.app", "v1beta1", &Folder{}, &FolderList{}, resource.WithKind("Folder"), + schemaFolder = resource.NewSimpleSchema("folder.grafana.app", "v1beta1", NewFolder(), &FolderList{}, resource.WithKind("Folder"), resource.WithPlural("folders"), resource.WithScope(resource.NamespacedScope)) kindFolder = resource.Kind{ Schema: schemaFolder, diff --git a/apps/folder/pkg/apis/manifestdata/folder_manifest.go b/apps/folder/pkg/apis/manifestdata/folder_manifest.go new file mode 100644 index 00000000000..7c053e52c38 --- /dev/null +++ b/apps/folder/pkg/apis/manifestdata/folder_manifest.go @@ -0,0 +1,116 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package manifestdata + +import ( + "fmt" + "strings" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" + + v1beta1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" +) + +var appManifestData = app.ManifestData{ + AppName: "folder", + Group: "folder.grafana.app", + PreferredVersion: "v1beta1", + Versions: []app.ManifestVersion{ + { + Name: "v1beta1", + Served: true, + Kinds: []app.ManifestVersionKind{ + { + Kind: "Folder", + Plural: "Folders", + Scope: "Namespaced", + Conversion: false, + }, + }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + Schemas: map[string]spec.Schema{}, + }, + }, + }, +} + +func LocalManifest() app.Manifest { + return app.NewEmbeddedManifest(appManifestData) +} + +func RemoteManifest() app.Manifest { + return app.NewAPIServerManifest("folder") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "Folder/v1beta1": v1beta1.FolderKind(), +} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} + +var customRouteToGoResponseType = map[string]any{} + +// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. +// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. +// If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) +func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoParamsType = map[string]runtime.Object{} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/iam/go.mod b/apps/iam/go.mod index bfe1fa1c50d..474779e0efe 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -52,7 +52,7 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/apps/folder v0.0.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 4c25bd92d43..7f2fd8f462f 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -835,8 +835,8 @@ github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f h1:5xkjl5Y/j2QefJKO github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f/go.mod h1:+O5QxOwwgP10jedZHapzXY+IPKTnzHBtIs5UUb9G+kI= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe h1:q+QaVANzNZxvTovycpQvDTfsNZ2rHh4XIIaccMnrIR4= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 0faf2c40efb..3844fec2689 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/investigations go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 k8s.io/apimachinery v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index c447da4e325..3df8e8b5ebe 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= diff --git a/apps/logsdrilldown/go.mod b/apps/logsdrilldown/go.mod index b70fb8a42f5..2a35278d19a 100644 --- a/apps/logsdrilldown/go.mod +++ b/apps/logsdrilldown/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/logsdrilldown go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 diff --git a/apps/logsdrilldown/go.sum b/apps/logsdrilldown/go.sum index c447da4e325..3df8e8b5ebe 100644 --- a/apps/logsdrilldown/go.sum +++ b/apps/logsdrilldown/go.sum @@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 4bdbe8c003c..ae38e3d0cd3 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/playlist go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 k8s.io/apimachinery v0.34.2 k8s.io/client-go v0.34.2 k8s.io/klog/v2 v2.130.1 diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index c447da4e325..3df8e8b5ebe 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -48,8 +48,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index c55674cc950..669c7c46844 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -10,8 +10,9 @@ replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver require ( github.com/emicklei/go-restful/v3 v3.13.0 + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 @@ -59,7 +60,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect github.com/go-openapi/jsonreference v0.21.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/go-openapi/swag/jsonname v0.25.1 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/go-test/deep v1.1.1 // indirect @@ -75,9 +76,8 @@ require ( github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 // indirect + github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect - github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect github.com/grafana/grafana-aws-sdk v1.3.0 // indirect @@ -142,7 +142,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/alertmanager v0.28.0 // indirect + github.com/prometheus/alertmanager v0.28.2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.3 // indirect @@ -194,8 +194,8 @@ require ( golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect @@ -215,3 +215,6 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) + +// Use our fork of the upstream Alertmanager. +replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index d582270e43b..1d7387b28b3 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -110,8 +110,8 @@ github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= @@ -174,8 +174,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 h1:nbxKRtrbuhvOYmI2RhOYauHRJCtpR+vTNIgg1lFUCws= -github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181/go.mod h1:VtPNIFlEOJPPEc13Ax6ZTbNV3M/sAzLID72YjgzOPVA= +github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmFAlqnWsXoRyUwSa2GHNEMSEDKGKfQ4= +github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= @@ -184,8 +184,8 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= @@ -196,6 +196,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA= @@ -368,8 +370,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/alertmanager v0.28.0 h1:sLN+6HhZet8hrbmGHLAHWsTXgZSVCvq9Ix3U3wvivqc= -github.com/prometheus/alertmanager v0.28.0/go.mod h1:/okSnb2LlodbMlRoOWQEKtqI/coOo2NKZDm2Hu9QHLQ= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= @@ -611,10 +611,10 @@ gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= +google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= diff --git a/apps/preferences/go.mod b/apps/preferences/go.mod index 9a5c8088c0f..661002a3b59 100644 --- a/apps/preferences/go.mod +++ b/apps/preferences/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/preferences go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 diff --git a/apps/preferences/go.sum b/apps/preferences/go.sum index 22bf6a8cbcd..75a19848d73 100644 --- a/apps/preferences/go.sum +++ b/apps/preferences/go.sum @@ -33,8 +33,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index e84ab0d35da..3cccd7dd8be 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -44,7 +44,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect - github.com/grafana/grafana-app-sdk v0.48.4 // indirect + github.com/grafana/grafana-app-sdk v0.48.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index 791bf68e797..0e1ba180fad 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -62,8 +62,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f h1:f+Z5Xpfp1WNYjUe23ginerWsHWUsRgOWrr3WGu3SlWs= diff --git a/apps/sdk.mk b/apps/sdk.mk index 62d89ed8ed3..68cf4c1c9c9 100644 --- a/apps/sdk.mk +++ b/apps/sdk.mk @@ -1,4 +1,4 @@ -APP_SDK_VERSION = v0.48.4 +APP_SDK_VERSION = v0.48.5 APP_SDK_DIR = $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) APP_SDK_BIN = $(APP_SDK_DIR)/grafana-app-sdk diff --git a/apps/secret/go.mod b/apps/secret/go.mod index da71ef8e666..a1ed48be6e7 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/secret go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf github.com/stretchr/testify v1.11.1 go.yaml.in/yaml/v3 v3.0.4 diff --git a/apps/secret/go.sum b/apps/secret/go.sum index 950843f4a7d..166e281f5af 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -37,8 +37,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg= diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index 128609888ba..4a414de5b4e 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/shorturl go 1.25.5 require ( - github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba k8s.io/apimachinery v0.34.2 diff --git a/apps/shorturl/go.sum b/apps/shorturl/go.sum index c7f57094324..58ecc31fd82 100644 --- a/apps/shorturl/go.sum +++ b/apps/shorturl/go.sum @@ -56,8 +56,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba h1:Qam8QzVRsyZN39zgZ9Vj6e8PEfswvv2McnqCZ/v5NcI= diff --git a/go.mod b/go.mod index 7ef730e391d..e589e5a18a5 100644 --- a/go.mod +++ b/go.mod @@ -97,7 +97,7 @@ require ( github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend - github.com/grafana/grafana-app-sdk v0.48.4 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk v0.48.5 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-app-sdk/logging v0.48.3 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-aws-sdk v1.3.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // @grafana/partner-datasources diff --git a/go.sum b/go.sum index 7723cfa19a9..91104bfeaf4 100644 --- a/go.sum +++ b/go.sum @@ -1635,8 +1635,8 @@ github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8= github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y= -github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= -github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= diff --git a/go.work.sum b/go.work.sum index 36bd9f3510d..3017d8eb878 100644 --- a/go.work.sum +++ b/go.work.sum @@ -775,7 +775,6 @@ github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5 github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= @@ -2084,6 +2083,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go. google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822 h1:zWFRixYR5QlotL+Uv3YfsPRENIrQFXiGs+iwqel6fOQ= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= @@ -2114,6 +2114,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= From 8e11851bb085fac6ce2438d4b7dd5d340100ce48 Mon Sep 17 00:00:00 2001 From: Austin Pond Date: Sat, 6 Dec 2025 03:01:28 -0500 Subject: [PATCH 340/378] =?UTF-8?q?Dashboards:=20Use=20the=20OpenAPI=20gen?= =?UTF-8?q?erated=20by=20app-sdk=20in=20the=20manifest=20to=20=E2=80=A6=20?= =?UTF-8?q?(#114858)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/dashboard/Makefile | 3 +- apps/dashboard/pkg/apis/dashboard_manifest.go | 24 + pkg/registry/apis/dashboard/register.go | 45 + .../dashboard.grafana.app-v2alpha1.json | 2061 ++++---- .../dashboard.grafana.app-v2beta1.json | 4591 +++++++++++++++++ pkg/tests/apis/openapi_test.go | 3 + 6 files changed, 5552 insertions(+), 1175 deletions(-) create mode 100644 pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json diff --git a/apps/dashboard/Makefile b/apps/dashboard/Makefile index 3d5c7060199..7ff9b946dc9 100644 --- a/apps/dashboard/Makefile +++ b/apps/dashboard/Makefile @@ -11,8 +11,7 @@ do-generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generati --tsgenpath=../../packages/grafana-schema/src/schema \ --grouping=group \ --defencoding=none \ - --genoperatorstate=false \ - --noschemasinmanifest + --genoperatorstate=false .PHONY: post-generate-cleanup post-generate-cleanup: ## Clean up the generated code diff --git a/apps/dashboard/pkg/apis/dashboard_manifest.go b/apps/dashboard/pkg/apis/dashboard_manifest.go index 974062efcec..c4e35bd8f40 100644 --- a/apps/dashboard/pkg/apis/dashboard_manifest.go +++ b/apps/dashboard/pkg/apis/dashboard_manifest.go @@ -6,6 +6,7 @@ package apis import ( + "encoding/json" "fmt" "strings" @@ -21,6 +22,24 @@ import ( v2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" ) +var ( + rawSchemaDashboardv0alpha1 = []byte(`{"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + versionSchemaDashboardv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaDashboardv0alpha1, &versionSchemaDashboardv0alpha1) + rawSchemaSnapshotv0alpha1 = []byte(`{"Snapshot":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"dashboard":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"The raw dashboard (unstructured for now)","type":"object"},"expires":{"default":0,"description":"Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds)","type":"integer"},"external":{"default":false,"description":"When set to true, the snapshot exists in a remote server","type":"boolean"},"externalUrl":{"description":"The external URL where the snapshot can be seen","type":"string"},"originalUrl":{"description":"The URL that created the dashboard originally","type":"string"},"timestamp":{"description":"Snapshot creation timestamp","type":"string"},"title":{"description":"Snapshot title","type":"string"}},"type":"object"}}`) + versionSchemaSnapshotv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaSnapshotv0alpha1, &versionSchemaSnapshotv0alpha1) + rawSchemaDashboardv1beta1 = []byte(`{"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + versionSchemaDashboardv1beta1 app.VersionSchema + _ = json.Unmarshal(rawSchemaDashboardv1beta1, &versionSchemaDashboardv1beta1) + rawSchemaDashboardv2alpha1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a DataQueryKind is the datasource type","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["kind","spec"],"type":"object"},"DataSourceRef":{"additionalProperties":false,"properties":{"type":{"description":"The plugin type-id","type":"string"},"uid":{"description":"Specific datasource instance","type":"string"}},"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"description":"Switch variable specification","properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing).","enum":["dontHide","hideLabel","hideVariable"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a VizConfigKind is the plugin ID","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"}},"required":["kind","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"pluginVersion":{"type":"string"}},"required":["pluginVersion","options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + versionSchemaDashboardv2alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaDashboardv2alpha1, &versionSchemaDashboardv2alpha1) + rawSchemaDashboardv2beta1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQueryPlacement":{"const":"inControlsMenu","description":"Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu","type":"string"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties. Should not be available in as code tooling.","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"placement":{"$ref":"#/components/schemas/AnnotationQueryPlacement","description":"Placement can be used to display the annotation query somewhere else on the dashboard other than the default location."},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["query","enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"datasource":{"additionalProperties":false,"description":"New type for datasource reference\nNot creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.","properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"DataQuery","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"version":{"default":"v0","type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"const":"onTimeRangeChanged","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"default":"A","type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeCompare":{"type":"string"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing), ` + "`" + `inControlsMenu` + "`" + ` (show in a drop-down menu).","enum":["dontHide","hideLabel","hideVariable","inControlsMenu"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"group":{"description":"The group is the plugin ID","type":"string"},"kind":{"const":"VizConfig","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"},"version":{"type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + versionSchemaDashboardv2beta1 app.VersionSchema + _ = json.Unmarshal(rawSchemaDashboardv2beta1, &versionSchemaDashboardv2beta1) +) + var appManifestData = app.ManifestData{ AppName: "dashboard", Group: "dashboard.grafana.app", @@ -35,6 +54,7 @@ var appManifestData = app.ManifestData{ Plural: "Dashboards", Scope: "Namespaced", Conversion: false, + Schema: &versionSchemaDashboardv0alpha1, }, { @@ -42,6 +62,7 @@ var appManifestData = app.ManifestData{ Plural: "Snapshots", Scope: "Namespaced", Conversion: false, + Schema: &versionSchemaSnapshotv0alpha1, }, }, Routes: app.ManifestVersionRoutes{ @@ -60,6 +81,7 @@ var appManifestData = app.ManifestData{ Plural: "Dashboards", Scope: "Namespaced", Conversion: false, + Schema: &versionSchemaDashboardv1beta1, }, }, Routes: app.ManifestVersionRoutes{ @@ -78,6 +100,7 @@ var appManifestData = app.ManifestData{ Plural: "Dashboards", Scope: "Namespaced", Conversion: false, + Schema: &versionSchemaDashboardv2alpha1, }, }, Routes: app.ManifestVersionRoutes{ @@ -96,6 +119,7 @@ var appManifestData = app.ManifestData{ Plural: "Dashboards", Scope: "Namespaced", Conversion: false, + Schema: &versionSchemaDashboardv2beta1, }, }, Routes: app.ManifestVersionRoutes{ diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 16873ec2101..e9925f0a3b7 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -24,6 +24,7 @@ import ( authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana-app-sdk/logging" + manifestdata "github.com/grafana/grafana/apps/dashboard/pkg/apis" internal "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" @@ -843,6 +844,50 @@ func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefiniti maps.Copy(defs, dashv1.GetOpenAPIDefinitions(ref)) maps.Copy(defs, dashv2alpha1.GetOpenAPIDefinitions(ref)) maps.Copy(defs, dashv2beta1.GetOpenAPIDefinitions(ref)) + md := manifestdata.LocalManifest().ManifestData + // Overwrite the OpenAPI generated from kubernetes (sourced from the go types) with the OpenAPI generated by grafana-app-sdk + // from the manifest CUE, as it correctly handles the CUE disjunctions in the dashboard spec. + // We don't touch any types which were not specified in the manifest CUE (such as custom route types). + for _, version := range md.Versions { + // We don't need to correct the v0 or v1 openAPI as the spec type is just `any` + if len(version.Name) > 1 && (version.Name[1] == '0' || version.Name[1] == '1') { + continue + } + for _, kind := range version.Kinds { + pkgPrefix := fmt.Sprintf("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/%s", version.Name) + oapi, err := kind.Schema.AsKubeOpenAPI(schema.GroupVersionKind{ + Group: md.Group, + Version: version.Name, + Kind: kind.Kind, + }, ref, pkgPrefix) + if err != nil { + logging.DefaultLogger.Error("unable to generate openAPI for kind %s: %w", kind.Kind, err) + continue + } + maps.Copy(defs, oapi) + } + } + + // Fix legacyOptions schema for v2alpha1 and v2beta1 to allow any value type + // The generated schema incorrectly restricts values to objects, but map[string]interface{} can hold any type + // This fix must be applied here so structured-merge-diff uses the correct schema + // For some reason this issue occurs with both the kubernetes-generated openAPI sourced from go, _and_ the OpenAPI from the AppManifest + // TODO: @IfSentient this should really be addressed in the app-sdk's generation, or work out what about this particular CUE value is broken + for _, defKey := range []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQuerySpec", + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationQuerySpec", + } { + if def, ok := defs[defKey]; ok { + if legacyOptions, ok := def.Schema.Properties["legacyOptions"]; ok { + // Fix: Use additionalProperties: true to allow any value type (string, number, boolean, array, object, etc.) + // instead of restricting to objects only. This must match map[string]interface{} semantics. + legacyOptions.AdditionalProperties = &spec.SchemaOrBool{Allows: true} + def.Schema.Properties["legacyOptions"] = legacyOptions + defs[defKey] = def + } + } + } + return defs } } diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index 567636ff7fc..2cad6213d04 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -968,9 +968,10 @@ "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.Dashboard": { "type": "object", "required": [ + "kind", + "apiVersion", "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -990,21 +991,10 @@ ] }, "spec": { - "description": "Spec is the spec of the Dashboard", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpec" }, "status": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStatus" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStatus" } }, "x-kubernetes-group-version-kind": [ @@ -1085,28 +1075,35 @@ "type": "boolean" }, "style": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1ActionStyle" + "type": "object", + "properties": { + "backgroundColor": { + "type": "string" + } + }, + "additionalProperties": false }, "title": { - "type": "string", - "default": "" + "type": "string" }, "type": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionType" }, "variables": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionVariable" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionVariable" } } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionType": { + "type": "string", + "enum": [ + "fetch", + "infinity" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionVariable": { "type": "object", @@ -1117,18 +1114,20 @@ ], "properties": { "key": { - "type": "string", - "default": "" + "type": "string" }, "name": { - "type": "string", - "default": "" + "type": "string" }, "type": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionVariableType" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardActionVariableType": { + "description": "Action variable type", + "type": "string" }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdHocFilterWithLabels": { "description": "Define the AdHocFilterWithLabels type", @@ -1147,38 +1146,34 @@ "type": "boolean" }, "key": { - "type": "string", - "default": "" + "type": "string" }, "keyLabel": { "type": "string" }, "operator": { - "type": "string", - "default": "" - }, - "origin": { "type": "string" }, + "origin": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFilterOrigin" + }, "value": { - "type": "string", - "default": "" + "type": "string" }, "valueLabels": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } }, "values": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdhocVariableKind": { "description": "Adhoc variable kind", @@ -1189,18 +1184,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdhocVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdhocVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdhocVariableSpec": { "description": "Adhoc variable specification", @@ -1217,17 +1207,12 @@ "properties": { "allowCustomValue": { "type": "boolean", - "default": false + "default": true }, "baseFilters": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdHocFilterWithLabels" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdHocFilterWithLabels" } }, "datasource": { @@ -1236,12 +1221,7 @@ "defaultKeys": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMetricFindValue" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMetricFindValue" } }, "description": { @@ -1250,17 +1230,11 @@ "filters": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdHocFilterWithLabels" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdHocFilterWithLabels" } }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "label": { "type": "string" @@ -1273,7 +1247,8 @@ "type": "boolean", "default": false } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationEventFieldMapping": { "description": "Annotation event field mapping. Defines how to map a data frame field to an annotation event field.", @@ -1285,13 +1260,15 @@ }, "source": { "description": "Source type for the field value", - "type": "string" + "type": "string", + "default": "field" }, "value": { "description": "Constant value to use when source is \"text\"", "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationPanelFilter": { "type": "object", @@ -1301,18 +1278,18 @@ "properties": { "exclude": { "description": "Should the specified panels be included or excluded", - "type": "boolean" + "type": "boolean", + "default": false }, "ids": { "description": "Panel IDs that should be included or excluded", "type": "array", "items": { - "type": "integer", - "format": "int64", - "default": 0 + "type": "integer" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationQueryKind": { "type": "object", @@ -1322,18 +1299,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationQuerySpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationQuerySpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationQuerySpec": { "type": "object", @@ -1345,53 +1317,44 @@ ], "properties": { "builtIn": { - "type": "boolean" + "type": "boolean", + "default": false }, "datasource": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataSourceRef" }, "enable": { - "type": "boolean", - "default": false + "type": "boolean" }, "filter": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationPanelFilter" }, "hide": { - "type": "boolean", - "default": false + "type": "boolean" }, "iconColor": { - "type": "string", - "default": "" + "type": "string" }, "legacyOptions": { "description": "Catch-all field for datasource-specific properties", "type": "object", - "additionalProperties": { - "type": "object" - } + "additionalProperties": true }, "mappings": { "description": "Mappings define how to convert data frame fields to annotation event fields.", "type": "object", "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationEventFieldMapping" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationEventFieldMapping" } }, "name": { - "type": "string", - "default": "" + "type": "string" }, "query": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutItemKind": { "type": "object", @@ -1401,18 +1364,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutItemSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutItemSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutItemSpec": { "type": "object", @@ -1424,17 +1382,13 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingGroupKind" }, "element": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElementReference" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElementReference" }, "repeat": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridRepeatOptions" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutKind": { "type": "object", @@ -1444,18 +1398,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutSpec": { "type": "object", @@ -1466,40 +1415,47 @@ ], "properties": { "columnWidth": { - "type": "number", - "format": "double" + "type": "number" }, "columnWidthMode": { "type": "string", - "default": "" + "default": "standard", + "enum": [ + "narrow", + "standard", + "wide", + "custom" + ] }, "fillScreen": { - "type": "boolean" + "type": "boolean", + "default": false }, "items": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutItemKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutItemKind" } }, "maxColumnCount": { "type": "number", - "format": "double" + "default": 3 }, "rowHeight": { - "type": "number", - "format": "double" + "type": "number" }, "rowHeightMode": { "type": "string", - "default": "" + "default": "standard", + "enum": [ + "short", + "standard", + "tall", + "custom" + ] } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridRepeatOptions": { "type": "object", @@ -1509,14 +1465,13 @@ ], "properties": { "mode": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatMode" }, "value": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingDataKind": { "type": "object", @@ -1526,18 +1481,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingDataSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingDataSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingDataSpec": { "type": "object", @@ -1546,10 +1496,10 @@ ], "properties": { "value": { - "type": "boolean", - "default": false + "type": "boolean" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingGroupKind": { "type": "object", @@ -1559,18 +1509,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingGroupSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingGroupSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingGroupSpec": { "type": "object", @@ -1582,19 +1527,36 @@ "properties": { "condition": { "type": "string", - "default": "" + "enum": [ + "and", + "or" + ] }, "items": { "type": "array", "items": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind" + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingDataKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind" + } + ] } }, "visibility": { "type": "string", - "default": "" + "enum": [ + "show", + "hide" + ] } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind": { "type": "object", @@ -1604,18 +1566,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec": { "type": "object", @@ -1624,10 +1581,10 @@ ], "properties": { "value": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableKind": { "type": "object", @@ -1637,32 +1594,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableSpec" } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind": { - "type": "object", - "properties": { - "ConditionalRenderingDataKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingDataKind" - }, - "ConditionalRenderingTimeRangeSizeKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind" - }, - "ConditionalRenderingVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableKind" - } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingVariableSpec": { "type": "object", @@ -1674,17 +1612,21 @@ "properties": { "operator": { "type": "string", - "default": "" + "enum": [ + "equals", + "notEquals", + "matches", + "notMatches" + ] }, "value": { - "type": "string", - "default": "" + "type": "string" }, "variable": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConstantVariableKind": { "description": "Constant variable kind", @@ -1695,18 +1637,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConstantVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConstantVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConstantVariableSpec": { "description": "Constant variable specification", @@ -1720,19 +1657,13 @@ ], "properties": { "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "description": { "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "label": { "type": "string" @@ -1749,7 +1680,8 @@ "type": "boolean", "default": false } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConversionStatus": { "description": "ConversionStatus is the status of the conversion of the dashboard.", @@ -1759,23 +1691,24 @@ ], "properties": { "error": { - "description": "The error message from the conversion. Empty if the conversion has not failed.", + "description": "The error message from the conversion.\nEmpty if the conversion has not failed.", "type": "string" }, "failed": { - "description": "Whether from another version has failed. If true, means that the dashboard is not valid, and the caller should instead fetch the stored version.", - "type": "boolean", - "default": false + "description": "Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.", + "type": "boolean" }, "source": { "description": "The original value map[string]any", - "type": "object" + "type": "object", + "additionalProperties": {} }, "storedVersion": { - "description": "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.", + "description": "The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.", "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardCustomVariableKind": { "description": "Custom variable kind", @@ -1786,18 +1719,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardCustomVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardCustomVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardCustomVariableSpec": { "description": "Custom variable specification", @@ -1819,22 +1747,16 @@ }, "allowCustomValue": { "type": "boolean", - "default": false + "default": true }, "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "description": { "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "includeAll": { "type": "boolean", @@ -1854,12 +1776,7 @@ "options": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" } }, "query": { @@ -1870,7 +1787,17 @@ "type": "boolean", "default": false } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardCursorSync": { + "description": "\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.", + "type": "string", + "enum": [ + "Crosshair", + "Tooltip", + "Off" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLink": { "description": "Links with references to other dashboards or external resources", @@ -1894,8 +1821,7 @@ }, "icon": { "description": "Icon name to be displayed with the link", - "type": "string", - "default": "" + "type": "string" }, "includeVars": { "description": "If true, includes current template variables values in the link as query params", @@ -1908,15 +1834,13 @@ "default": false }, "placement": { - "description": "Placement can be used to display the link somewhere else on the dashboard other than above the visualisations.", - "type": "string" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLinkPlacement" }, "tags": { "description": "List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards", "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } }, "targetBlank": { @@ -1926,24 +1850,33 @@ }, "title": { "description": "Title to display with the link", - "type": "string", - "default": "" + "type": "string" }, "tooltip": { "description": "Tooltip to display when the user hovers their mouse over it", - "type": "string", - "default": "" + "type": "string" }, "type": { - "description": "Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) FIXME: The type is generated as `type: DashboardLinkType | dashboardLinkType.Link;` but it should be `type: DashboardLinkType`", - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLinkType" }, "url": { "description": "Link URL. Only required/valid if the type is link", "type": "string" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLinkPlacement": { + "description": "Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu", + "type": "string" + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLinkType": { + "description": "Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)", + "type": "string", + "enum": [ + "link", + "dashboards" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataLink": { "type": "object", @@ -1956,14 +1889,13 @@ "type": "boolean" }, "title": { - "type": "string", - "default": "" + "type": "string" }, "url": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind": { "type": "object", @@ -1974,16 +1906,17 @@ "properties": { "kind": { "description": "The kind of a DataQueryKind is the datasource type", - "type": "string", - "default": "" + "type": "string" }, "spec": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "additionalProperties": {} } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataSourceRef": { "type": "object", @@ -1996,10 +1929,20 @@ "description": "Specific datasource instance", "type": "string" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataTopic": { + "description": "A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.", + "type": "string", + "enum": [ + "series", + "annotations", + "alertStates" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataTransformerConfig": { - "description": "Transformations allow to manipulate data returned by a query before the system applies a visualization. Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, use the output of one transformation as the input to another transformation, etc.", + "description": "Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.", "type": "object", "required": [ "id", @@ -2011,27 +1954,22 @@ "type": "boolean" }, "filter": { - "description": "Optional frame matcher. When missing it will be applied to all results", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMatcherConfig" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMatcherConfig" }, "id": { "description": "Unique identifier of transformer", - "type": "string", - "default": "" + "type": "string" }, "options": { - "description": "Options to be passed to the transformer Valid options depend on the transformer id", - "type": "object" + "description": "Options to be passed to the transformer\nValid options depend on the transformer id", + "type": "object", + "additionalProperties": {} }, "topic": { - "description": "Where to pull DataFrames from as input to transformation", - "type": "string" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataTopic" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDatasourceVariableKind": { "description": "Datasource variable kind", @@ -2042,18 +1980,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDatasourceVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDatasourceVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDatasourceVariableSpec": { "description": "Datasource variable specification", @@ -2077,22 +2010,16 @@ }, "allowCustomValue": { "type": "boolean", - "default": false + "default": true }, "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "description": { "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "includeAll": { "type": "boolean", @@ -2112,12 +2039,7 @@ "options": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" } }, "pluginId": { @@ -2125,8 +2047,7 @@ "default": "" }, "refresh": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRefresh" }, "regex": { "type": "string", @@ -2136,7 +2057,8 @@ "type": "boolean", "default": false } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDynamicConfigValue": { "type": "object", @@ -2149,9 +2071,22 @@ "default": "" }, "value": { - "type": "object" + "type": "object", + "additionalProperties": {} } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElement": { + "description": "Supported dashboard elements\n|* more element types in the future", + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelKind" + } + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElementReference": { "type": "object", @@ -2161,14 +2096,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "name": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFetchOptions": { "type": "object", @@ -2185,31 +2119,28 @@ "items": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } } }, "method": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardHttpRequestMethod" }, "queryParams": { - "description": "These are 2D arrays of strings, each representing a key-value pair We are defining them this way because we can't generate a go struct that that would have exactly two strings in each sub-array", + "description": "These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array", "type": "array", "items": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } } }, "url": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColor": { "description": "Map a field to a color.", @@ -2223,51 +2154,74 @@ "type": "string" }, "mode": { - "description": "The main color scheme mode.", - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColorModeId" }, "seriesBy": { - "description": "Some visualizations need to know how to assign a series color from by value color schemes.", - "type": "string" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColorSeriesByMode" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColorModeId": { + "description": "Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n`thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold\n`palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n`palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n`continuous-viridis`: Continuous Viridis palette mode\n`continuous-magma`: Continuous Magma palette mode\n`continuous-plasma`: Continuous Plasma palette mode\n`continuous-inferno`: Continuous Inferno palette mode\n`continuous-cividis`: Continuous Cividis palette mode\n`continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode\n`continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode\n`continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode\n`continuous-YlRd`: Continuous Yellow-Red palette mode\n`continuous-BlPu`: Continuous Blue-Purple palette mode\n`continuous-YlBl`: Continuous Yellow-Blue palette mode\n`continuous-blues`: Continuous Blue palette mode\n`continuous-reds`: Continuous Red palette mode\n`continuous-greens`: Continuous Green palette mode\n`continuous-purples`: Continuous Purple palette mode\n`shades`: Shades of a single color. Specify a single color, useful in an override rule.\n`fixed`: Fixed color mode. Specify a single color, useful in an override rule.", + "type": "string", + "enum": [ + "thresholds", + "palette-classic", + "palette-classic-by-name", + "continuous-viridis", + "continuous-magma", + "continuous-plasma", + "continuous-inferno", + "continuous-cividis", + "continuous-GrYlRd", + "continuous-RdYlGr", + "continuous-BlYlRd", + "continuous-YlRd", + "continuous-BlPu", + "continuous-YlBl", + "continuous-blues", + "continuous-reds", + "continuous-greens", + "continuous-purples", + "fixed", + "shades" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColorSeriesByMode": { + "description": "Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.", + "type": "string", + "enum": [ + "min", + "max", + "last" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldConfig": { - "description": "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. Each column within this structure is called a field. A field can represent a single time series or table column. Field options allow you to change how the data is displayed in your visualizations.", + "description": "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.", "type": "object", "properties": { "actions": { "description": "Define interactive HTTP requests that can be triggered from data visualizations.", "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAction" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAction" } }, "color": { - "description": "Panel color configuration", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColor" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldColor" }, "custom": { - "description": "custom is specified by the FieldConfig field in panel plugin schemas.", + "description": "custom is specified by the FieldConfig field\nin panel plugin schemas.", "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "additionalProperties": {} } }, "decimals": { - "description": "Specify the number of decimals Grafana includes in the rendered value. If you leave this field blank, Grafana automatically truncates the number of decimals based on the value. For example 1.1234 will display as 1.12 and 100.456 will display as 100. To display all decimals, set the unit to `String`.", - "type": "number", - "format": "double" + "description": "Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to `String`.", + "type": "number" }, "description": { "description": "Human readable field metadata", @@ -2278,7 +2232,7 @@ "type": "string" }, "displayNameFromDS": { - "description": "This can be used by data sources that return and explicit naming structure for values and labels When this property is configured, this value is used rather than the default naming strategy.", + "description": "This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.", "type": "string" }, "filterable": { @@ -2289,54 +2243,49 @@ "description": "The behavior when clicking on a result", "type": "array", "items": { - "type": "object" + "type": "object", + "additionalProperties": {} } }, "mappings": { "description": "Convert input values into a display string", "type": "array", "items": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMapping" } }, "max": { "description": "The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.", - "type": "number", - "format": "double" + "type": "number" }, "min": { "description": "The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.", - "type": "number", - "format": "double" + "type": "number" }, "noValue": { "description": "Alternative to empty string", "type": "string" }, "path": { - "description": "An explicit path to the field in the datasource. When the frame meta includes a path, This will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and may be used to update the results", + "description": "An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results", "type": "string" }, "thresholds": { - "description": "Map numeric values to states", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThresholdsConfig" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThresholdsConfig" }, "unit": { - "description": "Unit a field should use. The unit you select is applied to all fields except time. You can use the units ID availables in Grafana or a custom unit. Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts As custom unit, you can use the following formats: `suffix:\u003csuffix\u003e` for custom unit that should go after value. `prefix:\u003cprefix\u003e` for custom unit that should go before value. `time:\u003cformat\u003e` For custom date time formats type for example `time:YYYY-MM-DD`. `si:\u003cbase scale\u003e\u003cunit characters\u003e` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character. `count:\u003cunit\u003e` for a custom count unit. `currency:\u003cunit\u003e` for custom a currency unit.", + "description": "Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n`suffix:` for custom unit that should go after value.\n`prefix:` for custom unit that should go before value.\n`time:` For custom date time formats type for example `time:YYYY-MM-DD`.\n`si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n`count:` for a custom count unit.\n`currency:` for custom a currency unit.", "type": "string" }, "writeable": { "description": "True if data source can write a value to the path. Auth/authz are supported separately", "type": "boolean" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldConfigSource": { - "description": "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. Each column within this structure is called a field. A field can represent a single time series or table column. Field options allow you to change how the data is displayed in your visualizations.", + "description": "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.", "type": "object", "required": [ "defaults", @@ -2344,27 +2293,41 @@ ], "properties": { "defaults": { - "description": "Defaults are the options applied to all fields.", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldConfig" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldConfig" }, "overrides": { "description": "Overrides are the options applied to specific fields overriding the defaults.", "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1FieldConfigSourceOverrides" + "type": "object", + "required": [ + "matcher", + "properties" + ], + "properties": { + "__systemRef": { + "description": "Describes config override rules created when interacting with Grafana.", + "type": "string" + }, + "matcher": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMatcherConfig" + }, + "properties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDynamicConfigValue" + } } - ] + }, + "additionalProperties": false } } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFilterOrigin": { + "description": "Determine the origin of the adhoc variable filter", + "type": "string" }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemKind": { "type": "object", @@ -2374,18 +2337,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemSpec": { "type": "object", @@ -2398,38 +2356,25 @@ ], "properties": { "element": { - "description": "reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElementReference" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElementReference" }, "height": { - "type": "integer", - "format": "int64", - "default": 0 + "type": "integer" }, "repeat": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatOptions" }, "width": { - "type": "integer", - "format": "int64", - "default": 0 + "type": "integer" }, "x": { - "type": "integer", - "format": "int64", - "default": 0 + "type": "integer" }, "y": { - "type": "integer", - "format": "int64", - "default": 0 + "type": "integer" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKind": { "type": "object", @@ -2439,52 +2384,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutSpec" } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": { - "type": "object", - "properties": { - "AutoGridLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutKind" - }, - "GridLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKind" - }, - "RowsLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutKind" - }, - "TabsLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind" - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind": { - "type": "object", - "properties": { - "AutoGridLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutKind" - }, - "GridLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKind" - }, - "RowsLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutKind" - }, - "TabsLayoutKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind" - } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutSpec": { "type": "object", @@ -2495,15 +2401,11 @@ "items": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutItemKind" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGroupByVariableKind": { "description": "Group variable kind", @@ -2514,18 +2416,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGroupByVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGroupByVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGroupByVariableSpec": { "description": "GroupBy variable specification", @@ -2540,12 +2437,7 @@ ], "properties": { "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "datasource": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataSourceRef" @@ -2557,8 +2449,7 @@ "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "label": { "type": "string" @@ -2574,19 +2465,25 @@ "options": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" } }, "skipUrlSync": { "type": "boolean", "default": false } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardHttpRequestMethod": { + "type": "string", + "enum": [ + "GET", + "PUT", + "POST", + "DELETE", + "PATCH" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardInfinityOptions": { "type": "object", @@ -2600,39 +2497,35 @@ "type": "string" }, "datasourceUid": { - "type": "string", - "default": "" + "type": "string" }, "headers": { "type": "array", "items": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } } }, "method": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardHttpRequestMethod" }, "queryParams": { - "description": "These are 2D arrays of strings, each representing a key-value pair We are defining them this way because we can't generate a go struct that that would have exactly two strings in each sub-array", + "description": "These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array", "type": "array", "items": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } } }, "url": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardIntervalVariableKind": { "description": "Interval variable kind", @@ -2643,18 +2536,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardIntervalVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardIntervalVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardIntervalVariableSpec": { "description": "Interval variable specification", @@ -2678,7 +2566,6 @@ }, "auto_count": { "type": "integer", - "format": "int64", "default": 0 }, "auto_min": { @@ -2686,19 +2573,13 @@ "default": "" }, "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "description": { "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "label": { "type": "string" @@ -2710,12 +2591,7 @@ "options": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" } }, "query": { @@ -2723,14 +2599,14 @@ "default": "" }, "refresh": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRefresh" }, "skipUrlSync": { "type": "boolean", "default": false } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelKind": { "type": "object", @@ -2740,18 +2616,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelKindSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelKindSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelKindSpec": { "type": "object", @@ -2763,27 +2634,20 @@ "properties": { "id": { "description": "Panel ID for the library panel in the dashboard", - "type": "number", - "format": "double", - "default": 0 + "type": "number" }, "libraryPanel": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelRef" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelRef" }, "title": { "description": "Title for the library panel in the dashboard", - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelRef": { - "description": "A library panel is a reusable panel that you can use in any dashboard. When you make a change to a library panel, that change propagates to all instances of where the panel is used. Library panels streamline reuse of panels across multiple dashboards.", + "description": "A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.", "type": "object", "required": [ "name", @@ -2792,15 +2656,14 @@ "properties": { "name": { "description": "Library panel name", - "type": "string", - "default": "" + "type": "string" }, "uid": { "description": "Library panel uid", - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardList": { "type": "object", @@ -2845,8 +2708,18 @@ } ] }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMappingType": { + "description": "Supported value mapping types\n`value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n`range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n`regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n`special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.", + "type": "string", + "enum": [ + "value", + "range", + "regex", + "special" + ] + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMatcherConfig": { - "description": "Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.", + "description": "Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.", "type": "object", "required": [ "id" @@ -2859,9 +2732,11 @@ }, "options": { "description": "The matcher options. This is specific to the matcher implementation.", - "type": "object" + "type": "object", + "additionalProperties": {} } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMetricFindValue": { "description": "Define the MetricFindValue type", @@ -2877,13 +2752,20 @@ "type": "string" }, "text": { - "type": "string", - "default": "" + "type": "string" }, "value": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStringOrFloat64" + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelKind": { "type": "object", @@ -2893,29 +2775,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelSpec" } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelKindOrLibraryPanelKind": { - "type": "object", - "properties": { - "LibraryPanelKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardLibraryPanelKind" - }, - "PanelKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelKind" - } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelQueryKind": { "type": "object", @@ -2925,18 +2791,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelQuerySpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelQuerySpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelQuerySpec": { "type": "object", @@ -2950,22 +2811,16 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataSourceRef" }, "hidden": { - "type": "boolean", - "default": false + "type": "boolean" }, "query": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind" }, "refId": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelSpec": { "type": "object", @@ -2979,49 +2834,31 @@ ], "properties": { "data": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryGroupKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryGroupKind" }, "description": { - "type": "string", - "default": "" + "type": "string" }, "id": { - "type": "number", - "format": "double", - "default": 0 + "type": "number" }, "links": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataLink" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataLink" } }, "title": { - "type": "string", - "default": "" + "type": "string" }, "transparent": { "type": "boolean" }, "vizConfig": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVizConfigKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVizConfigKind" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryGroupKind": { "type": "object", @@ -3031,18 +2868,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryGroupSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryGroupSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryGroupSpec": { "type": "object", @@ -3055,34 +2887,20 @@ "queries": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelQueryKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelQueryKind" } }, "queryOptions": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryOptionsSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryOptionsSpec" }, "transformations": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTransformationKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTransformationKind" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryOptionsSpec": { "type": "object", @@ -3097,12 +2915,10 @@ "type": "string" }, "maxDataPoints": { - "type": "integer", - "format": "int64" + "type": "integer" }, "queryCachingTTL": { - "type": "integer", - "format": "int64" + "type": "integer" }, "timeFrom": { "type": "string" @@ -3110,7 +2926,8 @@ "timeShift": { "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKind": { "description": "Query variable kind", @@ -3121,50 +2938,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableSpec" } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind": { - "type": "object", - "properties": { - "AdhocVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdhocVariableKind" - }, - "ConstantVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConstantVariableKind" - }, - "CustomVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardCustomVariableKind" - }, - "DatasourceVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDatasourceVariableKind" - }, - "GroupByVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGroupByVariableKind" - }, - "IntervalVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardIntervalVariableKind" - }, - "QueryVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKind" - }, - "SwitchVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableKind" - }, - "TextVariableKind": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableKind" - } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableSpec": { "description": "Query variable specification", @@ -3189,15 +2969,10 @@ }, "allowCustomValue": { "type": "boolean", - "default": false + "default": true }, "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "datasource": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataSourceRef" @@ -3209,8 +2984,7 @@ "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "includeAll": { "type": "boolean", @@ -3230,28 +3004,17 @@ "options": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" } }, "placeholder": { "type": "string" }, "query": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataQueryKind" }, "refresh": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRefresh" }, "regex": { "type": "string", @@ -3262,27 +3025,27 @@ "default": false }, "sort": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableSort" }, "staticOptions": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" } }, "staticOptionsOrder": { - "type": "string" + "type": "string", + "enum": [ + "before", + "after", + "sorted" + ] } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRangeMap": { - "description": "Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.", + "description": "Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.", "type": "object", "required": [ "type", @@ -3291,21 +3054,35 @@ "properties": { "options": { "description": "Range to match against and the result to apply when the value is within the range", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1RangeMapOptions" + "type": "object", + "required": [ + "from", + "to", + "result" + ], + "properties": { + "from": { + "description": "Min value of the range. It can be null which means -Infinity", + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" + }, + "to": { + "description": "Max value of the range. It can be null which means +Infinity", + "type": "number" } - ] + }, + "additionalProperties": false }, "type": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMappingType" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRegexMap": { - "description": "Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.", + "description": "Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.", "type": "object", "required": [ "type", @@ -3314,18 +3091,31 @@ "properties": { "options": { "description": "Regular expression to match against and the result to apply when the value matches the regex", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1RegexMapOptions" + "type": "object", + "required": [ + "pattern", + "result" + ], + "properties": { + "pattern": { + "description": "Regular expression to match against", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" } - ] + }, + "additionalProperties": false }, "type": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMappingType" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatMode": { + "description": "other repeat modes will be added in the future: label, frame", + "type": "string" }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatOptions": { "type": "object", @@ -3335,21 +3125,23 @@ ], "properties": { "direction": { - "type": "string" + "type": "string", + "enum": [ + "h", + "v" + ] }, "maxPerRow": { - "type": "integer", - "format": "int64" + "type": "integer" }, "mode": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatMode" }, "value": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowRepeatOptions": { "type": "object", @@ -3359,14 +3151,13 @@ ], "properties": { "mode": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatMode" }, "value": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutKind": { "type": "object", @@ -3376,18 +3167,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutRowKind": { "type": "object", @@ -3397,18 +3183,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutRowSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutRowSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutRowSpec": { "type": "object", @@ -3429,7 +3210,20 @@ "type": "boolean" }, "layout": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind" + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutKind" + } + ] }, "repeat": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowRepeatOptions" @@ -3437,7 +3231,8 @@ "title": { "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutSpec": { "type": "object", @@ -3448,15 +3243,11 @@ "rows": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutRowKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutRowKind" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpec": { "type": "object", @@ -3476,18 +3267,11 @@ "annotations": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationQueryKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAnnotationQueryKind" } }, "cursorSync": { - "description": "Configuration of dashboard cursor sync behavior. \"Off\" for no shared crosshair or tooltip (default). \"Crosshair\" for shared crosshair. \"Tooltip\" for shared crosshair AND shared tooltip.", - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardCursorSync" }, "description": { "description": "Description of dashboard.", @@ -3495,31 +3279,40 @@ }, "editable": { "description": "Whether a dashboard is editable or not.", - "type": "boolean" + "type": "boolean", + "default": true }, "elements": { "type": "object", "additionalProperties": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardPanelKindOrLibraryPanelKind" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardElement" } }, "layout": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind" + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind" + } + ] }, "links": { "description": "Links with references to other dashboards or external websites.", "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLink" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDashboardLink" } }, "liveNow": { - "description": "When set to true, the dashboard will redraw panels at an interval matching the pixel width. This will keep data \"moving left\" regardless of the query refresh rate. This setting helps avoid dashboards presenting stale live data.", + "description": "When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.", "type": "boolean" }, "preload": { @@ -3528,42 +3321,35 @@ "default": false }, "revision": { - "description": "Plugins only. The version of the dashboard installed together with the plugin. This is used to determine if the dashboard should be updated when the plugin is updated.", - "type": "integer", - "format": "int32" + "description": "Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.", + "type": "integer" }, "tags": { "description": "Tags associated with dashboard.", "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } }, "timeSettings": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTimeSettingsSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTimeSettingsSpec" }, "title": { "description": "Title of dashboard.", - "type": "string", - "default": "" + "type": "string" }, "variables": { "description": "Configured template variables.", "type": "array", "items": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableKind" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpecialValueMap": { - "description": "Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.", + "description": "Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.", "type": "object", "required": [ "type", @@ -3571,58 +3357,47 @@ ], "properties": { "options": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1SpecialValueMapOptions" + "type": "object", + "required": [ + "match", + "result" + ], + "properties": { + "match": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpecialValueMatch" + }, + "result": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" } - ] + }, + "additionalProperties": false }, "type": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMappingType" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpecialValueMatch": { + "description": "Special value types supported by the `SpecialValueMap`", + "type": "string", + "enum": [ + "true", + "false", + "null", + "nan", + "null+nan", + "empty" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStatus": { "type": "object", "properties": { "conversion": { - "description": "Optional conversion status.", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConversionStatus" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConversionStatus" } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStringOrArrayOfString": { - "type": "object", - "properties": { - "ArrayOfString": { - "type": "array", - "items": { - "type": "string", - "default": "" - } - }, - "String": { - "type": "string" - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStringOrFloat64": { - "type": "object", - "properties": { - "Float64": { - "type": "number", - "format": "double" - }, - "String": { - "type": "string" - } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableKind": { "type": "object", @@ -3632,18 +3407,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableSpec": { "description": "Switch variable specification", @@ -3659,22 +3429,21 @@ "properties": { "current": { "type": "string", - "default": "" + "default": "false" }, "description": { "type": "string" }, "disabledValue": { "type": "string", - "default": "" + "default": "false" }, "enabledValue": { "type": "string", - "default": "" + "default": "true" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "label": { "type": "string" @@ -3687,7 +3456,8 @@ "type": "boolean", "default": false } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabRepeatOptions": { "type": "object", @@ -3697,14 +3467,13 @@ ], "properties": { "mode": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRepeatMode" }, "value": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind": { "type": "object", @@ -3714,18 +3483,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutSpec": { "type": "object", @@ -3736,15 +3500,11 @@ "tabs": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutTabKind" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutTabKind" } } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutTabKind": { "type": "object", @@ -3754,18 +3514,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutTabSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutTabSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutTabSpec": { "type": "object", @@ -3777,7 +3532,20 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConditionalRenderingGroupKind" }, "layout": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind" + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRowsLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAutoGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind" + } + ] }, "repeat": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabRepeatOptions" @@ -3785,7 +3553,8 @@ "title": { "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableKind": { "description": "Text variable kind", @@ -3796,18 +3565,13 @@ ], "properties": { "kind": { - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableSpec": { "description": "Text variable specification", @@ -3821,19 +3585,13 @@ ], "properties": { "current": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" }, "description": { "type": "string" }, "hide": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide" }, "label": { "type": "string" @@ -3850,7 +3608,8 @@ "type": "boolean", "default": false } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThreshold": { "type": "object", @@ -3860,15 +3619,14 @@ ], "properties": { "color": { - "type": "string", - "default": "" + "type": "string" }, "value": { "description": "Value null means -Infinity", - "type": "number", - "format": "double" + "type": "number" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThresholdsConfig": { "type": "object", @@ -3878,21 +3636,23 @@ ], "properties": { "mode": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThresholdsMode" }, "steps": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThreshold" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThreshold" } } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardThresholdsMode": { + "type": "string", + "enum": [ + "absolute", + "percentage" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTimeRangeOption": { "type": "object", @@ -3904,20 +3664,21 @@ "properties": { "display": { "type": "string", - "default": "" + "default": "Last 6 hours" }, "from": { "type": "string", - "default": "" + "default": "now-6h" }, "to": { "type": "string", - "default": "" + "default": "now" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTimeSettingsSpec": { - "description": "Time configuration It defines the default time config for the time picker, the refresh picker for the specific dashboard.", + "description": "Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.", "type": "object", "required": [ "from", @@ -3929,64 +3690,76 @@ ], "properties": { "autoRefresh": { - "description": "Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\". v1: refresh", + "description": "Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh", "type": "string", "default": "" }, "autoRefreshIntervals": { - "description": "Interval options available in the refresh picker dropdown. v1: timepicker.refresh_intervals", + "description": "Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals", "type": "array", + "default": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], "items": { - "type": "string", - "default": "" + "type": "string" } }, "fiscalYearStartMonth": { "description": "The month that the fiscal year starts on. 0 = January, 11 = December", "type": "integer", - "format": "int64", "default": 0 }, "from": { - "description": "Start time range for dashboard. Accepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", + "description": "Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", "type": "string", - "default": "" + "default": "now-6h" }, "hideTimepicker": { - "description": "Whether timepicker is visible or not. v1: timepicker.hidden", + "description": "Whether timepicker is visible or not.\nv1: timepicker.hidden", "type": "boolean", "default": false }, "nowDelay": { - "description": "Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. v1: timepicker.nowDelay", + "description": "Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay", "type": "string" }, "quickRanges": { - "description": "Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. v1: timepicker.quick_ranges , not exposed in the UI", + "description": "Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI", "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTimeRangeOption" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTimeRangeOption" } }, "timezone": { "description": "Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".", - "type": "string" + "type": "string", + "default": "browser" }, "to": { - "description": "End time range for dashboard. Accepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", + "description": "End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", "type": "string", - "default": "" + "default": "now" }, "weekStart": { "description": "Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".", - "type": "string" + "type": "string", + "enum": [ + "saturday", + "monday", + "sunday" + ] } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTransformationKind": { "type": "object", @@ -3997,136 +3770,16 @@ "properties": { "kind": { "description": "The kind of a TransformationKind is the transformation ID", - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataTransformerConfig" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDataTransformerConfig" } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1ActionStyle": { - "type": "object", - "properties": { - "backgroundColor": { - "type": "string" - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1FieldConfigSourceOverrides": { - "type": "object", - "required": [ - "matcher", - "properties" - ], - "properties": { - "__systemRef": { - "description": "Describes config override rules created when interacting with Grafana.", - "type": "string" - }, - "matcher": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMatcherConfig" - } - ] - }, - "properties": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDynamicConfigValue" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1RangeMapOptions": { - "type": "object", - "required": [ - "from", - "to", - "result" - ], - "properties": { - "from": { - "description": "Min value of the range. It can be null which means -Infinity", - "type": "number", - "format": "double" - }, - "result": { - "description": "Config to apply when the value is within the range", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" - } - ] - }, - "to": { - "description": "Max value of the range. It can be null which means +Infinity", - "type": "number", - "format": "double" - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1RegexMapOptions": { - "type": "object", - "required": [ - "pattern", - "result" - ], - "properties": { - "pattern": { - "description": "Regular expression to match against", - "type": "string", - "default": "" - }, - "result": { - "description": "Config to apply when the value matches the regex", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" - } - ] - } - } - }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardV2alpha1SpecialValueMapOptions": { - "type": "object", - "required": [ - "match", - "result" - ], - "properties": { - "match": { - "description": "Special value to match against", - "type": "string", - "default": "" - }, - "result": { - "description": "Config to apply when the value matches the special value", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" - } - ] - } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMap": { - "description": "Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.", + "description": "Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.", "type": "object", "required": [ "type", @@ -4134,39 +3787,33 @@ ], "properties": { "options": { - "description": "Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }", + "description": "Map with : ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }", "type": "object", "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult" } }, "type": { - "type": "string", - "default": "" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardMappingType" } - } + }, + "additionalProperties": false }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap": { - "type": "object", - "properties": { - "RangeMap": { + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMapping": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMap" + }, + { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRangeMap" }, - "RegexMap": { + { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardRegexMap" }, - "SpecialValueMap": { + { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSpecialValueMap" - }, - "ValueMap": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMap" } - } + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardValueMappingResult": { "description": "Result used as replacement with text and color when the value matches", @@ -4182,14 +3829,54 @@ }, "index": { "description": "Position in the mapping array. Only used internally.", - "type": "integer", - "format": "int32" + "type": "integer" }, "text": { "description": "Text to display when the value matches", "type": "string" } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableHide": { + "description": "Determine if the variable shows on dashboard\nAccepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing).", + "type": "string", + "enum": [ + "dontHide", + "hideLabel", + "hideVariable" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableKind": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardConstantVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardDatasourceVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardIntervalVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardCustomVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGroupByVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardAdhocVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableKind" + } + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption": { "description": "Variable option specification", @@ -4205,21 +3892,58 @@ }, "text": { "description": "Text to be displayed for the option", - "allOf": [ + "oneOf": [ { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStringOrArrayOfString" + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } } ] }, "value": { "description": "Value of the option", - "allOf": [ + "oneOf": [ { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardStringOrArrayOfString" + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } } ] } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRefresh": { + "description": "Options to config when to refresh a variable\n`never`: Never refresh the variable\n`onDashboardLoad`: Queries the data source every time the dashboard loads.\n`onTimeRangeChanged`: Queries the data source when the dashboard time range changes.", + "type": "string", + "enum": [ + "never", + "onDashboardLoad", + "onTimeRangeChanged" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableSort": { + "description": "Sort variable options\nAccepted values are:\n`disabled`: No sorting\n`alphabeticalAsc`: Alphabetical ASC\n`alphabeticalDesc`: Alphabetical DESC\n`numericalAsc`: Numerical ASC\n`numericalDesc`: Numerical DESC\n`alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC\n`alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC\n`naturalAsc`: Natural ASC\n`naturalDesc`: Natural DESC\nVariableSort enum with default value", + "type": "string", + "enum": [ + "disabled", + "alphabeticalAsc", + "alphabeticalDesc", + "numericalAsc", + "numericalDesc", + "alphabeticalCaseInsensitiveAsc", + "alphabeticalCaseInsensitiveDesc", + "naturalAsc", + "naturalDesc" + ] }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVizConfigKind": { "type": "object", @@ -4230,18 +3954,13 @@ "properties": { "kind": { "description": "The kind of a VizConfigKind is the plugin ID", - "type": "string", - "default": "" + "type": "string" }, "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVizConfigSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVizConfigSpec" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVizConfigSpec": { "description": "--- Kinds ---", @@ -4253,24 +3972,20 @@ ], "properties": { "fieldConfig": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldConfigSource" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardFieldConfigSource" }, "options": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "additionalProperties": {} } }, "pluginVersion": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardWithAccessInfo": { "description": "This is like the legacy DTO where access and metadata are all returned in a single call", @@ -4488,7 +4203,7 @@ } }, "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { - "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", "type": "object" }, "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { @@ -4842,4 +4557,4 @@ } } } -} \ No newline at end of file +} diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json new file mode 100644 index 00000000000..198ad3aea25 --- /dev/null +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json @@ -0,0 +1,4591 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Grafana dashboards as resources", + "title": "dashboard.grafana.app/v2beta1" + }, + "paths": { + "/apis/dashboard.grafana.app/v2beta1/": { + "get": { + "tags": [ + "API Discovery" + ], + "description": "Describe the available kubernetes resources", + "operationId": "getAPIResources", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + } + } + } + } + } + }, + "/apis/dashboard.grafana.app/v2beta1/namespaces/{namespace}/dashboards": { + "get": { + "tags": [ + "Dashboard" + ], + "description": "list objects of kind Dashboard", + "operationId": "listDashboard", + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "post": { + "tags": [ + "Dashboard" + ], + "description": "create a Dashboard", + "operationId": "createDashboard", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "delete": { + "tags": [ + "Dashboard" + ], + "description": "delete collection of Dashboard", + "operationId": "deletecollectionDashboard", + "parameters": [ + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v2beta1/namespaces/{namespace}/dashboards/{name}": { + "get": { + "tags": [ + "Dashboard" + ], + "description": "read the specified Dashboard", + "operationId": "getDashboard", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "put": { + "tags": [ + "Dashboard" + ], + "description": "replace the specified Dashboard", + "operationId": "replaceDashboard", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "delete": { + "tags": [ + "Dashboard" + ], + "description": "delete a Dashboard", + "operationId": "deleteDashboard", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "patch": { + "tags": [ + "Dashboard" + ], + "description": "partially update the specified Dashboard", + "operationId": "updateDashboard", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "Dashboard" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Dashboard", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v2beta1/namespaces/{namespace}/dashboards/{name}/dto": { + "get": { + "tags": [ + "Dashboard" + ], + "description": "connect GET requests to dto of Dashboard", + "operationId": "getDashboardDto", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardWithAccessInfo" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v2beta1", + "kind": "DashboardWithAccessInfo" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the DashboardWithAccessInfo", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + } + }, + "components": { + "schemas": { + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.AnnotationActions": { + "type": "object", + "required": [ + "canAdd", + "canEdit", + "canDelete" + ], + "properties": { + "canAdd": { + "type": "boolean", + "default": false + }, + "canDelete": { + "type": "boolean", + "default": false + }, + "canEdit": { + "type": "boolean", + "default": false + } + } + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.AnnotationPermission": { + "type": "object", + "required": [ + "dashboard", + "organization" + ], + "properties": { + "dashboard": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.AnnotationActions" + } + ] + }, + "organization": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.AnnotationActions" + } + ] + } + } + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard": { + "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpec" + }, + "status": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardStatus" + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "Dashboard", + "version": "v2beta1" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAccess": { + "description": "Information about how the requesting user can use a given dashboard", + "type": "object", + "required": [ + "isPublic", + "canSave", + "canEdit", + "canAdmin", + "canStar", + "canDelete", + "annotationsPermissions" + ], + "properties": { + "annotationsPermissions": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.AnnotationPermission" + }, + "canAdmin": { + "type": "boolean", + "default": false + }, + "canDelete": { + "type": "boolean", + "default": false + }, + "canEdit": { + "type": "boolean", + "default": false + }, + "canSave": { + "description": "The permissions part", + "type": "boolean", + "default": false + }, + "canStar": { + "type": "boolean", + "default": false + }, + "isPublic": { + "type": "boolean", + "default": false + }, + "slug": { + "description": "Metadata fields", + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAction": { + "type": "object", + "required": [ + "type", + "title" + ], + "properties": { + "confirmation": { + "type": "string" + }, + "fetch": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFetchOptions" + }, + "infinity": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardInfinityOptions" + }, + "oneClick": { + "type": "boolean" + }, + "style": { + "type": "object", + "properties": { + "backgroundColor": { + "type": "string" + } + }, + "additionalProperties": false + }, + "title": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardActionType" + }, + "variables": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardActionVariable" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardActionType": { + "type": "string", + "enum": [ + "fetch", + "infinity" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardActionVariable": { + "type": "object", + "required": [ + "key", + "name", + "type" + ], + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardActionVariableType" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardActionVariableType": { + "description": "Action variable type", + "type": "string" + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdHocFilterWithLabels": { + "description": "Define the AdHocFilterWithLabels type", + "type": "object", + "required": [ + "key", + "operator", + "value" + ], + "properties": { + "condition": { + "description": "@deprecated", + "type": "string" + }, + "forceEdit": { + "type": "boolean" + }, + "key": { + "type": "string" + }, + "keyLabel": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "origin": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFilterOrigin" + }, + "value": { + "type": "string" + }, + "valueLabels": { + "type": "array", + "items": { + "type": "string" + } + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdhocVariableKind": { + "description": "Adhoc variable kind", + "type": "object", + "required": [ + "kind", + "group", + "spec" + ], + "properties": { + "datasource": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "group": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdhocVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdhocVariableSpec": { + "description": "Adhoc variable specification", + "type": "object", + "required": [ + "name", + "baseFilters", + "filters", + "defaultKeys", + "hide", + "skipUrlSync", + "allowCustomValue" + ], + "properties": { + "allowCustomValue": { + "type": "boolean", + "default": true + }, + "baseFilters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdHocFilterWithLabels" + } + }, + "defaultKeys": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMetricFindValue" + } + }, + "description": { + "type": "string" + }, + "filters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdHocFilterWithLabels" + } + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationEventFieldMapping": { + "description": "Annotation event field mapping. Defines how to map a data frame field to an annotation event field.", + "type": "object", + "properties": { + "regex": { + "description": "Regular expression to apply to the field value", + "type": "string" + }, + "source": { + "description": "Source type for the field value", + "type": "string", + "default": "field" + }, + "value": { + "description": "Constant value to use when source is \"text\"", + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationPanelFilter": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "exclude": { + "description": "Should the specified panels be included or excluded", + "type": "boolean", + "default": false + }, + "ids": { + "description": "Panel IDs that should be included or excluded", + "type": "array", + "items": { + "type": "integer" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationQueryKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationQuerySpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationQueryPlacement": { + "description": "Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu", + "type": "string" + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationQuerySpec": { + "type": "object", + "required": [ + "query", + "enable", + "hide", + "iconColor", + "name" + ], + "properties": { + "builtIn": { + "type": "boolean", + "default": false + }, + "enable": { + "type": "boolean" + }, + "filter": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationPanelFilter" + }, + "hide": { + "type": "boolean" + }, + "iconColor": { + "type": "string" + }, + "legacyOptions": { + "description": "Catch-all field for datasource-specific properties. Should not be available in as code tooling.", + "type": "object", + "additionalProperties": true + }, + "mappings": { + "description": "Mappings define how to convert data frame fields to annotation event fields.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationEventFieldMapping" + } + }, + "name": { + "type": "string" + }, + "placement": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationQueryPlacement" + }, + "query": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataQueryKind" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutItemKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutItemSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutItemSpec": { + "type": "object", + "required": [ + "element" + ], + "properties": { + "conditionalRendering": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingGroupKind" + }, + "element": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardElementReference" + }, + "repeat": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridRepeatOptions" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutSpec": { + "type": "object", + "required": [ + "columnWidthMode", + "rowHeightMode", + "items" + ], + "properties": { + "columnWidth": { + "type": "number" + }, + "columnWidthMode": { + "type": "string", + "default": "standard", + "enum": [ + "narrow", + "standard", + "wide", + "custom" + ] + }, + "fillScreen": { + "type": "boolean", + "default": false + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutItemKind" + } + }, + "maxColumnCount": { + "type": "number", + "default": 3 + }, + "rowHeight": { + "type": "number" + }, + "rowHeightMode": { + "type": "string", + "default": "standard", + "enum": [ + "short", + "standard", + "tall", + "custom" + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridRepeatOptions": { + "type": "object", + "required": [ + "mode", + "value" + ], + "properties": { + "mode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatMode" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingDataKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingDataSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingDataSpec": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingGroupKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingGroupSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingGroupSpec": { + "type": "object", + "required": [ + "visibility", + "condition", + "items" + ], + "properties": { + "condition": { + "type": "string", + "enum": [ + "and", + "or" + ] + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingDataKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingTimeRangeSizeKind" + } + ] + } + }, + "visibility": { + "type": "string", + "enum": [ + "show", + "hide" + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingTimeRangeSizeKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingTimeRangeSizeSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingTimeRangeSizeSpec": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingVariableKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingVariableSpec": { + "type": "object", + "required": [ + "variable", + "operator", + "value" + ], + "properties": { + "operator": { + "type": "string", + "enum": [ + "equals", + "notEquals", + "matches", + "notMatches" + ] + }, + "value": { + "type": "string" + }, + "variable": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConstantVariableKind": { + "description": "Constant variable kind", + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConstantVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConstantVariableSpec": { + "description": "Constant variable specification", + "type": "object", + "required": [ + "name", + "query", + "current", + "hide", + "skipUrlSync" + ], + "properties": { + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "query": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConversionStatus": { + "description": "ConversionStatus is the status of the conversion of the dashboard.", + "type": "object", + "required": [ + "failed" + ], + "properties": { + "error": { + "description": "The error message from the conversion.\nEmpty if the conversion has not failed.", + "type": "string" + }, + "failed": { + "description": "Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.", + "type": "boolean" + }, + "source": { + "description": "The original value map[string]any", + "type": "object", + "additionalProperties": {} + }, + "storedVersion": { + "description": "The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.", + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardCustomVariableKind": { + "description": "Custom variable kind", + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardCustomVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardCustomVariableSpec": { + "description": "Custom variable specification", + "type": "object", + "required": [ + "name", + "query", + "current", + "options", + "multi", + "includeAll", + "hide", + "skipUrlSync", + "allowCustomValue" + ], + "properties": { + "allValue": { + "type": "string" + }, + "allowCustomValue": { + "type": "boolean", + "default": true + }, + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "includeAll": { + "type": "boolean", + "default": false + }, + "label": { + "type": "string" + }, + "multi": { + "type": "boolean", + "default": false + }, + "name": { + "type": "string", + "default": "" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + } + }, + "query": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardCursorSync": { + "description": "\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.", + "type": "string", + "enum": [ + "Crosshair", + "Tooltip", + "Off" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLink": { + "description": "Links with references to other dashboards or external resources", + "type": "object", + "required": [ + "title", + "type", + "icon", + "tooltip", + "tags", + "asDropdown", + "targetBlank", + "includeVars", + "keepTime" + ], + "properties": { + "asDropdown": { + "description": "If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards", + "type": "boolean", + "default": false + }, + "icon": { + "description": "Icon name to be displayed with the link", + "type": "string" + }, + "includeVars": { + "description": "If true, includes current template variables values in the link as query params", + "type": "boolean", + "default": false + }, + "keepTime": { + "description": "If true, includes current time range in the link as query params", + "type": "boolean", + "default": false + }, + "placement": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLinkPlacement" + }, + "tags": { + "description": "List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards", + "type": "array", + "items": { + "type": "string" + } + }, + "targetBlank": { + "description": "If true, the link will be opened in a new tab", + "type": "boolean", + "default": false + }, + "title": { + "description": "Title to display with the link", + "type": "string" + }, + "tooltip": { + "description": "Tooltip to display when the user hovers their mouse over it", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLinkType" + }, + "url": { + "description": "Link URL. Only required/valid if the type is link", + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLinkPlacement": { + "description": "Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu", + "type": "string" + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLinkType": { + "description": "Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)", + "type": "string", + "enum": [ + "link", + "dashboards" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataLink": { + "type": "object", + "required": [ + "title", + "url" + ], + "properties": { + "targetBlank": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataQueryKind": { + "type": "object", + "required": [ + "kind", + "group", + "version", + "spec" + ], + "properties": { + "datasource": { + "description": "New type for datasource reference\nNot creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.", + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "group": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "spec": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + }, + "version": { + "type": "string", + "default": "v0" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataTopic": { + "description": "A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.", + "type": "string", + "enum": [ + "series", + "annotations", + "alertStates" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataTransformerConfig": { + "description": "Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.", + "type": "object", + "required": [ + "id", + "options" + ], + "properties": { + "disabled": { + "description": "Disabled transformations are skipped", + "type": "boolean" + }, + "filter": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMatcherConfig" + }, + "id": { + "description": "Unique identifier of transformer", + "type": "string" + }, + "options": { + "description": "Options to be passed to the transformer\nValid options depend on the transformer id", + "type": "object", + "additionalProperties": {} + }, + "topic": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataTopic" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDatasourceVariableKind": { + "description": "Datasource variable kind", + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDatasourceVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDatasourceVariableSpec": { + "description": "Datasource variable specification", + "type": "object", + "required": [ + "name", + "pluginId", + "refresh", + "regex", + "current", + "options", + "multi", + "includeAll", + "hide", + "skipUrlSync", + "allowCustomValue" + ], + "properties": { + "allValue": { + "type": "string" + }, + "allowCustomValue": { + "type": "boolean", + "default": true + }, + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "includeAll": { + "type": "boolean", + "default": false + }, + "label": { + "type": "string" + }, + "multi": { + "type": "boolean", + "default": false + }, + "name": { + "type": "string", + "default": "" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + } + }, + "pluginId": { + "type": "string", + "default": "" + }, + "refresh": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRefresh" + }, + "regex": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDynamicConfigValue": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "default": "" + }, + "value": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardElement": { + "description": "Supported dashboard elements\n|* more element types in the future", + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardLibraryPanelKind" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardElementReference": { + "type": "object", + "required": [ + "kind", + "name" + ], + "properties": { + "kind": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFetchOptions": { + "type": "object", + "required": [ + "method", + "url" + ], + "properties": { + "body": { + "type": "string" + }, + "headers": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "method": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardHttpRequestMethod" + }, + "queryParams": { + "description": "These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array", + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldColor": { + "description": "Map a field to a color.", + "type": "object", + "required": [ + "mode" + ], + "properties": { + "fixedColor": { + "description": "The fixed color value for fixed or shades color modes.", + "type": "string" + }, + "mode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldColorModeId" + }, + "seriesBy": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldColorSeriesByMode" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldColorModeId": { + "description": "Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n`thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold\n`palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n`palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n`continuous-viridis`: Continuous Viridis palette mode\n`continuous-magma`: Continuous Magma palette mode\n`continuous-plasma`: Continuous Plasma palette mode\n`continuous-inferno`: Continuous Inferno palette mode\n`continuous-cividis`: Continuous Cividis palette mode\n`continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode\n`continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode\n`continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode\n`continuous-YlRd`: Continuous Yellow-Red palette mode\n`continuous-BlPu`: Continuous Blue-Purple palette mode\n`continuous-YlBl`: Continuous Yellow-Blue palette mode\n`continuous-blues`: Continuous Blue palette mode\n`continuous-reds`: Continuous Red palette mode\n`continuous-greens`: Continuous Green palette mode\n`continuous-purples`: Continuous Purple palette mode\n`shades`: Shades of a single color. Specify a single color, useful in an override rule.\n`fixed`: Fixed color mode. Specify a single color, useful in an override rule.", + "type": "string", + "enum": [ + "thresholds", + "palette-classic", + "palette-classic-by-name", + "continuous-viridis", + "continuous-magma", + "continuous-plasma", + "continuous-inferno", + "continuous-cividis", + "continuous-GrYlRd", + "continuous-RdYlGr", + "continuous-BlYlRd", + "continuous-YlRd", + "continuous-BlPu", + "continuous-YlBl", + "continuous-blues", + "continuous-reds", + "continuous-greens", + "continuous-purples", + "fixed", + "shades" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldColorSeriesByMode": { + "description": "Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.", + "type": "string", + "enum": [ + "min", + "max", + "last" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldConfig": { + "description": "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.", + "type": "object", + "properties": { + "actions": { + "description": "Define interactive HTTP requests that can be triggered from data visualizations.", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAction" + } + }, + "color": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldColor" + }, + "custom": { + "description": "custom is specified by the FieldConfig field\nin panel plugin schemas.", + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + }, + "decimals": { + "description": "Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to `String`.", + "type": "number" + }, + "description": { + "description": "Human readable field metadata", + "type": "string" + }, + "displayName": { + "description": "The display value for this field. This supports template variables blank is auto", + "type": "string" + }, + "displayNameFromDS": { + "description": "This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.", + "type": "string" + }, + "filterable": { + "description": "True if data source field supports ad-hoc filters", + "type": "boolean" + }, + "links": { + "description": "The behavior when clicking on a result", + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + } + }, + "mappings": { + "description": "Convert input values into a display string", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMapping" + } + }, + "max": { + "description": "The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.", + "type": "number" + }, + "min": { + "description": "The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.", + "type": "number" + }, + "noValue": { + "description": "Alternative to empty string", + "type": "string" + }, + "path": { + "description": "An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results", + "type": "string" + }, + "thresholds": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThresholdsConfig" + }, + "unit": { + "description": "Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n`suffix:` for custom unit that should go after value.\n`prefix:` for custom unit that should go before value.\n`time:` For custom date time formats type for example `time:YYYY-MM-DD`.\n`si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n`count:` for a custom count unit.\n`currency:` for custom a currency unit.", + "type": "string" + }, + "writeable": { + "description": "True if data source can write a value to the path. Auth/authz are supported separately", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldConfigSource": { + "description": "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.", + "type": "object", + "required": [ + "defaults", + "overrides" + ], + "properties": { + "defaults": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldConfig" + }, + "overrides": { + "description": "Overrides are the options applied to specific fields overriding the defaults.", + "type": "array", + "items": { + "type": "object", + "required": [ + "matcher", + "properties" + ], + "properties": { + "__systemRef": { + "description": "Describes config override rules created when interacting with Grafana.", + "type": "string" + }, + "matcher": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMatcherConfig" + }, + "properties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDynamicConfigValue" + } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFilterOrigin": { + "description": "Determine the origin of the adhoc variable filter", + "type": "string" + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutItemKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutItemSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutItemSpec": { + "type": "object", + "required": [ + "x", + "y", + "width", + "height", + "element" + ], + "properties": { + "element": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardElementReference" + }, + "height": { + "type": "integer" + }, + "repeat": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatOptions" + }, + "width": { + "type": "integer" + }, + "x": { + "type": "integer" + }, + "y": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutSpec": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutItemKind" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGroupByVariableKind": { + "description": "Group variable kind", + "type": "object", + "required": [ + "kind", + "group", + "spec" + ], + "properties": { + "datasource": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "group": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGroupByVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGroupByVariableSpec": { + "description": "GroupBy variable specification", + "type": "object", + "required": [ + "name", + "current", + "options", + "multi", + "hide", + "skipUrlSync" + ], + "properties": { + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "defaultValue": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "label": { + "type": "string" + }, + "multi": { + "type": "boolean", + "default": false + }, + "name": { + "type": "string", + "default": "" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + } + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardHttpRequestMethod": { + "type": "string", + "enum": [ + "GET", + "PUT", + "POST", + "DELETE", + "PATCH" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardInfinityOptions": { + "type": "object", + "required": [ + "method", + "url", + "datasourceUid" + ], + "properties": { + "body": { + "type": "string" + }, + "datasourceUid": { + "type": "string" + }, + "headers": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "method": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardHttpRequestMethod" + }, + "queryParams": { + "description": "These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array", + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardIntervalVariableKind": { + "description": "Interval variable kind", + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardIntervalVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardIntervalVariableSpec": { + "description": "Interval variable specification", + "type": "object", + "required": [ + "name", + "query", + "current", + "options", + "auto", + "auto_min", + "auto_count", + "refresh", + "hide", + "skipUrlSync" + ], + "properties": { + "auto": { + "type": "boolean", + "default": false + }, + "auto_count": { + "type": "integer", + "default": 0 + }, + "auto_min": { + "type": "string", + "default": "" + }, + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + } + }, + "query": { + "type": "string", + "default": "" + }, + "refresh": { + "type": "string" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardLibraryPanelKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardLibraryPanelKindSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardLibraryPanelKindSpec": { + "type": "object", + "required": [ + "id", + "title", + "libraryPanel" + ], + "properties": { + "id": { + "description": "Panel ID for the library panel in the dashboard", + "type": "number" + }, + "libraryPanel": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardLibraryPanelRef" + }, + "title": { + "description": "Title for the library panel in the dashboard", + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardLibraryPanelRef": { + "description": "A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.", + "type": "object", + "required": [ + "name", + "uid" + ], + "properties": { + "name": { + "description": "Library panel name", + "type": "string" + }, + "uid": { + "description": "Library panel uid", + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.Dashboard" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "DashboardList", + "version": "v2beta1" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMappingType": { + "description": "Supported value mapping types\n`value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n`range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n`regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n`special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.", + "type": "string", + "enum": [ + "value", + "range", + "regex", + "special" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMatcherConfig": { + "description": "Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.", + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "description": "The matcher id. This is used to find the matcher implementation from registry.", + "type": "string", + "default": "" + }, + "options": { + "description": "The matcher options. This is specific to the matcher implementation.", + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMetricFindValue": { + "description": "Define the MetricFindValue type", + "type": "object", + "required": [ + "text" + ], + "properties": { + "expandable": { + "type": "boolean" + }, + "group": { + "type": "string" + }, + "text": { + "type": "string" + }, + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelQueryKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelQuerySpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelQuerySpec": { + "type": "object", + "required": [ + "query", + "refId", + "hidden" + ], + "properties": { + "hidden": { + "type": "boolean" + }, + "query": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataQueryKind" + }, + "refId": { + "type": "string", + "default": "A" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelSpec": { + "type": "object", + "required": [ + "id", + "title", + "description", + "links", + "data", + "vizConfig" + ], + "properties": { + "data": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryGroupKind" + }, + "description": { + "type": "string" + }, + "id": { + "type": "number" + }, + "links": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataLink" + } + }, + "title": { + "type": "string" + }, + "transparent": { + "type": "boolean" + }, + "vizConfig": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVizConfigKind" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryGroupKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryGroupSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryGroupSpec": { + "type": "object", + "required": [ + "queries", + "transformations", + "queryOptions" + ], + "properties": { + "queries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardPanelQueryKind" + } + }, + "queryOptions": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryOptionsSpec" + }, + "transformations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTransformationKind" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryOptionsSpec": { + "type": "object", + "properties": { + "cacheTimeout": { + "type": "string" + }, + "hideTimeOverride": { + "type": "boolean" + }, + "interval": { + "type": "string" + }, + "maxDataPoints": { + "type": "integer" + }, + "queryCachingTTL": { + "type": "integer" + }, + "timeCompare": { + "type": "string" + }, + "timeFrom": { + "type": "string" + }, + "timeShift": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryVariableKind": { + "description": "Query variable kind", + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryVariableSpec": { + "description": "Query variable specification", + "type": "object", + "required": [ + "name", + "current", + "hide", + "refresh", + "skipUrlSync", + "query", + "regex", + "sort", + "options", + "multi", + "includeAll", + "allowCustomValue" + ], + "properties": { + "allValue": { + "type": "string" + }, + "allowCustomValue": { + "type": "boolean", + "default": true + }, + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "definition": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "includeAll": { + "type": "boolean", + "default": false + }, + "label": { + "type": "string" + }, + "multi": { + "type": "boolean", + "default": false + }, + "name": { + "type": "string", + "default": "" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + } + }, + "placeholder": { + "type": "string" + }, + "query": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataQueryKind" + }, + "refresh": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRefresh" + }, + "regex": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + }, + "sort": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableSort" + }, + "staticOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + } + }, + "staticOptionsOrder": { + "type": "string", + "enum": [ + "before", + "after", + "sorted" + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRangeMap": { + "description": "Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.", + "type": "object", + "required": [ + "type", + "options" + ], + "properties": { + "options": { + "description": "Range to match against and the result to apply when the value is within the range", + "type": "object", + "required": [ + "from", + "to", + "result" + ], + "properties": { + "from": { + "description": "Min value of the range. It can be null which means -Infinity", + "type": "number" + }, + "result": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMappingResult" + }, + "to": { + "description": "Max value of the range. It can be null which means +Infinity", + "type": "number" + } + }, + "additionalProperties": false + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMappingType" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRegexMap": { + "description": "Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.", + "type": "object", + "required": [ + "type", + "options" + ], + "properties": { + "options": { + "description": "Regular expression to match against and the result to apply when the value matches the regex", + "type": "object", + "required": [ + "pattern", + "result" + ], + "properties": { + "pattern": { + "description": "Regular expression to match against", + "type": "string" + }, + "result": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMappingResult" + } + }, + "additionalProperties": false + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMappingType" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatMode": { + "description": "other repeat modes will be added in the future: label, frame", + "type": "string" + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatOptions": { + "type": "object", + "required": [ + "mode", + "value" + ], + "properties": { + "direction": { + "type": "string", + "enum": [ + "h", + "v" + ] + }, + "maxPerRow": { + "type": "integer" + }, + "mode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatMode" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowRepeatOptions": { + "type": "object", + "required": [ + "mode", + "value" + ], + "properties": { + "mode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatMode" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutRowKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutRowSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutRowSpec": { + "type": "object", + "required": [ + "layout" + ], + "properties": { + "collapse": { + "type": "boolean" + }, + "conditionalRendering": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingGroupKind" + }, + "fillScreen": { + "type": "boolean" + }, + "hideHeader": { + "type": "boolean" + }, + "layout": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutKind" + } + ] + }, + "repeat": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowRepeatOptions" + }, + "title": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutSpec": { + "type": "object", + "required": [ + "rows" + ], + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutRowKind" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpec": { + "type": "object", + "required": [ + "annotations", + "cursorSync", + "elements", + "layout", + "links", + "preload", + "tags", + "timeSettings", + "title", + "variables" + ], + "properties": { + "annotations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAnnotationQueryKind" + } + }, + "cursorSync": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardCursorSync" + }, + "description": { + "description": "Description of dashboard.", + "type": "string" + }, + "editable": { + "description": "Whether a dashboard is editable or not.", + "type": "boolean", + "default": true + }, + "elements": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardElement" + } + }, + "layout": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutKind" + } + ] + }, + "links": { + "description": "Links with references to other dashboards or external websites.", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDashboardLink" + } + }, + "liveNow": { + "description": "When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.", + "type": "boolean" + }, + "preload": { + "description": "When set to true, the dashboard will load all panels in the dashboard when it's loaded.", + "type": "boolean", + "default": false + }, + "revision": { + "description": "Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.", + "type": "integer" + }, + "tags": { + "description": "Tags associated with dashboard.", + "type": "array", + "items": { + "type": "string" + } + }, + "timeSettings": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTimeSettingsSpec" + }, + "title": { + "description": "Title of dashboard.", + "type": "string" + }, + "variables": { + "description": "Configured template variables.", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableKind" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpecialValueMap": { + "description": "Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.", + "type": "object", + "required": [ + "type", + "options" + ], + "properties": { + "options": { + "type": "object", + "required": [ + "match", + "result" + ], + "properties": { + "match": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpecialValueMatch" + }, + "result": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMappingResult" + } + }, + "additionalProperties": false + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMappingType" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpecialValueMatch": { + "description": "Special value types supported by the `SpecialValueMap`", + "type": "string", + "enum": [ + "true", + "false", + "null", + "nan", + "null+nan", + "empty" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardStatus": { + "type": "object", + "properties": { + "conversion": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConversionStatus" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSwitchVariableKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSwitchVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSwitchVariableSpec": { + "type": "object", + "required": [ + "name", + "current", + "enabledValue", + "disabledValue", + "hide", + "skipUrlSync" + ], + "properties": { + "current": { + "type": "string", + "default": "false" + }, + "description": { + "type": "string" + }, + "disabledValue": { + "type": "string", + "default": "false" + }, + "enabledValue": { + "type": "string", + "default": "true" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabRepeatOptions": { + "type": "object", + "required": [ + "mode", + "value" + ], + "properties": { + "mode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRepeatMode" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutSpec": { + "type": "object", + "required": [ + "tabs" + ], + "properties": { + "tabs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutTabKind" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutTabKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutTabSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutTabSpec": { + "type": "object", + "required": [ + "layout" + ], + "properties": { + "conditionalRendering": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConditionalRenderingGroupKind" + }, + "layout": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRowsLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAutoGridLayoutKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabsLayoutKind" + } + ] + }, + "repeat": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTabRepeatOptions" + }, + "title": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTextVariableKind": { + "description": "Text variable kind", + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTextVariableSpec" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTextVariableSpec": { + "description": "Text variable specification", + "type": "object", + "required": [ + "name", + "current", + "query", + "hide", + "skipUrlSync" + ], + "properties": { + "current": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption" + }, + "description": { + "type": "string" + }, + "hide": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "query": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThreshold": { + "type": "object", + "required": [ + "value", + "color" + ], + "properties": { + "color": { + "type": "string" + }, + "value": { + "description": "Value null means -Infinity", + "type": "number" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThresholdsConfig": { + "type": "object", + "required": [ + "mode", + "steps" + ], + "properties": { + "mode": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThresholdsMode" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThreshold" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardThresholdsMode": { + "type": "string", + "enum": [ + "absolute", + "percentage" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTimeRangeOption": { + "type": "object", + "required": [ + "display", + "from", + "to" + ], + "properties": { + "display": { + "type": "string", + "default": "Last 6 hours" + }, + "from": { + "type": "string", + "default": "now-6h" + }, + "to": { + "type": "string", + "default": "now" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTimeSettingsSpec": { + "description": "Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.", + "type": "object", + "required": [ + "from", + "to", + "autoRefresh", + "autoRefreshIntervals", + "hideTimepicker", + "fiscalYearStartMonth" + ], + "properties": { + "autoRefresh": { + "description": "Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh", + "type": "string", + "default": "" + }, + "autoRefreshIntervals": { + "description": "Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals", + "type": "array", + "default": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "items": { + "type": "string" + } + }, + "fiscalYearStartMonth": { + "description": "The month that the fiscal year starts on. 0 = January, 11 = December", + "type": "integer", + "default": 0 + }, + "from": { + "description": "Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", + "type": "string", + "default": "now-6h" + }, + "hideTimepicker": { + "description": "Whether timepicker is visible or not.\nv1: timepicker.hidden", + "type": "boolean", + "default": false + }, + "nowDelay": { + "description": "Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay", + "type": "string" + }, + "quickRanges": { + "description": "Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTimeRangeOption" + } + }, + "timezone": { + "description": "Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".", + "type": "string", + "default": "browser" + }, + "to": { + "description": "End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", + "type": "string", + "default": "now" + }, + "weekStart": { + "description": "Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".", + "type": "string", + "enum": [ + "saturday", + "monday", + "sunday" + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTransformationKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "description": "The kind of a TransformationKind is the transformation ID", + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDataTransformerConfig" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMap": { + "description": "Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.", + "type": "object", + "required": [ + "type", + "options" + ], + "properties": { + "options": { + "description": "Map with : ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMappingResult" + } + }, + "type": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardMappingType" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMapping": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMap" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRangeMap" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardRegexMap" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpecialValueMap" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardValueMappingResult": { + "description": "Result used as replacement with text and color when the value matches", + "type": "object", + "properties": { + "color": { + "description": "Text to use when the value matches", + "type": "string" + }, + "icon": { + "description": "Icon to display when the value matches. Only specific visualizations.", + "type": "string" + }, + "index": { + "description": "Position in the mapping array. Only used internally.", + "type": "integer" + }, + "text": { + "description": "Text to display when the value matches", + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableHide": { + "description": "Determine if the variable shows on dashboard\nAccepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing), `inControlsMenu` (show in a drop-down menu).", + "type": "string", + "enum": [ + "dontHide", + "hideLabel", + "hideVariable", + "inControlsMenu" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableKind": { + "oneOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardQueryVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardTextVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardConstantVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardDatasourceVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardIntervalVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardCustomVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardGroupByVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAdhocVariableKind" + }, + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSwitchVariableKind" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableOption": { + "description": "Variable option specification", + "type": "object", + "required": [ + "text", + "value" + ], + "properties": { + "selected": { + "description": "Whether the option is selected or not", + "type": "boolean" + }, + "text": { + "description": "Text to be displayed for the option", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "value": { + "description": "Value of the option", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRefresh": { + "description": "Options to config when to refresh a variable\n`never`: Never refresh the variable\n`onDashboardLoad`: Queries the data source every time the dashboard loads.\n`onTimeRangeChanged`: Queries the data source when the dashboard time range changes.", + "type": "string", + "enum": [ + "never", + "onDashboardLoad", + "onTimeRangeChanged" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableSort": { + "description": "Sort variable options\nAccepted values are:\n`disabled`: No sorting\n`alphabeticalAsc`: Alphabetical ASC\n`alphabeticalDesc`: Alphabetical DESC\n`numericalAsc`: Numerical ASC\n`numericalDesc`: Numerical DESC\n`alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC\n`alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC\n`naturalAsc`: Natural ASC\n`naturalDesc`: Natural DESC\nVariableSort enum with default value", + "type": "string", + "enum": [ + "disabled", + "alphabeticalAsc", + "alphabeticalDesc", + "numericalAsc", + "numericalDesc", + "alphabeticalCaseInsensitiveAsc", + "alphabeticalCaseInsensitiveDesc", + "naturalAsc", + "naturalDesc" + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVizConfigKind": { + "type": "object", + "required": [ + "kind", + "group", + "version", + "spec" + ], + "properties": { + "group": { + "description": "The group is the plugin ID", + "type": "string" + }, + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVizConfigSpec" + }, + "version": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVizConfigSpec": { + "description": "--- Kinds ---", + "type": "object", + "required": [ + "options", + "fieldConfig" + ], + "properties": { + "fieldConfig": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardFieldConfigSource" + }, + "options": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardWithAccessInfo": { + "description": "This is like the legacy DTO where access and metadata are all returned in a single call", + "type": "object", + "required": [ + "metadata", + "spec", + "status", + "access" + ], + "properties": { + "access": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardAccess" + } + ] + }, + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Spec is the spec of the Dashboard", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardSpec" + } + ] + }, + "status": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "DashboardWithAccessInfo", + "version": "v2beta1" + } + ] + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { + "description": "APIResource specifies the name of a resource and whether it is namespaced.", + "type": "object", + "required": [ + "name", + "singularName", + "namespaced", + "kind", + "verbs" + ], + "properties": { + "categories": { + "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale\".", + "type": "string" + }, + "kind": { + "description": "kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo')", + "type": "string", + "default": "" + }, + "name": { + "description": "name is the plural name of the resource.", + "type": "string", + "default": "" + }, + "namespaced": { + "description": "namespaced indicates if a resource is namespaced or not.", + "type": "boolean", + "default": false + }, + "shortNames": { + "description": "shortNames is a list of suggested short names of the resource.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "singularName": { + "description": "singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface.", + "type": "string", + "default": "" + }, + "storageVersionHash": { + "description": "The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates.", + "type": "string" + }, + "verbs": { + "description": "verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy)", + "type": "array", + "items": { + "type": "string", + "default": "" + } + }, + "version": { + "description": "version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)\".", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { + "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", + "type": "object", + "required": [ + "groupVersion", + "resources" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "groupVersion": { + "description": "groupVersion is the group and version this APIResourceList is for.", + "type": "string", + "default": "" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "resources": { + "description": "resources contains the name of the resources and if they are namespaced.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResource" + } + ] + }, + "x-kubernetes-list-type": "atomic" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions": { + "description": "DeleteOptions may be provided when deleting an API object.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "dryRun": { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "gracePeriodSeconds": { + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "type": "integer", + "format": "int64" + }, + "ignoreStoreReadErrorWithClusterBreakingPotential": { + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "type": "boolean" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "orphanDependents": { + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "type": "boolean" + }, + "preconditions": { + "description": "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions" + } + ] + }, + "propagationPolicy": { + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { + "description": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", + "type": "object", + "properties": { + "continue": { + "description": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", + "type": "string" + }, + "remainingItemCount": { + "description": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", + "type": "integer", + "format": "int64" + }, + "resourceVersion": { + "description": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": "string" + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": "string" + }, + "fieldsV1": { + "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1" + } + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": "string" + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": "string" + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": "string" + }, + "time": { + "description": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "type": "object", + "properties": { + "annotations": { + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "creationTimestamp": { + "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "type": "integer", + "format": "int64" + }, + "deletionTimestamp": { + "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set", + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": "string" + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "type": "integer", + "format": "int64" + }, + "labels": { + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": "string" + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference" + } + ] + }, + "x-kubernetes-list-map-keys": [ + "uid" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "type": "object", + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string", + "default": "" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": "boolean" + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": "boolean" + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string", + "default": "" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string", + "default": "" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string", + "default": "" + } + }, + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Patch": { + "description": "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions": { + "description": "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", + "type": "object", + "properties": { + "resourceVersion": { + "description": "Specifies the target ResourceVersion", + "type": "string" + }, + "uid": { + "description": "Specifies the target UID.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Status": { + "description": "Status is a return value for calls that don't return other objects.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "code": { + "description": "Suggested HTTP return code for this status, 0 if not set.", + "type": "integer", + "format": "int32" + }, + "details": { + "description": "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails" + } + ], + "x-kubernetes-list-type": "atomic" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "message": { + "description": "A human-readable description of the status of this operation.", + "type": "string" + }, + "metadata": { + "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + }, + "reason": { + "description": "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", + "type": "string" + }, + "status": { + "description": "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause": { + "description": "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", + "type": "object", + "properties": { + "field": { + "description": "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", + "type": "string" + }, + "message": { + "description": "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", + "type": "string" + }, + "reason": { + "description": "A machine-readable description of the cause of the error. If this value is empty there is no information available.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails": { + "description": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", + "type": "object", + "properties": { + "causes": { + "description": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "The group attribute of the resource associated with the status StatusReason.", + "type": "string" + }, + "kind": { + "description": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", + "type": "string" + }, + "retryAfterSeconds": { + "description": "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", + "type": "integer", + "format": "int32" + }, + "uid": { + "description": "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "type": "string", + "format": "date-time" + } + } + } +} diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index 3e02a985eb1..b9d0283e37a 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -78,6 +78,9 @@ func TestIntegrationOpenAPIs(t *testing.T) { }, { Group: "dashboard.grafana.app", Version: "v2alpha1", + }, { + Group: "dashboard.grafana.app", + Version: "v2beta1", }, { Group: "folder.grafana.app", Version: "v1beta1", From 592c599ca6d6e7c000ee98c6b0b1a93d30b02d53 Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Sat, 6 Dec 2025 10:37:20 +0100 Subject: [PATCH 341/378] Alerting: Add configurable transport to historian app (#114935) --- .../historian/pkg/app/config/config.go | 10 ++++-- .../historian/pkg/app/config/config_test.go | 36 +++++++++++-------- .../pkg/app/notification/lokireader.go | 10 ++++-- .../apps/alerting/historian/register.go | 4 ++- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/apps/alerting/historian/pkg/app/config/config.go b/apps/alerting/historian/pkg/app/config/config.go index 5d8027d933b..cb3b3caa711 100644 --- a/apps/alerting/historian/pkg/app/config/config.go +++ b/apps/alerting/historian/pkg/app/config/config.go @@ -1,6 +1,7 @@ package config import ( + "net/http" "net/url" "time" @@ -15,9 +16,14 @@ const ( lokiDefaultMaxQuerySize = 65536 // 64kb ) +type LokiConfig struct { + lokiclient.LokiConfig + Transport http.RoundTripper +} + type NotificationConfig struct { Enabled bool - Loki lokiclient.LokiConfig + Loki LokiConfig } type RuntimeConfig struct { @@ -27,7 +33,7 @@ type RuntimeConfig struct { func (n *NotificationConfig) AddFlagsWithPrefix(prefix string, flags *pflag.FlagSet) { flags.BoolVar(&n.Enabled, prefix+".enabled", false, "Enable notification query endpoints") - addLokiFlags(&n.Loki, prefix+".loki", flags) + addLokiFlags(&n.Loki.LokiConfig, prefix+".loki", flags) } func (r *RuntimeConfig) AddFlagsWithPrefix(prefix string, flags *pflag.FlagSet) { diff --git a/apps/alerting/historian/pkg/app/config/config_test.go b/apps/alerting/historian/pkg/app/config/config_test.go index 8234f4c945f..7f8ab623a7a 100644 --- a/apps/alerting/historian/pkg/app/config/config_test.go +++ b/apps/alerting/historian/pkg/app/config/config_test.go @@ -24,10 +24,12 @@ func TestRuntimeConfig(t *testing.T) { expected: RuntimeConfig{ Notification: NotificationConfig{ Enabled: false, - Loki: lokiclient.LokiConfig{ - ReadPathURL: nil, - MaxQueryLength: 721 * time.Hour, - MaxQuerySize: 65536, + Loki: LokiConfig{ + LokiConfig: lokiclient.LokiConfig{ + ReadPathURL: nil, + MaxQueryLength: 721 * time.Hour, + MaxQuerySize: 65536, + }, }, }, }, @@ -38,10 +40,12 @@ func TestRuntimeConfig(t *testing.T) { expected: RuntimeConfig{ Notification: NotificationConfig{ Enabled: true, - Loki: lokiclient.LokiConfig{ - ReadPathURL: nil, - MaxQueryLength: 721 * time.Hour, - MaxQuerySize: 65536, + Loki: LokiConfig{ + LokiConfig: lokiclient.LokiConfig{ + ReadPathURL: nil, + MaxQueryLength: 721 * time.Hour, + MaxQuerySize: 65536, + }, }, }, }, @@ -57,13 +61,15 @@ func TestRuntimeConfig(t *testing.T) { expected: RuntimeConfig{ Notification: NotificationConfig{ Enabled: false, - Loki: lokiclient.LokiConfig{ - ReadPathURL: lokiURL, - BasicAuthUser: "foo", - BasicAuthPassword: "bar", - TenantID: "baz", - MaxQueryLength: 721 * time.Hour, - MaxQuerySize: 65536, + Loki: LokiConfig{ + LokiConfig: lokiclient.LokiConfig{ + ReadPathURL: lokiURL, + BasicAuthUser: "foo", + BasicAuthPassword: "bar", + TenantID: "baz", + MaxQueryLength: 721 * time.Hour, + MaxQuerySize: 65536, + }, }, }, }, diff --git a/apps/alerting/historian/pkg/app/notification/lokireader.go b/apps/alerting/historian/pkg/app/notification/lokireader.go index e8cea23dda7..c26519e59b4 100644 --- a/apps/alerting/historian/pkg/app/notification/lokireader.go +++ b/apps/alerting/historian/pkg/app/notification/lokireader.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "regexp" "sort" "strings" @@ -19,6 +20,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/apps/alerting/historian/pkg/apis/alertinghistorian/v0alpha1" + "github.com/grafana/grafana/apps/alerting/historian/pkg/app/config" "github.com/grafana/grafana/apps/alerting/historian/pkg/app/logutil" ) @@ -47,7 +49,7 @@ type LokiReader struct { logger logging.Logger } -func NewLokiReader(cfg lokiclient.LokiConfig, reg prometheus.Registerer, logger logging.Logger, tracer trace.Tracer) *LokiReader { +func NewLokiReader(cfg config.LokiConfig, reg prometheus.Registerer, logger logging.Logger, tracer trace.Tracer) *LokiReader { duration := instrument.NewHistogramCollector(promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{ Namespace: Namespace, Subsystem: Subsystem, @@ -56,9 +58,13 @@ func NewLokiReader(cfg lokiclient.LokiConfig, reg prometheus.Registerer, logger Buckets: instrument.DefBuckets, }, instrument.HistogramCollectorBuckets)) + requester := &http.Client{ + Transport: cfg.Transport, + } + gkLogger := logutil.ToGoKitLogger(logger) return &LokiReader{ - client: lokiclient.NewLokiClient(cfg, lokiclient.NewRequester(), nil, duration, gkLogger, tracer, LokiClientSpanName), + client: lokiclient.NewLokiClient(cfg.LokiConfig, requester, nil, duration, gkLogger, tracer, LokiClientSpanName), logger: logger, } } diff --git a/pkg/registry/apps/alerting/historian/register.go b/pkg/registry/apps/alerting/historian/register.go index 7fc2176d758..68830dcd0ef 100644 --- a/pkg/registry/apps/alerting/historian/register.go +++ b/pkg/registry/apps/alerting/historian/register.go @@ -42,7 +42,9 @@ func RegisterAppInstaller( appSpecificConfig.Notification = historianAppConfig.NotificationConfig{ Enabled: nhCfg.Enabled, - Loki: lokiConfig, + Loki: historianAppConfig.LokiConfig{ + LokiConfig: lokiConfig, + }, } } } From 78b1ae4f27c0d8ae5473faf55523da791bfccbff Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Sat, 6 Dec 2025 16:45:18 +0300 Subject: [PATCH 342/378] Search: Fix field selector parsing (#114940) --- pkg/storage/unified/apistore/util.go | 2 +- pkg/storage/unified/apistore/util_test.go | 43 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/pkg/storage/unified/apistore/util.go b/pkg/storage/unified/apistore/util.go index d3763f652a6..6cb4c5b31f8 100644 --- a/pkg/storage/unified/apistore/util.go +++ b/pkg/storage/unified/apistore/util.go @@ -124,7 +124,7 @@ func toListRequest(k *resourcepb.ResourceKey, opts storage.ListOptions) (*resour if r.Value != "" { requirement.Values = append(requirement.Values, r.Value) } - req.Options.Labels = append(req.Options.Labels, requirement) + req.Options.Fields = append(req.Options.Fields, requirement) } } diff --git a/pkg/storage/unified/apistore/util_test.go b/pkg/storage/unified/apistore/util_test.go index 0bee8adf75e..68ca1ad462f 100644 --- a/pkg/storage/unified/apistore/util_test.go +++ b/pkg/storage/unified/apistore/util_test.go @@ -117,6 +117,49 @@ func TestToListRequest(t *testing.T) { }, wantErr: nil, }, + { + name: "with field selector", + key: &resourcepb.ResourceKey{ + Group: "test", + Resource: "test", + Namespace: "default", + }, + opts: storage.ListOptions{ + Predicate: storage.SelectionPredicate{ + Label: labels.SelectorFromSet(labels.Set{"label": "A"}), + Field: fields.SelectorFromSet(fields.Set{"field": "B"}), + }, + }, + want: &resourcepb.ListRequest{ + VersionMatchV2: 1, + Options: &resourcepb.ListOptions{ + Key: &resourcepb.ResourceKey{ + Group: "test", + Resource: "test", + Namespace: "default", + }, + Labels: []*resourcepb.Requirement{ + { + Key: "label", + Operator: string(selection.Equals), + Values: []string{"A"}, + }, + }, + Fields: []*resourcepb.Requirement{ + { + Key: "field", + Operator: string(selection.Equals), + Values: []string{"B"}, + }, + }, + }, + }, + wantPredicate: storage.SelectionPredicate{ + Label: labels.SelectorFromSet(labels.Set{"label": "A"}), + Field: fields.SelectorFromSet(fields.Set{"field": "B"}), + }, + wantErr: nil, + }, { name: "with trash label", key: &resourcepb.ResourceKey{ From d0977b524561d8bfcc20687f0d10cd5e2c5a0754 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Mon, 8 Dec 2025 09:22:28 +0100 Subject: [PATCH 343/378] `grafana-iam`: Add role apis to the standalone app (#114897) --- pkg/registry/apis/iam/authorizer.go | 2 +- pkg/registry/apis/iam/register.go | 41 ++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index 0ec018d86de..05c8da97c2e 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -44,7 +44,7 @@ func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient auth authorizer := gfauthorizer.NewResourceAuthorizer(accessClient) resourceAuthorizer[iamv0.CoreRoleInfo.GetName()] = iamauthorizer.NewCoreRoleAuthorizer(accessClient) resourceAuthorizer[iamv0.RoleInfo.GetName()] = authorizer - resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = allowAuthorizer // Handled at storage layer + resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = allowAuthorizer // Handled by the backend wrapper resourceAuthorizer[iamv0.RoleBindingInfo.GetName()] = authorizer resourceAuthorizer[iamv0.ServiceAccountResourceInfo.GetName()] = authorizer resourceAuthorizer[iamv0.UserResourceInfo.GetName()] = authorizer diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 786635fa19a..32e2c9fefef 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/prometheus/client_golang/prometheus" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -23,7 +24,6 @@ import ( "github.com/grafana/authlib/types" iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" @@ -32,6 +32,7 @@ import ( iamauthorizer "github.com/grafana/grafana/pkg/registry/apis/iam/authorizer" "github.com/grafana/grafana/pkg/registry/apis/iam/externalgroupmapping" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" + "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/iam/resourcepermission" "github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount" "github.com/grafana/grafana/pkg/registry/apis/iam/sso" @@ -39,6 +40,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam/teambinding" "github.com/grafana/grafana/pkg/registry/apis/iam/user" "github.com/grafana/grafana/pkg/services/accesscontrol" + gfauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer/storewrapper" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/authz/zanzana" @@ -116,6 +118,8 @@ func RegisterAPIService( func NewAPIService( accessClient types.AccessClient, dbProvider legacysql.LegacyDatabaseProvider, + coreRoleStorage CoreRoleStorageBackend, + roleStorage RoleStorageBackend, features featuremgmt.FeatureToggles, zClient zanzana.Client, reg prometheus.Registerer, @@ -123,10 +127,17 @@ func NewAPIService( store := legacy.NewLegacySQLStores(dbProvider) resourcePermissionsStorage := resourcepermission.ProvideStorageBackend(dbProvider) registerMetrics(reg) + + resourceAuthorizer := gfauthorizer.NewResourceAuthorizer(accessClient) + coreRoleAuthorizer := iamauthorizer.NewCoreRoleAuthorizer(accessClient) + return &IdentityAccessManagementAPIBuilder{ store: store, display: user.NewLegacyDisplayREST(store), resourcePermissionsStorage: resourcePermissionsStorage, + rolesStorage: roleStorage, + coreRolesStorage: coreRoleStorage, + roleBindingsStorage: noopstorage.ProvideStorageBackend(), // TODO: add a proper storage backend logger: log.New("iam.apis"), features: features, accessClient: accessClient, @@ -135,20 +146,32 @@ func NewAPIService( reg: reg, authorizer: authorizer.AuthorizerFunc( func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + user, ok := types.AuthInfoFrom(ctx) + if !ok { + return authorizer.DecisionDeny, "no identity found", apierrors.NewUnauthorized("no identity found in context") + } + + if a.GetResource() == "coreroles" { + if user.GetIdentityType() != types.TypeAccessPolicy { + return authorizer.DecisionDeny, "only access policy identities have access for now", nil + } + return coreRoleAuthorizer.Authorize(ctx, a) + } + // For now only authorize resourcepermissions resource if a.GetResource() == "resourcepermissions" { - // Authorization is handled at the storage layer + // Authorization is handled by the backend wrapper return authorizer.DecisionAllow, "", nil } - user, err := identity.GetRequester(ctx) - if err != nil { - return authorizer.DecisionDeny, "no identity found", err + if a.GetResource() == "roles" { + if user.GetIdentityType() != types.TypeAccessPolicy { + return authorizer.DecisionDeny, "only access policy identities have access for now", nil + } + return resourceAuthorizer.Authorize(ctx, a) } - if user.GetIsGrafanaAdmin() { - return authorizer.DecisionAllow, "", nil - } - return authorizer.DecisionDeny, "only grafana admins have access for now", nil + + return authorizer.DecisionDeny, "access denied", nil }), } } From 8bf3ac97108cd826c76a845d1544541a6e632622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 8 Dec 2025 10:13:56 +0100 Subject: [PATCH 344/378] SelectBase: Use standard portal container (#114844) * SelectBase: Use standard portal container * Fixed positioning issue --- packages/grafana-ui/src/components/Select/SelectBase.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index 8609c307c25..cad0eb10e20 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -16,6 +16,7 @@ import { t, Trans } from '@grafana/i18n'; import { useTheme2 } from '../../themes/ThemeContext'; import { Icon } from '../Icon/Icon'; +import { getPortalContainer } from '../Portal/Portal'; import { CustomInput } from './CustomInput'; import { DropdownIndicator } from './DropdownIndicator'; @@ -123,7 +124,7 @@ export function SelectBase({ minMenuHeight, maxVisibleValues, menuPlacement = 'auto', - menuPosition, + menuPosition = 'fixed', menuShouldPortal = true, noOptionsMessage = t('grafana-ui.select.no-options-label', 'No options found'), onBlur, @@ -255,9 +256,9 @@ export function SelectBase({ maxVisibleValues, menuIsOpen: isOpen, menuPlacement: menuPlacement === 'auto' && closeToBottom ? 'top' : menuPlacement, - menuPosition, + menuPosition: menuShouldPortal ? 'fixed' : menuPosition, menuShouldBlockScroll: true, - menuPortalTarget: menuShouldPortal && typeof document !== 'undefined' ? document.body : undefined, + menuPortalTarget: menuShouldPortal && getPortalContainer(), menuShouldScrollIntoView: false, onBlur, onChange: onChangeWithEmpty, From 3490c3b0fdc8cab4fc40ccce89b4be9d82aec6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 8 Dec 2025 10:19:44 +0100 Subject: [PATCH 345/378] e2e: add tests for translations (#114390) e2e: add tests for translations --- devenv/plugins.yaml | 4 + .../components/App/App.tsx | 3 +- .../grafana-extensionstest-app/constants.ts | 1 + .../i18next.config.ts | 13 +++ .../en-US/grafana-extensionstest-app.json | 7 ++ .../es-ES/grafana-extensionstest-app.json | 7 ++ .../sv-SE/grafana-extensionstest-app.json | 7 ++ .../grafana-extensionstest-app/module.tsx | 3 + .../grafana-extensionstest-app/package.json | 10 ++- .../pages/Config.tsx | 17 ++++ .../pages/index.tsx | 1 + .../grafana-extensionstest-app/plugin.json | 5 +- .../tests/translations/french.spec.ts | 12 +++ .../tests/translations/swedish.spec.ts | 12 +++ .../webpack.config.ts | 1 + .../components/ConfigEditor.tsx | 27 ++++-- .../grafana-test-datasource/i18next.config.ts | 13 +++ .../en-US/grafana-e2etest-datasource.json | 23 +++++ .../es-ES/grafana-e2etest-datasource.json | 23 +++++ .../sv-SE/grafana-e2etest-datasource.json | 23 +++++ .../grafana-test-datasource/module.ts | 4 + .../grafana-test-datasource/package.json | 9 +- .../grafana-test-datasource/plugin.json | 5 +- .../tests/translations/french.spec.ts | 11 +++ .../tests/translations/swedish.spec.ts | 11 +++ .../grafana-test-datasource/webpack.config.ts | 1 + .../grafana-test-panel/CHANGELOG.md | 1 + .../test-plugins/grafana-test-panel/README.md | 0 .../components/SimplePanel.tsx | 83 +++++++++++++++++++ .../grafana-test-panel/i18next.config.ts | 13 +++ .../grafana-test-panel/img/logo.svg | 1 + .../locales/en-US/grafana-e2etest-panel.json | 30 +++++++ .../locales/es-ES/grafana-e2etest-panel.json | 30 +++++++ .../locales/sv-SE/grafana-e2etest-panel.json | 30 +++++++ .../test-plugins/grafana-test-panel/module.ts | 46 ++++++++++ .../grafana-test-panel/package.json | 50 +++++++++++ .../grafana-test-panel/plugin.json | 26 ++++++ .../tests/translations/french.spec.ts | 13 +++ .../tests/translations/swedish.spec.ts | 13 +++ .../grafana-test-panel/tsconfig.json | 8 ++ .../test-plugins/grafana-test-panel/types.ts | 7 ++ .../grafana-test-panel/webpack.config.ts | 45 ++++++++++ pkg/build/e2e-playwright/main.go | 5 ++ playwright.config.ts | 4 + scripts/grafana-server/custom.ini | 2 +- yarn.lock | 38 +++++++++ 46 files changed, 678 insertions(+), 20 deletions(-) create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/i18next.config.ts create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/locales/en-US/grafana-extensionstest-app.json create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/locales/es-ES/grafana-extensionstest-app.json create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/locales/sv-SE/grafana-extensionstest-app.json create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/pages/Config.tsx create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/french.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/swedish.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/i18next.config.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/locales/en-US/grafana-e2etest-datasource.json create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/locales/es-ES/grafana-e2etest-datasource.json create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/locales/sv-SE/grafana-e2etest-datasource.json create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/tests/translations/french.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/tests/translations/swedish.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/CHANGELOG.md create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/README.md create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/components/SimplePanel.tsx create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/i18next.config.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/img/logo.svg create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/locales/en-US/grafana-e2etest-panel.json create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/locales/es-ES/grafana-e2etest-panel.json create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/locales/sv-SE/grafana-e2etest-panel.json create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/module.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/package.json create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/plugin.json create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/tests/translations/french.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/tests/translations/swedish.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/tsconfig.json create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/types.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-panel/webpack.config.ts diff --git a/devenv/plugins.yaml b/devenv/plugins.yaml index 554a4828cff..0f292e324f1 100644 --- a/devenv/plugins.yaml +++ b/devenv/plugins.yaml @@ -21,3 +21,7 @@ apps: org_id: 1 org_name: Main Org. disabled: false +panels: + - type: grafana-e2etest-panel + org_id: 1 + org_name: Main Org. diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx index d57a1476e19..26d7b201466 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx @@ -3,7 +3,7 @@ import { Route, Routes } from 'react-router-dom'; import { AppRootProps } from '@grafana/data'; import { ROUTES } from '../../constants'; -import { AddedComponents, AddedLinks, ExposedComponents } from '../../pages'; +import { AddedComponents, AddedLinks, Config, ExposedComponents } from '../../pages'; import { testIds } from '../../testIds'; export function App(props: AppRootProps) { @@ -13,6 +13,7 @@ export function App(props: AppRootProps) { } /> } /> } /> + } /> } /> diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts index 120eb5d8191..c208781acf3 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts @@ -8,4 +8,5 @@ export enum ROUTES { ExposedComponents = 'exposed-components', AddedComponents = 'added-components', AddedLinks = 'added-links', + Config = 'config', } diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/i18next.config.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/i18next.config.ts new file mode 100644 index 00000000000..ba1645d38c9 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/i18next.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'i18next-cli'; +import pluginJson from './plugin.json'; + +export default defineConfig({ + locales: pluginJson.languages, + extract: { + input: ['**/*.{tsx,ts}'], + output: 'locales/{{language}}/{{namespace}}.json', + defaultNS: pluginJson.id, + functions: ['t', '*.t'], + transComponents: ['Trans'], + }, +}); diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/en-US/grafana-extensionstest-app.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/en-US/grafana-extensionstest-app.json new file mode 100644 index 00000000000..2fc28958864 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/en-US/grafana-extensionstest-app.json @@ -0,0 +1,7 @@ +{ + "config-page": { + "header": { + "text": "Is this translated" + } + } +} diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/es-ES/grafana-extensionstest-app.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/es-ES/grafana-extensionstest-app.json new file mode 100644 index 00000000000..2c2f51a239d --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/es-ES/grafana-extensionstest-app.json @@ -0,0 +1,7 @@ +{ + "config-page": { + "header": { + "text": "¿Está traducido?" + } + } +} diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/sv-SE/grafana-extensionstest-app.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/sv-SE/grafana-extensionstest-app.json new file mode 100644 index 00000000000..8bae86f58aa --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/locales/sv-SE/grafana-extensionstest-app.json @@ -0,0 +1,7 @@ +{ + "config-page": { + "header": { + "text": "Det här är översatt" + } + } +} diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx index 89ed1af12c4..9f585c0d5f7 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx @@ -3,6 +3,9 @@ import { App } from './components/App'; import { QueryModal } from './components/QueryModal'; import { selectQuery } from './utils/utils'; import pluginJson from './plugin.json'; +import { initPluginTranslations } from '@grafana/i18n'; + +await initPluginTranslations(pluginJson.id); export const plugin = new AppPlugin<{}>() .setRootPage(App) diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json index f89721aaac9..7f094c82b9b 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json @@ -6,7 +6,8 @@ "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development", "typecheck": "tsc --noEmit", - "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx ." + "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .", + "i18n-extract": "i18next-cli extract --sync-primary" }, "author": "Grafana Labs", "license": "Apache-2.0", @@ -20,17 +21,19 @@ "@types/semver": "7.5.8", "@types/uuid": "9.0.8", "glob": "10.5.0", + "i18next-cli": "^1.24.22", "ts-node": "10.9.2", "typescript": "5.5.4", "webpack": "5.95.0", "webpack-merge": "5.10.0" }, "engines": { - "node": ">=20" + "node": ">= 22 <25" }, "dependencies": { "@emotion/css": "11.11.2", "@grafana/data": "workspace:*", + "@grafana/i18n": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", @@ -42,5 +45,6 @@ }, "peerDependencies": { "@grafana/runtime": "*" - } + }, + "packageManager": "yarn@4.11.0" } diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/Config.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/Config.tsx new file mode 100644 index 00000000000..8b31e490242 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/Config.tsx @@ -0,0 +1,17 @@ +import { Trans } from '@grafana/i18n'; +import { PluginPage } from '@grafana/runtime'; +import { Stack } from '@grafana/ui'; + +export function Config() { + return ( + + +
+

+ Is this translated +

+
+
+
+ ); +} diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx index 1326d3c7bdf..84ddfc8e6ea 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx @@ -1,3 +1,4 @@ export { ExposedComponents } from './ExposedComponents'; export { AddedComponents } from './AddedComponents'; export { AddedLinks } from './AddedLinks'; +export { Config } from './Config'; diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json index 3f5adfa215a..c5ce29f2dc4 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json @@ -82,10 +82,11 @@ ] }, "dependencies": { - "grafanaDependency": ">=10.4.0", + "grafanaDependency": ">=12.0.0", "plugins": [], "extensions": { "exposedComponents": ["grafana-extensionexample1-app/reusable-component/v1", "grafana/add-to-dashboard-form/v1"] } - } + }, + "languages": ["en-US", "es-ES", "sv-SE"] } diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/french.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/french.spec.ts new file mode 100644 index 00000000000..a991453b0cc --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/french.spec.ts @@ -0,0 +1,12 @@ +import { FRENCH_FRANCE } from '@grafana/i18n'; +import { expect, test } from '@grafana/plugin-e2e'; +import pluginJson from '../../plugin.json'; +import { ROUTES } from '../../constants'; + +test.use({ userPreferences: { language: FRENCH_FRANCE } }); + +test('should display default translation (en-US)', async ({ gotoAppPage }) => { + const configPage = await gotoAppPage({ pluginId: pluginJson.id, path: ROUTES.Config }); + + await expect(configPage.ctx.page.getByText('Is this translated')).toBeVisible(); +}); diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/swedish.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/swedish.spec.ts new file mode 100644 index 00000000000..404f5053948 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/translations/swedish.spec.ts @@ -0,0 +1,12 @@ +import { SWEDISH_SWEDEN } from '@grafana/i18n'; +import { expect, test } from '@grafana/plugin-e2e'; +import pluginJson from '../../plugin.json'; +import { ROUTES } from '../../constants'; + +test.use({ userPreferences: { language: SWEDISH_SWEDEN } }); + +test('should display correct translation', async ({ gotoAppPage }) => { + const configPage = await gotoAppPage({ pluginId: pluginJson.id, path: ROUTES.Config }); + + await expect(configPage.ctx.page.getByText('Det här är översatt')).toBeVisible(); +}); diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts index 564555396a5..ceff913dbba 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts @@ -34,6 +34,7 @@ const config = async (env: Env): Promise => { ], }), ], + externals: [...(baseConfig.externals as any), 'i18next'], }; return mergeWithCustomize({ diff --git a/e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx b/e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx index 2c46992a5d9..06101e6e050 100644 --- a/e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx +++ b/e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx @@ -1,6 +1,7 @@ import { ChangeEvent } from 'react'; import { Checkbox, InlineField, InlineSwitch, Input, SecretInput, Select } from '@grafana/ui'; import { DataSourcePluginOptionsEditorProps, SelectableValue, toOption } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { MyDataSourceOptions, MySecureJsonData } from '../types'; interface Props extends DataSourcePluginOptionsEditorProps {} @@ -45,36 +46,46 @@ export function ConfigEditor(props: Props) { return ( <> - + ) => onJsonDataChange('path', e.target.value)} value={jsonData.path} - placeholder="Enter the path, e.g. /api/v1" + placeholder={t('config-editor.path.placeholder', 'Enter the path, e.g. /api/v1')} width={40} /> - + ) => onSecureJsonDataChange('path', e.target.value)} /> - + ) => onJsonDataChange('switchEnabled', e.target.checked)} /> - + ) => onJsonDataChange('checkboxEnabled', e.target.checked)} /> - + - + + render={({ field: { ref, value, onChange, ...field } }) => ( + {inputs.dataSources && inputs.dataSources.map((input: DataSourceInput) => { if (input.pluginId === ExpressionDatasourceRef.type) { @@ -102,6 +100,7 @@ export const ImportDashboardFormV2 = ({ key={input.pluginId} invalid={!!errors[dataSourceOption]} error={errors[dataSourceOption] ? 'Please select a data source' : undefined} + noMargin > name={dataSourceOption} @@ -133,7 +132,7 @@ export const ImportDashboardFormV2 = ({ ); })} - + - + ); }; diff --git a/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx index 9f87a602d3f..e71b419b8e4 100644 --- a/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx +++ b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx @@ -1,5 +1,3 @@ -import { useState } from 'react'; - import { locationUtil } from '@grafana/data'; import { locationService, reportInteraction } from '@grafana/runtime'; import { @@ -20,7 +18,6 @@ const IMPORT_FINISHED_EVENT_NAME = 'dashboard_import_imported'; type FormData = SaveDashboardCommand & { [key: `datasource-${string}`]: string }; export function ImportDashboardOverviewV2() { - const [uidReset, setUidReset] = useState(false); const dispatch = useDispatch(); // Get state from Redux store @@ -29,10 +26,6 @@ export function ImportDashboardOverviewV2() { const inputs = useSelector((state: StoreState) => state.importDashboard.inputs); const folder = searchObj.folderUid ? { uid: String(searchObj.folderUid) } : { uid: '' }; - function onUidReset() { - setUidReset(true); - } - function onCancel() { dispatch(clearLoadedDashboard()); } @@ -180,7 +173,7 @@ export function ImportDashboardOverviewV2() { <> onSubmit={onSubmit} - defaultValues={{ dashboard, k8s: { annotations: { 'grafana.app/folder': folder.uid } } }} + defaultValues={{ dashboard, folderUid: folder.uid, k8s: { annotations: { 'grafana.app/folder': folder.uid } } }} validateOnMount validateOn="onChange" > @@ -191,9 +184,7 @@ export function ImportDashboardOverviewV2() { errors={errors} control={control} getValues={getValues} - uidReset={uidReset} onCancel={onCancel} - onUidReset={onUidReset} onSubmit={onSubmit} watch={watch} /> From 83311049adbd6c0e3ba936897ec1252a699f849f Mon Sep 17 00:00:00 2001 From: Renato Costa <103441181+renatolabs@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:16:33 -0500 Subject: [PATCH 378/378] fix: create dashboard in legacy storage within transaction (#114808) fix: create dashboard within transaction --- .../apis/dashboard/legacy/sql_dashboards.go | 23 +++++++++- pkg/registry/apis/dashboard/legacy/storage.go | 11 ++++- .../api/dashboards/api_dashboards_test.go | 44 +++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 24ec135e785..fbb0e825cd1 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -140,11 +140,30 @@ func NewDashboardSQLAccess(sql legacysql.LegacyDatabaseProvider, } func (a *dashboardSqlAccess) executeQuery(ctx context.Context, helper *legacysql.LegacyDatabaseHelper, query string, args ...any) (*sql.Rows, error) { - // Use transaction if available in context. + var tx *sql.Tx + // After this function runs, the `tx` variable will only be set if + // this function was called in the context of a transaction set up by a + // caller upstream. In that case, we reuse the transaction. + _ = helper.DB.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + coreTx, err := sess.Tx() + if err != nil { + return nil + } + + tx = coreTx.Tx + return nil + }) + + // Use transaction from unified storage if available in the context. // This allows us to run migrations in a transaction which is specifically required for SQLite. - if tx := resource.TransactionFromContext(ctx); tx != nil { + if tx == nil { + tx = resource.TransactionFromContext(ctx) + } + + if tx != nil { return tx.QueryContext(ctx, query, args...) } + return helper.DB.GetSqlxSession().Query(ctx, query, args...) } diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index d8c948c9664..1521021c424 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -132,10 +132,19 @@ func (a *dashboardSqlAccess) WriteEvent(ctx context.Context, event resource.Writ } } else { failOnExisting := event.Type == resourcepb.WatchEvent_ADDED - after, _, err := a.SaveDashboard(ctx, info.OrgID, dash, failOnExisting) + sql, err := a.sql(ctx) if err != nil { return 0, err } + + var after *dashboard.Dashboard + if err := sql.DB.InTransaction(ctx, func(ctx context.Context) error { + var err error + after, _, err = a.SaveDashboard(ctx, info.OrgID, dash, failOnExisting) + return err + }); err != nil { + return 0, err + } if after != nil { meta, err := utils.MetaAccessor(after) if err != nil { diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index 50ab7d939ea..fcfb09cb5ba 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -233,6 +233,7 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) { err = resp.Body.Close() require.NoError(t, err) }) + t.Run("When updating uid with a dashboard already using that uid", func(t *testing.T) { resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ "dashboard": map[string]interface{}{ @@ -266,6 +267,34 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) { err = resp.Body.Close() require.NoError(t, err) }) + + t.Run("When creating a dashboard that references a non-existent library panel", func(t *testing.T) { + originalCount := getDashboardCount(t, grafanaListedAddr, "admin", "admin") + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Bad dashboard", + "panels": []interface{}{ + map[string]interface{}{ + "gridPos": map[string]int{"h": 0, "w": 0, "x": 0, "y": 0}, + "libraryPanel": map[string]string{ + "name": "Bad panel", + "uid": "invalid-uid", + }, + }, + }, + }, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "library element could not be found") + err = resp.Body.Close() + require.NoError(t, err) + + // A new dashboard is not created in this situation. + require.Equal(t, originalCount, getDashboardCount(t, grafanaListedAddr, "admin", "admin")) + }) } func TestIntegrationDashboardQuota(t *testing.T) { @@ -982,6 +1011,21 @@ func postDashboard(t *testing.T, grafanaListedAddr, user, password string, paylo return http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec } +func getDashboardCount(t *testing.T, grafanaListenAddr, user, password string) int { + endpoint := fmt.Sprintf("http://%s:%s@%s/apis/dashboard.grafana.app/v0alpha1/namespaces/default/search", user, password, grafanaListenAddr) + resp, err := http.Get(endpoint) //nolint:gosec + require.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + var payload map[string]any + require.NoError(t, json.Unmarshal(body, &payload)) + + return int(payload["totalHits"].(float64)) +} + func TestIntegrationDashboardServicePermissions(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t)