diff --git a/.betterer.results b/.betterer.results index b593c39ca68..f6c07dd6633 100644 --- a/.betterer.results +++ b/.betterer.results @@ -285,6 +285,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], + "packages/grafana-data/src/types/scopes.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "packages/grafana-data/src/types/select.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -1491,9 +1494,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] ], - "public/app/features/alerting/unified/Templates.test.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/components/Monaco/types\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/alerting/unified/components/AlertLabel.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], @@ -1728,9 +1728,6 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPoint.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], - "public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPointMenu.tsx:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`app/features/explore/extensions/ToolbarExtensionPointMenu\`)", "0"] - ], "public/app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] @@ -1901,9 +1898,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "11"], [0, 0, 0, "No untranslated strings. Wrap text with ", "12"] ], - "public/app/features/alerting/unified/components/receivers/TemplateEditor.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/components/Monaco/types\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/alerting/unified/components/receivers/TemplateForm.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], @@ -1988,10 +1982,8 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] ], "public/app/features/alerting/unified/components/receivers/form/fields/KeyValueMapInput.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], @@ -2022,9 +2014,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] ], - "public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/components/Monaco/types\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], @@ -2298,14 +2287,13 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "8"] ], "public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/components/Text/Text\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] ], "public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] @@ -2371,12 +2359,6 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/rule-viewer/tabs/Routing.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/alerting/unified/components/rules/ActionButton.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/components/Button\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], - "public/app/features/alerting/unified/components/rules/ActionIcon.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/components/Tooltip\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/alerting/unified/components/rules/AlertInstanceDetails.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], @@ -2814,15 +2796,9 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/rule-list/components/RuleGroup.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], - "public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/components/Text/Text\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/alerting/unified/state/AlertingQueryRunner.test.ts:5381": [ [0, 0, 0, "\'@grafana/runtime/src/utils/DataSourceWithBackend\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] ], - "public/app/features/alerting/unified/state/actions.ts:5381": [ - [0, 0, 0, "\'@grafana/runtime/src/utils/logging\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], "public/app/features/alerting/unified/types/receiver-form.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 92581c315aa..64363540c79 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -776,6 +776,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/doc-validator.yml @grafana/docs-tooling /.github/workflows/deploy-pr-preview.yml @grafana/docs-tooling /.github/workflows/epic-add-to-platform-ux-parent-project.yml @meanmina +/.github/workflows/feature-toggles-ci.yml @grafana/docs-tooling /.github/workflows/github-release.yml @grafana/grafana-developer-enablement-squad /.github/workflows/issue-opened.yml @grafana/grafana-community-support /.github/workflows/metrics-collector.yml @torkelo @@ -812,6 +813,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/trivy-scan.yml @grafana/grafana-backend-services-squad /.github/workflows/changelog.yml @zserge /.github/workflows/actions/changelog @zserge +/.github/workflows/frontend-unit-tests.yml @grafana/grafana-frontend-platform # Generated files not requiring owner approval /packages/grafana-data/src/types/featureToggles.gen.ts @grafanabot diff --git a/.github/renovate.json5 b/.github/renovate.json5 index a782ac2c0a0..43a537f2fa1 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -32,6 +32,7 @@ extends: ["schedule:monthly"], groupName: "Storybook updates", matchPackageNames: ["/^@?storybook/"], + rangeStrategy: "bump", }, { groupName: "React Aria", diff --git a/.github/workflows/feature-toggles-ci.yml b/.github/workflows/feature-toggles-ci.yml new file mode 100644 index 00000000000..a6c9f5c52dc --- /dev/null +++ b/.github/workflows/feature-toggles-ci.yml @@ -0,0 +1,25 @@ +name: Feature toggles CI + +on: + pull_request: + paths: + - 'pkg/services/featuremgmt/toggles_gen_test.go' + - 'pkg/services/featuremgmt/registry.go' + - 'docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache: true + + - name: Run feature toggle tests + run: go test -v -run TestFeatureToggleFiles ./pkg/services/featuremgmt/ diff --git a/.github/workflows/frontend-unit-tests.yml b/.github/workflows/frontend-unit-tests.yml new file mode 100644 index 00000000000..495bb647233 --- /dev/null +++ b/.github/workflows/frontend-unit-tests.yml @@ -0,0 +1,29 @@ +name: Frontend tests +on: + pull_request: + push: + branches: + - main + - release-*.*.* + +jobs: + test-frontend: + runs-on: ubuntu-latest-8-cores + name: "Unit tests (${{ matrix.chunk }} / 8)" + strategy: + fail-fast: true + matrix: + chunk: [1, 2, 3, 4, 5, 6, 7, 8] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + cache-dependency-path: 'yarn.lock' + - run: yarn install --immutable --check-cache + - run: yarn run test:ci + env: + TEST_MAX_WORKERS: 2 + TEST_SHARD: ${{ matrix.chunk }} + TEST_SHARD_TOTAL: 8 diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 74dc1dfe028..cf49c4e862d 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -32,7 +32,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect @@ -83,7 +83,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index cae5be00879..3b3312aebf7 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -60,8 +60,8 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -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= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -269,8 +269,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go. google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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/investigations/go.mod b/apps/investigations/go.mod index 12da166af27..7e9c06424d0 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -27,7 +27,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect @@ -73,7 +73,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.32.1 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index d3caa3d08e3..bc0a2332d93 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -40,8 +40,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -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= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -195,8 +195,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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/playlist/go.mod b/apps/playlist/go.mod index cd07f8c199e..474148a6799 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -28,7 +28,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect @@ -74,7 +74,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.32.1 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index d3caa3d08e3..bc0a2332d93 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -40,8 +40,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -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= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -195,8 +195,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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/go.mod b/go.mod index d32c2843663..e664a31975a 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/golang/mock v1.7.0-rc.1 // @grafana/alerting-backend github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group github.com/golang/snappy v0.0.4 // @grafana/alerting-backend - github.com/google/go-cmp v0.6.0 // @grafana/grafana-backend-group + github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group github.com/google/wire v0.6.0 // @grafana/grafana-backend-group @@ -88,7 +88,7 @@ require ( github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.2.1 // @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.266.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.267.0 // @grafana/plugins-platform-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // @grafana/observability-traces-and-profiling @@ -180,7 +180,7 @@ require ( gonum.org/v1/gonum v0.15.1 // @grafana/oss-big-tent google.golang.org/api v0.216.0 // @grafana/grafana-backend-group google.golang.org/grpc v1.70.0 // @grafana/plugins-platform-backend - google.golang.org/protobuf v1.36.4 // @grafana/plugins-platform-backend + google.golang.org/protobuf v1.36.5 // @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 gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend @@ -331,7 +331,7 @@ require ( github.com/dolthub/maphash v0.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/edsrzf/mmap-go v1.2.0 // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/proto v1.13.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect diff --git a/go.sum b/go.sum index 96e57fa263f..29c820b8964 100644 --- a/go.sum +++ b/go.sum @@ -1094,8 +1094,8 @@ github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8E github.com/efficientgo/core v1.0.0-rc.3 h1:X6CdgycYWDcbYiJr1H1+lQGzx13o7bq3EUkbB9DsSPc= github.com/efficientgo/core v1.0.0-rc.3/go.mod h1:FfGdkzWarkuzOlY04VY+bGfb1lWrjaL6x/GLcQ4vJps= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -1422,8 +1422,9 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 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/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -1558,8 +1559,8 @@ github.com/grafana/grafana-google-sdk-go v0.2.1 h1:XeFdKnkXBjOJjXc1gf4iMx4h5aCHT github.com/grafana/grafana-google-sdk-go v0.2.1/go.mod h1:RiITSHwBhqVTTd3se3HQq5Ncs/wzzhTB9OK5N0J0PEU= 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.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8 h1:mG/6nDlEBVxWlo2GQJVASzucw3ByPIBsec06XcPrjgQ= github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250220163425-b4c4b9abbdc8 h1:w42GlvkmHG4nM/p1kb2nKmROVP+AHtL3qWEYMhnhCVM= @@ -3357,8 +3358,8 @@ google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= diff --git a/go.work.sum b/go.work.sum index 0e79edfd1d2..25fc8254a99 100644 --- a/go.work.sum +++ b/go.work.sum @@ -142,8 +142,6 @@ cloud.google.com/go/memcache v1.11.1 h1:2FGuyd3WY7buNDAkMBdmeIOheNWA3gwaXrttLrEd cloud.google.com/go/memcache v1.11.1/go.mod h1:3zF+dEqmEmElHuO4NtHiShekQY5okQtssjPBv7jpmZ8= cloud.google.com/go/metastore v1.14.1 h1:kGx+IUSSYCVn8LisCT4fpxCC9rauEVonzi7RlygdqWY= cloud.google.com/go/metastore v1.14.1/go.mod h1:WDvsAcbQLl9M4xL+eIpbKogH7aEaPWMhO9aRBcFOnJE= -cloud.google.com/go/monitoring v1.21.1 h1:zWtbIoBMnU5LP9A/fz8LmWMGHpk4skdfeiaa66QdFGc= -cloud.google.com/go/monitoring v1.21.1/go.mod h1:Rj++LKrlht9uBi8+Eb530dIrzG/cU/lB8mt+lbeFK1c= cloud.google.com/go/networkconnectivity v1.15.1 h1:EizN+cFGHzRAyiFTK8jT1PqTo+cSnbc2IGh6OmllS7Y= cloud.google.com/go/networkconnectivity v1.15.1/go.mod h1:tYAcT4Ahvq+BiePXL/slYipf/8FF0oNJw3MqFhBnSPI= cloud.google.com/go/networkmanagement v1.14.1 h1:0x3hVI6xbp3N/choffKPHMSxbzaPdHSD92cBElebXEk= @@ -204,8 +202,6 @@ cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkv cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= cloud.google.com/go/shell v1.8.1 h1:etoJal+LB7Pn8+5vE2aAh6QcFbBmerIOh5MxNDoXykw= cloud.google.com/go/shell v1.8.1/go.mod h1:jaU7OHeldDhTwgs3+clM0KYEDYnBAPevUI6wNLf7ycE= -cloud.google.com/go/spanner v1.70.0 h1:nj6p/GJTgMDiSQ1gQ034ItsKuJgHiMOjtOlONOg8PSo= -cloud.google.com/go/spanner v1.70.0/go.mod h1:X5T0XftydYp0K1adeJQDJtdWpbrOeJ7wHecM4tK6FiE= cloud.google.com/go/speech v1.25.1 h1:iGZJS3wrdkje/Vqiacx1+r+zVwUZoXVMdklYIVsvfNw= cloud.google.com/go/speech v1.25.1/go.mod h1:WgQghvghkZ1htG6BhYn98mP7Tg0mti8dBFDLMVXH/vM= cloud.google.com/go/storagetransfer v1.11.1 h1:Hd7H1zXGQGEWyWXxWVXDMuNCGasNQim1y9CIaMZIBX8= @@ -280,10 +276,6 @@ github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW5 github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0 h1:kAtNAWwvTt5+iew6baV0kbOrtjYTXPtWNSyOFlcxkBU= github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0/go.mod h1:VRKXU8C7Y/aUKjRBTGfw0Ndv4YqNxlB8zAPJJDxbASE= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 h1:oVLqHXhnYtUwM89y9T1fXGaK9wTkXHgNp8/ZNMQzUxE= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc= github.com/IBM/go-sdk-core/v5 v5.17.4/go.mod h1:KsAAI7eStAWwQa4F96MLy+whYSh39JzNjklZRbN/8ns= github.com/IBM/ibm-cos-sdk-go v1.11.0 h1:Jp55NLN3OvBwucMGpP5wNybyjncsmTZ9+GPHai/1cE8= @@ -387,7 +379,6 @@ github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9Mwe github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= @@ -510,6 +501,7 @@ github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJ github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= github.com/elazarl/goproxy v1.3.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= @@ -593,6 +585,7 @@ github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs0 github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= @@ -907,7 +900,6 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stoewer/parquet-cli v0.0.7 h1:rhdZODIbyMS3twr4OM3am8BPPT5pbfMcHLH93whDM5o= github.com/stoewer/parquet-cli v0.0.7/go.mod h1:bskxHdj8q3H1EmfuCqjViFoeO3NEvs5lzZAQvI8Nfjk= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/substrait-io/substrait v0.57.1 h1:GW8nnYfSowMseHR8Os82/X6lNtQGIK7p4p+lr6r+auw= github.com/substrait-io/substrait v0.57.1/go.mod h1:q9s+tjo+gK0lsA+SqYB0lhojNuxvdPdfYlGUP0hjbrA= github.com/substrait-io/substrait-go v1.2.0 h1:3ZNRkc8FYD7ifCagKEOZQtUcgMceMQfwo2N1NGaK4Q4= @@ -1055,8 +1047,6 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.53.0 h1:BdkKDtcrHThgjcEia1737O go.opentelemetry.io/contrib/bridges/prometheus v0.53.0/go.mod h1:ZkhVxcJgeXlL/lVyT/vxNHVFiSG5qOaDwYaSgD8IfZo= go.opentelemetry.io/contrib/config v0.7.0 h1:b1rK5tGTuhhPirJiMxOcyQfZs76j2VapY6ODn3b2Dbs= go.opentelemetry.io/contrib/config v0.7.0/go.mod h1:8tdiFd8N5etOi3XzBmAoMxplEzI3TcL8dU5rM5/xcOQ= -go.opentelemetry.io/contrib/detectors/gcp v1.32.0 h1:P78qWqkLSShicHmAzfECaTgvslqHxblNE9j62Ws1NK8= -go.opentelemetry.io/contrib/detectors/gcp v1.32.0/go.mod h1:TVqo0Sda4Cv8gCIixd7LuLwW4EylumVWfhjZJjDD4DU= go.opentelemetry.io/contrib/exporters/autoexport v0.53.0 h1:13K+tY7E8GJInkrvRiPAhC0gi/7vKjzDNhtmCf+QXG8= go.opentelemetry.io/contrib/exporters/autoexport v0.53.0/go.mod h1:lyQF6xQ4iDnMg4sccNdFs1zf62xd79YI8vZqKjOTwMs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= @@ -1137,12 +1127,9 @@ golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1200,6 +1187,7 @@ google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= diff --git a/package.json b/package.json index 012ee9da149..a07f4835a14 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lint": "yarn run lint:ts && yarn run lint:sass", "lint:ts": "eslint ./ ./public/app/extensions/ --cache --no-error-on-unmatched-pattern", "lint:sass": "yarn stylelint '{public/sass,packages}/**/*.scss' --cache", - "test:ci": "mkdir -p reports/junit && JEST_JUNIT_OUTPUT_DIR=reports/junit jest --ci --reporters=default --reporters=jest-junit -w ${TEST_MAX_WORKERS:-100%}", + "test:ci": "mkdir -p reports/junit && JEST_JUNIT_OUTPUT_DIR=reports/junit jest --ci --reporters=default --reporters=jest-junit -w ${TEST_MAX_WORKERS:-100%} --shard=${TEST_SHARD:-1}/${TEST_SHARD_TOTAL:-1}", "lint:fix": "yarn lint:ts --fix", "packages:build": "nx run-many -t build --projects='tag:scope:package'", "packages:clean": "rimraf ./npm-artifacts && nx run-many -t clean --projects='tag:scope:package' --maxParallel=100", @@ -276,8 +276,8 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "6.0.2", - "@grafana/scenes-react": "6.0.2", + "@grafana/scenes": "6.1.4", + "@grafana/scenes-react": "6.1.4", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index ec84e8f0d7b..67c8483ed11 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -576,6 +576,8 @@ export { type ScopeNodeSpec, type ScopeNode, scopeFilterOperatorMap, + reverseScopeFilterOperatorMap, + isEqualityOrMultiOperator, } from './types/scopes'; export { PluginState, diff --git a/packages/grafana-data/src/types/scopes.ts b/packages/grafana-data/src/types/scopes.ts index 90579b72017..2f85a19237d 100644 --- a/packages/grafana-data/src/types/scopes.ts +++ b/packages/grafana-data/src/types/scopes.ts @@ -18,6 +18,12 @@ export interface ScopeDashboardBinding { } export type ScopeFilterOperator = 'equals' | 'not-equals' | 'regex-match' | 'regex-not-match' | 'one-of' | 'not-one-of'; +export type EqualityOrMultiOperator = Extract; + +export function isEqualityOrMultiOperator(value: string): value is EqualityOrMultiOperator { + const operators = new Set(['equals', 'not-equals', 'one-of', 'not-one-of']); + return operators.has(value); +} export const scopeFilterOperatorMap: Record = { '=': 'equals', @@ -28,6 +34,11 @@ export const scopeFilterOperatorMap: Record = { '!=|': 'not-one-of', }; +export const reverseScopeFilterOperatorMap: Record = Object.fromEntries( + Object.entries(scopeFilterOperatorMap).map(([symbol, operator]) => [operator, symbol]) + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions +) as Record; + export interface ScopeSpecFilter { key: string; value: string; 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 a8a7511443d..bb799f4053e 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -574,6 +574,7 @@ ResponsiveGridLayoutItemKind: { ResponsiveGridLayoutItemSpec: { element: ElementReference + repeat?: ResponsiveGridRepeatOptions } TabsLayoutKind: { 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 a8fe2500f3a..77bb17e5d4f 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -867,6 +867,7 @@ export const defaultResponsiveGridLayoutItemKind = (): ResponsiveGridLayoutItemK export interface ResponsiveGridLayoutItemSpec { element: ElementReference; + repeat?: ResponsiveGridRepeatOptions; } export const defaultResponsiveGridLayoutItemSpec = (): ResponsiveGridLayoutItemSpec => ({ diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index edfe54e00b2..cac9067a6a9 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.6 require ( github.com/emicklei/go-restful/v3 v3.11.0 - github.com/grafana/grafana-plugin-sdk-go v0.266.0 + github.com/grafana/grafana-plugin-sdk-go v0.267.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 @@ -39,7 +39,7 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -59,7 +59,7 @@ require ( github.com/google/cel-go v0.22.1 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect @@ -150,7 +150,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index eb715898bf0..11bd9b72803 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -49,8 +49,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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -118,8 +118,9 @@ github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvR github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 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= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -134,8 +135,8 @@ 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.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I= github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA= @@ -495,8 +496,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 033f48738ec..a3f8b199c92 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -23,6 +23,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -43,7 +44,7 @@ require ( golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // 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 diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index bd82bcbb03f..a2532bad3ff 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -25,8 +25,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -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= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -151,8 +151,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 0c45aa74e58..59a16f25782 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -5,7 +5,7 @@ go 1.23.4 toolchain go1.23.6 require ( - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 github.com/grafana/grafana-app-sdk/logging v0.30.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 @@ -93,7 +93,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index b3d15d3aee2..f1076d70d55 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -68,8 +68,8 @@ github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvR github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -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= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -312,8 +312,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/apiserver/rest/dualwriter.go b/pkg/apiserver/rest/dualwriter.go index 15be881b250..31ca7e9c97e 100644 --- a/pkg/apiserver/rest/dualwriter.go +++ b/pkg/apiserver/rest/dualwriter.go @@ -26,8 +26,18 @@ var ( _ rest.SingularNameProvider = (DualWriter)(nil) ) +type dualWriteContextKey struct{} + +func IsDualWriteUpdate(ctx context.Context) bool { + return ctx.Value(dualWriteContextKey{}) == true +} + +func WithDualWriteUpdate(ctx context.Context) context.Context { + return context.WithValue(ctx, dualWriteContextKey{}, true) +} + // Function that will create a dual writer -type DualWriteBuilder func(gr schema.GroupResource, legacy LegacyStorage, storage Storage) (Storage, error) +type DualWriteBuilder func(gr schema.GroupResource, legacy Storage, unified Storage) (Storage, error) // Storage is a storage implementation that satisfies the same interfaces as genericregistry.Store. type Storage interface { @@ -36,26 +46,12 @@ type Storage interface { rest.TableConvertor rest.SingularNameProvider rest.Getter - // TODO: when watch is implemented, we can replace all the below with rest.StandardStorage rest.Lister rest.CreaterUpdater rest.GracefulDeleter rest.CollectionDeleter } -// LegacyStorage is a storage implementation that writes to the Grafana SQL database. -type LegacyStorage interface { - rest.Storage - rest.Scoper - rest.SingularNameProvider - rest.CreaterUpdater - rest.Lister - rest.GracefulDeleter - rest.CollectionDeleter - rest.TableConvertor - rest.Getter -} - // DualWriter is a storage implementation that writes first to LegacyStorage and then to Storage. // If writing to LegacyStorage fails, the write to Storage is skipped and the error is returned. // Storage is used for all read operations. This is useful as a migration step from SQL based @@ -79,7 +75,6 @@ type LegacyStorage interface { type DualWriter interface { Storage - LegacyStorage Mode() DualWriterMode } @@ -110,8 +105,8 @@ const ( // NewDualWriter returns a new DualWriter. func NewDualWriter( mode DualWriterMode, - legacy LegacyStorage, - storage Storage, + legacy Storage, + unified Storage, reg prometheus.Registerer, resource string, ) Storage { @@ -122,17 +117,17 @@ func NewDualWriter( return legacy case Mode1: // read and write only from legacy storage - return newDualWriterMode1(legacy, storage, metrics, resource) + return newDualWriterMode1(legacy, unified, metrics, resource) case Mode2: // write to both, read from storage but use legacy as backup - return newDualWriterMode2(legacy, storage, metrics, resource) + return newDualWriterMode2(legacy, unified, metrics, resource) case Mode3: // write to both, read from storage only - return newDualWriterMode3(legacy, storage, metrics, resource) + return newDualWriterMode3(legacy, unified, metrics, resource) case Mode4, Mode5: - return storage + return unified default: - return newDualWriterMode1(legacy, storage, metrics, resource) + return newDualWriterMode1(legacy, unified, metrics, resource) } } diff --git a/pkg/apiserver/rest/dualwriter_mode1.go b/pkg/apiserver/rest/dualwriter_mode1.go index 2ce24e52008..7395fc5d13b 100644 --- a/pkg/apiserver/rest/dualwriter_mode1.go +++ b/pkg/apiserver/rest/dualwriter_mode1.go @@ -15,7 +15,7 @@ import ( ) type DualWriterMode1 struct { - Legacy LegacyStorage + Legacy Storage Storage Storage *dualWriterMetrics resource string @@ -26,7 +26,7 @@ const mode1Str = "1" // NewDualWriterMode1 returns a new DualWriter in mode 1. // Mode 1 represents writing to and reading from LegacyStorage. -func newDualWriterMode1(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode1 { +func newDualWriterMode1(legacy Storage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode1 { return &DualWriterMode1{ Legacy: legacy, Storage: storage, diff --git a/pkg/apiserver/rest/dualwriter_mode1_test.go b/pkg/apiserver/rest/dualwriter_mode1_test.go index e5504e4df4e..398b25a3acc 100644 --- a/pkg/apiserver/rest/dualwriter_mode1_test.go +++ b/pkg/apiserver/rest/dualwriter_mode1_test.go @@ -60,10 +60,10 @@ func TestMode1_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -122,10 +122,10 @@ func TestMode1_CreateOnUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -190,10 +190,10 @@ func TestMode1_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -251,10 +251,10 @@ func TestMode1_GetFromUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -308,10 +308,10 @@ func TestMode1_List(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -365,10 +365,10 @@ func TestMode1_ListFromUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -434,10 +434,10 @@ func TestMode1_Delete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -494,10 +494,10 @@ func TestMode1_DeleteFromUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -565,10 +565,10 @@ func TestMode1_DeleteCollection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -626,10 +626,10 @@ func TestMode1_DeleteCollectionFromUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -695,10 +695,10 @@ func TestMode1_Update(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -761,10 +761,10 @@ func TestMode1_UpdateOnUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { diff --git a/pkg/apiserver/rest/dualwriter_mode2.go b/pkg/apiserver/rest/dualwriter_mode2.go index 84e841270e5..17aae84e2fb 100644 --- a/pkg/apiserver/rest/dualwriter_mode2.go +++ b/pkg/apiserver/rest/dualwriter_mode2.go @@ -16,15 +16,9 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" ) -type dualWriteContextKey struct{} - -func IsDualWriteUpdate(ctx context.Context) bool { - return ctx.Value(dualWriteContextKey{}) == true -} - type DualWriterMode2 struct { Storage Storage - Legacy LegacyStorage + Legacy Storage *dualWriterMetrics resource string Log klog.Logger @@ -35,7 +29,7 @@ const mode2Str = "2" // newDualWriterMode2 returns a new DualWriter in mode 2. // Mode 2 represents writing to LegacyStorage first, then to Storage. // When reading, values from LegacyStorage will be returned. -func newDualWriterMode2(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode2 { +func newDualWriterMode2(legacy Storage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode2 { return &DualWriterMode2{ Legacy: legacy, Storage: storage, diff --git a/pkg/apiserver/rest/dualwriter_mode2_test.go b/pkg/apiserver/rest/dualwriter_mode2_test.go index dfd7b6db0f5..b0d653053df 100644 --- a/pkg/apiserver/rest/dualwriter_mode2_test.go +++ b/pkg/apiserver/rest/dualwriter_mode2_test.go @@ -54,10 +54,10 @@ func TestMode2_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -138,10 +138,10 @@ func TestMode2_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -212,10 +212,10 @@ func TestMode2_List(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -313,10 +313,10 @@ func TestMode2_Delete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -382,10 +382,10 @@ func TestMode2_DeleteCollection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -451,10 +451,10 @@ func TestMode2_Update(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { diff --git a/pkg/apiserver/rest/dualwriter_mode3.go b/pkg/apiserver/rest/dualwriter_mode3.go index bd28ef3c31c..db61f0ed7f7 100644 --- a/pkg/apiserver/rest/dualwriter_mode3.go +++ b/pkg/apiserver/rest/dualwriter_mode3.go @@ -17,7 +17,7 @@ import ( ) type DualWriterMode3 struct { - Legacy LegacyStorage + Legacy Storage Storage Storage watchImp rest.Watcher // watch is only available in mode 3 and 4 *dualWriterMetrics @@ -27,7 +27,7 @@ type DualWriterMode3 struct { // newDualWriterMode3 returns a new DualWriter in mode 3. // Mode 3 represents writing to LegacyStorage and Storage and reading from Storage. -func newDualWriterMode3(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode3 { +func newDualWriterMode3(legacy Storage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode3 { return &DualWriterMode3{ Legacy: legacy, Storage: storage, diff --git a/pkg/apiserver/rest/dualwriter_mode3_test.go b/pkg/apiserver/rest/dualwriter_mode3_test.go index f68287f4ead..832c8957aeb 100644 --- a/pkg/apiserver/rest/dualwriter_mode3_test.go +++ b/pkg/apiserver/rest/dualwriter_mode3_test.go @@ -61,10 +61,10 @@ func TestMode3_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -128,10 +128,10 @@ func TestMode3_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -187,10 +187,10 @@ func TestMode1_GetFromLegacyStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -234,10 +234,10 @@ func TestMode3_List(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupStorageFn != nil { @@ -311,10 +311,10 @@ func TestMode3_Delete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -380,10 +380,10 @@ func TestMode3_DeleteCollection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -449,10 +449,10 @@ func TestMode3_Update(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { diff --git a/pkg/apiserver/rest/dualwriter_syncer.go b/pkg/apiserver/rest/dualwriter_syncer.go index 9c36fdd22b7..b15cf5376ac 100644 --- a/pkg/apiserver/rest/dualwriter_syncer.go +++ b/pkg/apiserver/rest/dualwriter_syncer.go @@ -33,7 +33,7 @@ type SyncerConfig struct { RequestInfo *request.RequestInfo Mode DualWriterMode - LegacyStorage LegacyStorage + LegacyStorage Storage Storage Storage ServerLockService ServerLockService diff --git a/pkg/apiserver/rest/dualwriter_syncer_test.go b/pkg/apiserver/rest/dualwriter_syncer_test.go index d9d94cea54c..c1e0a216e9f 100644 --- a/pkg/apiserver/rest/dualwriter_syncer_test.go +++ b/pkg/apiserver/rest/dualwriter_syncer_test.go @@ -181,12 +181,12 @@ func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) { // mode 1 for _, tt := range tests { t.Run("Mode-1-"+tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) lm := &mock.Mock{} um := &mock.Mock{} - ls := legacyStoreMock{lm, l} + ls := storageMock{lm, l} us := storageMock{um, s} if tt.setupLegacyFn != nil { @@ -221,12 +221,12 @@ func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) { // mode 2 for _, tt := range tests { t.Run("Mode-2-"+tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) lm := &mock.Mock{} um := &mock.Mock{} - ls := legacyStoreMock{lm, l} + ls := storageMock{lm, l} us := storageMock{um, s} if tt.setupLegacyFn != nil { diff --git a/pkg/apiserver/rest/dualwriter_test.go b/pkg/apiserver/rest/dualwriter_test.go index 13fc007dc53..3faf131424e 100644 --- a/pkg/apiserver/rest/dualwriter_test.go +++ b/pkg/apiserver/rest/dualwriter_test.go @@ -64,7 +64,7 @@ func TestSetDualWritingMode(t *testing.T) { } for _, tt := range tests { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) sm := &mock.Mock{} @@ -75,7 +75,7 @@ func TestSetDualWritingMode(t *testing.T) { lm := &mock.Mock{} lm.On("List", mock.Anything, mock.Anything).Return(exampleList, nil) - ls := legacyStoreMock{lm, l} + ls := storageMock{lm, l} serverLockSvc := &fakeServerLock{ err: tt.serverLockError, diff --git a/pkg/apiserver/rest/storage_mocks_test.go b/pkg/apiserver/rest/storage_mocks_test.go index 582bcdc067c..3baaa14c526 100644 --- a/pkg/apiserver/rest/storage_mocks_test.go +++ b/pkg/apiserver/rest/storage_mocks_test.go @@ -11,102 +11,11 @@ import ( "k8s.io/apiserver/pkg/registry/rest" ) -type legacyStoreMock struct { - *mock.Mock - LegacyStorage -} - type storageMock struct { *mock.Mock Storage } -func (m legacyStoreMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, name, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, obj, createValidation, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) NewList() runtime.Object { - return nil -} - -func (m legacyStoreMock) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - select { - case <-ctx.Done(): - return nil, false, errors.New("context canceled") - default: - } - args := m.Called(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) - if err := args.Get(2); err != nil { - return nil, false, err.(error) - } - return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) -} - -func (m legacyStoreMock) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - select { - case <-ctx.Done(): - return nil, false, errors.New("context canceled") - default: - } - - args := m.Called(ctx, name, deleteValidation, options) - if err := args.Get(2); err != nil { - return nil, false, err.(error) - } - return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) -} - -func (m legacyStoreMock) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - args := m.Called(ctx, deleteValidation, options, listOptions) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - // Unified Store func (m storageMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { select { diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 1d5f29a7bed..fd52527bf94 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -14,7 +14,7 @@ require ( github.com/docker/docker v27.4.1+incompatible // @grafana/grafana-developer-enablement-squad github.com/drone/drone-cli v1.8.0 // @grafana/grafana-developer-enablement-squad github.com/gogo/protobuf v1.3.2 // indirect; @grafana/alerting-backend - github.com/google/go-cmp v0.6.0 // @grafana/grafana-backend-group + github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group github.com/google/go-github/v69 v69.2.0 // @grafana/grafana-developer-enablement-squad github.com/google/uuid v1.6.0 // indirect; @grafana/grafana-backend-group github.com/googleapis/gax-go/v2 v2.14.1 // indirect; @grafana/grafana-backend-group @@ -35,7 +35,7 @@ require ( golang.org/x/time v0.9.0 // indirect; @grafana/grafana-backend-group google.golang.org/api v0.216.0 // @grafana/grafana-backend-group google.golang.org/grpc v1.70.0 // indirect; @grafana/plugins-platform-backend - google.golang.org/protobuf v1.36.4 // indirect; @grafana/plugins-platform-backend + google.golang.org/protobuf v1.36.5 // indirect; @grafana/plugins-platform-backend gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend ) diff --git a/pkg/build/go.sum b/pkg/build/go.sum index 7c868c04a4c..b1e8e4c3aa9 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -126,8 +126,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -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/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -362,8 +362,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/build/wire/go.mod b/pkg/build/wire/go.mod index ee43dc13181..61a2bb2b9db 100644 --- a/pkg/build/wire/go.mod +++ b/pkg/build/wire/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/build/wire go 1.23.1 require ( - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/google/subcommands v1.2.0 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 golang.org/x/tools v0.29.0 diff --git a/pkg/build/wire/go.sum b/pkg/build/wire/go.sum index 56cfeb71f60..07103d75876 100644 --- a/pkg/build/wire/go.sum +++ b/pkg/build/wire/go.sum @@ -1,5 +1,5 @@ -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -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/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= diff --git a/pkg/codegen/go.mod b/pkg/codegen/go.mod index 3c3b0bfa94b..f6f900fc6b8 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -21,7 +21,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/golang/glog v1.2.4 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/pkg/codegen/go.sum b/pkg/codegen/go.sum index 25214c4b99e..e125a5bdc0f 100644 --- a/pkg/codegen/go.sum +++ b/pkg/codegen/go.sum @@ -25,8 +25,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -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/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/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index f0b4c12f7ec..7e96faa86ac 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -1,5 +1,5 @@ -//go:build enterprise -// +build enterprise +//go:build enterprise || pro +// +build enterprise pro package extensions diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index 5d415a7f052..8205acbb824 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -21,7 +21,7 @@ require ( github.com/getkin/kin-openapi v0.129.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/pkg/plugins/codegen/go.sum b/pkg/plugins/codegen/go.sum index a940ced9650..c6205d86509 100644 --- a/pkg/plugins/codegen/go.sum +++ b/pkg/plugins/codegen/go.sum @@ -24,8 +24,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -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/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/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 95380de2939..fb2946bb264 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.6 require ( github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 - github.com/grafana/grafana-plugin-sdk-go v0.266.0 + github.com/grafana/grafana-plugin-sdk-go v0.267.0 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_golang v1.20.5 github.com/prometheus/common v0.62.0 @@ -33,7 +33,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.17.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -48,7 +48,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect @@ -121,7 +121,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 66a0da7e790..f6b5b3b3d42 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -55,8 +55,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/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -100,8 +100,9 @@ github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZat github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 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= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -120,8 +121,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 h1:IR+UNYHqaU31t8/TArJk8K/GlDwOyxMpGNkWCXeZ28g= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040/go.mod h1:SPLNCARd4xdjCkue0O6hvuoveuS1dGJjDnfxYe405YQ= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= 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.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= @@ -394,8 +395,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go b/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go index 6b1377dc1f3..65f5593cf3a 100644 --- a/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go @@ -13,7 +13,7 @@ import ( model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/receiver/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" - grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" alertingac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -22,7 +22,7 @@ import ( ) var ( - _ grafanaRest.LegacyStorage = (*legacyStorage)(nil) + _ grafanarest.Storage = (*legacyStorage)(nil) ) type ReceiverService interface { diff --git a/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go b/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go index 00c20880212..07f5138aca8 100644 --- a/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go @@ -11,14 +11,14 @@ import ( "k8s.io/apiserver/pkg/registry/rest" model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/routingtree/v0alpha1" - grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models" ) var ( - _ grafanaRest.LegacyStorage = (*legacyStorage)(nil) + _ grafanarest.Storage = (*legacyStorage)(nil) ) type RouteService interface { diff --git a/pkg/registry/apis/alerting/notifications/templategroup/legacy_storage.go b/pkg/registry/apis/alerting/notifications/templategroup/legacy_storage.go index a492c4b3b49..ada3f463820 100644 --- a/pkg/registry/apis/alerting/notifications/templategroup/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/templategroup/legacy_storage.go @@ -4,13 +4,14 @@ import ( "context" "fmt" - "github.com/grafana/alerting/templates" "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/apiserver/pkg/registry/rest" + "github.com/grafana/alerting/templates" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/templategroup/v0alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -19,7 +20,7 @@ import ( ) var ( - _ grafanarest.LegacyStorage = (*legacyStorage)(nil) + _ grafanarest.Storage = (*legacyStorage)(nil) ) type TemplateService interface { diff --git a/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go b/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go index 7a7ef74165a..bb382ee9525 100644 --- a/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go @@ -11,14 +11,14 @@ import ( "k8s.io/apiserver/pkg/registry/rest" model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1" - grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ) var ( - _ grafanaRest.LegacyStorage = (*legacyStorage)(nil) + _ grafanarest.Storage = (*legacyStorage)(nil) ) type TimeIntervalService interface { diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go index 214c2ad2202..b5e033df390 100644 --- a/pkg/registry/apis/dashboard/legacy_storage.go +++ b/pkg/registry/apis/dashboard/legacy_storage.go @@ -28,7 +28,7 @@ type DashboardStorage struct { Features featuremgmt.FeatureToggles } -func (s *DashboardStorage) NewStore(scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter, reg prometheus.Registerer) (grafanarest.LegacyStorage, error) { +func (s *DashboardStorage) NewStore(scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter, reg prometheus.Registerer) (grafanarest.Storage, error) { server, err := resource.NewResourceServer(resource.ResourceServerOptions{ Backend: s.Access, Reg: reg, diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index c223026576f..a06dfe50a0f 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -17,6 +17,8 @@ import ( "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" + "github.com/prometheus/client_golang/prometheus" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/utils" dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard" @@ -42,7 +44,6 @@ import ( "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/storage/unified/apistore" "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/prometheus/client_golang/prometheus" ) var ( @@ -247,7 +248,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver func (b *DashboardsAPIBuilder) storageForVersion( opts builder.APIGroupOptions, - legacyStore grafanarest.LegacyStorage, + legacyStore grafanarest.Storage, largeObjects apistore.LargeObjectSupport, newDTOFunc func() runtime.Object, ) (map[string]rest.Storage, error) { diff --git a/pkg/registry/apis/folders/legacy_storage.go b/pkg/registry/apis/folders/legacy_storage.go index 0c96769a942..34dcaa9a73b 100644 --- a/pkg/registry/apis/folders/legacy_storage.go +++ b/pkg/registry/apis/folders/legacy_storage.go @@ -116,7 +116,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO } list.Items = append(list.Items, *r) } - if len(list.Items) >= int(paging.limit) { + if int64(len(list.Items)) >= paging.limit { list.Continue = paging.GetNextPageToken() } return list, nil diff --git a/pkg/registry/apps/playlist/register.go b/pkg/registry/apps/playlist/register.go index 9f79bb22f1d..a4215d880d4 100644 --- a/pkg/registry/apps/playlist/register.go +++ b/pkg/registry/apps/playlist/register.go @@ -48,7 +48,7 @@ func RegisterApp( return provider } -func (p *PlaylistAppProvider) legacyStorageGetter(requested schema.GroupVersionResource) grafanarest.LegacyStorage { +func (p *PlaylistAppProvider) legacyStorageGetter(requested schema.GroupVersionResource) grafanarest.Storage { gvr := schema.GroupVersionResource{ Group: playlistv0alpha1.PlaylistKind().Group(), Version: playlistv0alpha1.PlaylistKind().Version(), diff --git a/pkg/semconv/go.mod b/pkg/semconv/go.mod index b79133b4a5b..d015899f50c 100644 --- a/pkg/semconv/go.mod +++ b/pkg/semconv/go.mod @@ -6,5 +6,6 @@ require go.opentelemetry.io/otel v1.34.0 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect ) diff --git a/pkg/semconv/go.sum b/pkg/semconv/go.sum index 2b997e160c5..2d9cf378678 100644 --- a/pkg/semconv/go.sum +++ b/pkg/semconv/go.sum @@ -1,7 +1,7 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -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/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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index 33e06c2580b..8f6fe0e3783 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -286,7 +286,7 @@ func InstallAPIs( // nolint:staticcheck if storageOpts.StorageType != options.StorageTypeLegacy { - dualWrite = func(gr schema.GroupResource, legacy grafanarest.LegacyStorage, storage grafanarest.Storage) (grafanarest.Storage, error) { + dualWrite = func(gr schema.GroupResource, legacy grafanarest.Storage, storage grafanarest.Storage) (grafanarest.Storage, error) { // Dashboards + Folders may be managed (depends on feature toggles and database state) if dualWriteService != nil && dualWriteService.ShouldManage(gr) { return dualWriteService.NewStorage(gr, legacy, storage) // eventually this can replace this whole function diff --git a/pkg/services/apiserver/builder/runner/builder.go b/pkg/services/apiserver/builder/runner/builder.go index 1bc052fb46e..8a7b18a5fe2 100644 --- a/pkg/services/apiserver/builder/runner/builder.go +++ b/pkg/services/apiserver/builder/runner/builder.go @@ -18,7 +18,7 @@ import ( var _ AppBuilder = (*appBuilder)(nil) -type LegacyStorageGetter func(schema.GroupVersionResource) grafanarest.LegacyStorage +type LegacyStorageGetter func(schema.GroupVersionResource) grafanarest.Storage type AppBuilderConfig struct { Authorizer authorizer.Authorizer diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index df738065715..ee01105e37c 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -29,7 +29,7 @@ import ( func ProvideService(cfg *setting.Cfg, authenticator authn.Authenticator, features featuremgmt.FeatureToggles, ) *ContextHandler { return &ContextHandler{ - Cfg: cfg, + cfg: cfg, authenticator: authenticator, features: features, } @@ -37,7 +37,7 @@ func ProvideService(cfg *setting.Cfg, authenticator authn.Authenticator, feature // ContextHandler is a middleware. type ContextHandler struct { - Cfg *setting.Cfg + cfg *setting.Cfg authenticator authn.Authenticator features featuremgmt.FeatureToggles } @@ -104,7 +104,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { // inject ReqContext in the context ctx = context.WithValue(ctx, reqContextKey{}, reqContext) // store list of possible auth header in context - ctx = WithAuthHTTPHeaders(ctx, h.Cfg) + ctx = WithAuthHTTPHeaders(ctx, h.cfg) // Set the context for the http.Request.Context // This modifies both r and reqContext.Req since they point to the same value *reqContext.Req = *reqContext.Req.WithContext(ctx) @@ -137,7 +137,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { attribute.Int64("userId", reqContext.UserID), )) - if h.Cfg.IDResponseHeaderEnabled && reqContext.SignedInUser != nil { + if h.cfg.IDResponseHeaderEnabled && reqContext.SignedInUser != nil { reqContext.Resp.Before(h.addIDHeaderEndOfRequestFunc(reqContext.SignedInUser)) } @@ -167,11 +167,11 @@ func (h *ContextHandler) addIDHeaderEndOfRequestFunc(ident identity.Requester) w return } - if _, ok := h.Cfg.IDResponseHeaderNamespaces[string(ident.GetIdentityType())]; !ok { + if _, ok := h.cfg.IDResponseHeaderNamespaces[string(ident.GetIdentityType())]; !ok { return } - headerName := fmt.Sprintf("%s-Identity-Id", h.Cfg.IDResponseHeaderPrefix) + headerName := fmt.Sprintf("%s-Identity-Id", h.cfg.IDResponseHeaderPrefix) w.Header().Add(headerName, ident.GetID()) } } diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index 6130ea6e279..27bcbb845d1 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -334,10 +334,9 @@ func getRestoreVersion(msg string) (int, error) { return 0, nil } - ver, err := strconv.ParseInt(parts[1], 10, 64) + ver, err := strconv.Atoi(parts[1]) if err != nil { return 0, err } - - return int(ver), nil + return ver, nil } diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index e9dec2d4481..5dca89080a5 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -103,12 +103,46 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel. // RouteConvertPrometheusDeleteNamespace deletes all rule groups that were imported from a Prometheus-compatible source // within a specified namespace. func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response { - return response.Error(501, "Not implemented", nil) + logger := srv.logger.FromContext(c.Req.Context()) + + logger.Debug("Looking up folder in the root by title", "folder_title", namespaceTitle) + namespace, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser) + if err != nil { + return namespaceErrorResponse(err) + } + logger.Info("Deleting all Prometheus-imported rule groups", "folder_uid", namespace.UID, "folder_title", namespaceTitle) + + filterOpts := &provisioning.FilterOptions{ + NamespaceUIDs: []string{namespace.UID}, + ImportedPrometheusRule: util.Pointer(true), + } + err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, models.ProvenanceConvertedPrometheus, filterOpts) + if err != nil { + logger.Error("Failed to delete rule groups", "folder_uid", namespace.UID, "error", err) + return errorToResponse(err) + } + + return successfulResponse() } // RouteConvertPrometheusDeleteRuleGroup deletes a specific rule group if it was imported from a Prometheus-compatible source. func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { - return response.Error(501, "Not implemented", nil) + logger := srv.logger.FromContext(c.Req.Context()) + + logger.Debug("Looking up folder in the root by title", "folder_title", namespaceTitle) + folder, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser) + if err != nil { + return namespaceErrorResponse(err) + } + logger.Info("Deleting Prometheus-imported rule group", "folder_uid", folder.UID, "folder_title", namespaceTitle, "group", group) + + err = srv.alertRuleService.DeleteRuleGroup(c.Req.Context(), c.SignedInUser, folder.UID, group, models.ProvenanceConvertedPrometheus) + if err != nil { + logger.Error("Failed to delete rule group", "folder_uid", folder.UID, "group", group, "error", err) + return errorToResponse(err) + } + + return successfulResponse() } // RouteConvertPrometheusGetNamespace returns the Grafana-managed alert rules for a specified namespace (folder). @@ -220,7 +254,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextm return errorToResponse(err) } - return response.JSON(http.StatusAccepted, map[string]string{"status": "success"}) + return successfulResponse() } func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext, title string, logger log.Logger) (*folder.Folder, response.Response) { @@ -363,3 +397,9 @@ func namespaceErrorResponse(err error) response.Response { return toNamespaceErrorResponse(err) } + +func successfulResponse() response.Response { + return response.JSON(http.StatusAccepted, apimodels.ConvertPrometheusResponse{ + Status: "success", + }) +} diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index 05f84347334..51b378d437c 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -2,6 +2,7 @@ package api import ( "context" + "fmt" "net/http" "net/http/httptest" "testing" @@ -77,6 +78,79 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { require.Equal(t, http.StatusAccepted, response.Status()) }) + t.Run("should replace an existing rule group", func(t *testing.T) { + provenanceStore := fakes.NewFakeProvisioningStore() + srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore)) + + // 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) + + // And a rule + rule := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithGroupName(simpleGroup.Name)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition("123")). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + + rc := createRequestCtx() + response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup) + require.Equal(t, http.StatusAccepted, response.Status()) + + // Get the updated rule + remaining, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{ + OrgID: 1, + }) + require.NoError(t, err) + require.Len(t, remaining, 1) + + require.Equal(t, simpleGroup.Name, remaining[0].RuleGroup) + require.Equal(t, fmt.Sprintf("[%s] %s", simpleGroup.Name, simpleGroup.Rules[0].Alert), remaining[0].Title) + promRuleYAML, err := yaml.Marshal(simpleGroup.Rules[0]) + require.NoError(t, err) + require.Equal(t, string(promRuleYAML), remaining[0].PrometheusRuleDefinition()) + }) + + t.Run("should fail to replace a provisioned rule group", func(t *testing.T) { + provenanceStore := fakes.NewFakeProvisioningStore() + srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore)) + + // 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) + + rule := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithGroupName(simpleGroup.Name)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition("123")). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + // mark the rule as provisioned + err := provenanceStore.SetProvenance(context.Background(), rule, 1, models.ProvenanceAPI) + require.NoError(t, err) + + rc := createRequestCtx() + response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup) + require.Equal(t, http.StatusConflict, response.Status()) + + // Verify the rule is still present + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.NoError(t, err) + require.NotNil(t, remaining) + }) + t.Run("with valid pause header values should return 202", func(t *testing.T) { testCases := []struct { name string @@ -420,9 +494,219 @@ func TestRouteConvertPrometheusGetRules(t *testing.T) { }) } -func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasources.CacheService, *fakes.RuleStore, *foldertest.FakeService) { +func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) { + t.Run("for non-existent folder should return 404", func(t *testing.T) { + srv, _, _, _ := createConvertPrometheusSrv(t) + rc := createRequestCtx() + + response := srv.RouteConvertPrometheusDeleteNamespace(rc, "non-existent") + require.Equal(t, http.StatusNotFound, response.Status()) + }) + + t.Run("valid request should delete rules", func(t *testing.T) { + initNamespace := func(promDefinition string, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *fakes.RuleStore, *folder.Folder, *models.AlertRule) { + srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, opts...) + + // 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) + + rule := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition(promDefinition)). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + + return srv, ruleStore, fldr, rule + } + + t.Run("valid request should delete rules", func(t *testing.T) { + srv, ruleStore, fldr, rule := initNamespace("prometheus definition") + + // Create another rule group in a different namespace that should not be deleted + otherGroupName := "other-group" + otherRule := models.RuleGen. + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithGroupName(otherGroupName)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition("other prometheus definition")). + GenerateRef() + ruleStore.PutRule(context.Background(), otherRule) + + rc := createRequestCtx() + + response := srv.RouteConvertPrometheusDeleteNamespace(rc, fldr.Title) + require.Equal(t, http.StatusAccepted, response.Status()) + + // Verify the rule in the specified group was deleted + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.Error(t, err) + require.Nil(t, remaining) + + // Verify the rule in the other group still exists + remainingOther, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: otherRule.UID, + OrgID: otherRule.OrgID, + }) + require.NoError(t, err) + require.NotNil(t, remainingOther) + }) + + t.Run("fails to delete rules when they are provisioned", func(t *testing.T) { + provenanceStore := fakes.NewFakeProvisioningStore() + srv, ruleStore, fldr, rule := initNamespace("", withProvenanceStore(provenanceStore)) + rc := createRequestCtx() + + // Create a provisioned rule + rule2 := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition("prometheus definition")). + GenerateRef() + ruleStore.PutRule(context.Background(), rule2) + err := provenanceStore.SetProvenance(context.Background(), rule2, 1, models.ProvenanceAPI) + require.NoError(t, err) + + response := srv.RouteConvertPrometheusDeleteNamespace(rc, fldr.Title) + require.Equal(t, http.StatusConflict, response.Status()) + + // Verify the rule is still present + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.NoError(t, err) + require.NotNil(t, remaining) + }) + }) +} + +func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) { + t.Run("for non-existent folder should return 404", func(t *testing.T) { + srv, _, _, _ := createConvertPrometheusSrv(t) + rc := createRequestCtx() + + response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, "non-existent", "test-group") + require.Equal(t, http.StatusNotFound, response.Status()) + }) + + const groupName = "test-group" + + t.Run("valid request should delete rules", func(t *testing.T) { + initGroup := func(promDefinition string, groupName string, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *fakes.RuleStore, *folder.Folder, *models.AlertRule) { + srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, opts...) + + // 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) + + rule := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithGroupName(groupName)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition(promDefinition)). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + + return srv, ruleStore, fldr, rule + } + + t.Run("valid request should delete rules", func(t *testing.T) { + srv, ruleStore, fldr, rule := initGroup("prometheus definition", groupName) + rc := createRequestCtx() + + // Create another rule in a different group that should not be deleted + otherGroupName := "other-group" + otherRule := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithGroupName(otherGroupName)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition("other prometheus definition")). + GenerateRef() + ruleStore.PutRule(context.Background(), otherRule) + + response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName) + require.Equal(t, http.StatusAccepted, response.Status()) + + // Verify the rule was deleted + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.Error(t, err) + require.Nil(t, remaining) + + // Verify the otherRule from the "other-group" is still present + otherRuleRefreshed, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: otherRule.UID, + OrgID: otherRule.OrgID, + }) + require.NoError(t, err) + require.NotNil(t, otherRuleRefreshed) + }) + + t.Run("fails to delete rules when they are provisioned", func(t *testing.T) { + provenanceStore := fakes.NewFakeProvisioningStore() + srv, ruleStore, fldr, rule := initGroup("", groupName, withProvenanceStore(provenanceStore)) + rc := createRequestCtx() + + // Create a provisioned rule + rule2 := models.RuleGen. + With(models.RuleGen.WithNamespaceUID(fldr.UID)). + With(models.RuleGen.WithOrgID(1)). + With(models.RuleGen.WithGroupName(groupName)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition("prometheus definition")). + GenerateRef() + ruleStore.PutRule(context.Background(), rule2) + err := provenanceStore.SetProvenance(context.Background(), rule2, 1, models.ProvenanceAPI) + require.NoError(t, err) + + response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName) + require.Equal(t, http.StatusConflict, response.Status()) + + // Verify the rule is still present + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.NoError(t, err) + require.NotNil(t, remaining) + }) + }) +} + +type convertPrometheusSrvOptions struct { + provenanceStore provisioning.ProvisioningStore +} + +type convertPrometheusSrvOptionsFunc func(*convertPrometheusSrvOptions) + +func withProvenanceStore(store provisioning.ProvisioningStore) convertPrometheusSrvOptionsFunc { + return func(opts *convertPrometheusSrvOptions) { + opts.provenanceStore = store + } +} + +func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, datasources.CacheService, *fakes.RuleStore, *foldertest.FakeService) { t.Helper() + options := convertPrometheusSrvOptions{ + provenanceStore: fakes.NewFakeProvisioningStore(), + } + + for _, opt := range opts { + opt(&options) + } + ruleStore := fakes.NewRuleStore(t) folder := randFolder() ruleStore.Folders[1] = append(ruleStore.Folders[1], folder) @@ -441,7 +725,7 @@ func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasource alertRuleService := provisioning.NewAlertRuleService( ruleStore, - fakes.NewFakeProvisioningStore(), + options.provenanceStore, folderService, quotas, &provisioning.NopTransactionManager{}, diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 6eb8f38a3c4..b5c7f5ab1c2 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -75,6 +75,7 @@ type AlertRuleService interface { 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 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) GetAlertRuleGroupWithFolderFullpath(ctx context.Context, u identity.Requester, folder, group string) (alerting_models.AlertRuleGroupWithFolderFullpath, error) GetAlertGroupsWithFolderFullpath(ctx context.Context, u identity.Requester, opts *provisioning.FilterOptions) ([]alerting_models.AlertRuleGroupWithFolderFullpath, error) diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index a1cedae4b73..4dce6e33015 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -125,26 +125,32 @@ func (api *API) authorize(method, path string) web.Handler { // convert/prometheus API paths case http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}", - http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}": + http.MethodGet + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}", + http.MethodGet + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}", + http.MethodGet + "/api/convert/api/prom/rules/{NamespaceTitle}": eval = ac.EvalAll( ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(dashboards.ActionFoldersRead), ) - case http.MethodGet + "/api/convert/prometheus/config/v1/rules": + case http.MethodGet + "/api/convert/prometheus/config/v1/rules", + http.MethodGet + "/api/convert/api/prom/rules": eval = ac.EvalAll( ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(dashboards.ActionFoldersRead), ) - case http.MethodPost + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}": + case http.MethodPost + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}", + http.MethodPost + "/api/convert/api/prom/rules/{NamespaceTitle}": eval = ac.EvalAll( ac.EvalPermission(ac.ActionAlertingRuleCreate), ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus), ) case http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}", - http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}": + http.MethodDelete + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}", + http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}", + http.MethodDelete + "/api/convert/api/prom/rules/{NamespaceTitle}": eval = ac.EvalAny( ac.EvalAll( ac.EvalPermission(ac.ActionAlertingRuleRead), diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index 13b0f24c36c..993a72d3ef7 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -41,7 +41,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 63) + require.Len(t, paths, 66) ac := acmock.New() api := &API{AccessControl: ac, FeatureManager: featuremgmt.WithFeatures()} diff --git a/pkg/services/ngalert/api/errors.go b/pkg/services/ngalert/api/errors.go index 0207d06cc43..a66db1e4fb2 100644 --- a/pkg/services/ngalert/api/errors.go +++ b/pkg/services/ngalert/api/errors.go @@ -3,6 +3,7 @@ package api import ( "errors" "fmt" + "net/http" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/errutil" @@ -33,7 +34,7 @@ func errorToResponse(err error) response.Response { return response.Err(err) } if errors.Is(err, datasources.ErrDataSourceNotFound) { - return ErrResp(404, err, "") + return ErrResp(http.StatusNotFound, err, "") } if errors.Is(err, errUnexpectedDatasourceType) { return ErrResp(400, err, "") @@ -41,5 +42,5 @@ func errorToResponse(err error) response.Response { if errors.Is(err, errFolderAccess) { return toNamespaceErrorResponse(err) } - return ErrResp(500, err, "") + return ErrResp(http.StatusInternalServerError, err, "") } diff --git a/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go b/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go index 3f2e0566295..2ee728172c9 100644 --- a/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go +++ b/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go @@ -19,6 +19,12 @@ import ( ) type ConvertPrometheusApi interface { + RouteConvertPrometheusCortexDeleteNamespace(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusCortexDeleteRuleGroup(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusCortexGetNamespace(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusCortexGetRuleGroup(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusCortexGetRules(*contextmodel.ReqContext) response.Response + RouteConvertPrometheusCortexPostRuleGroup(*contextmodel.ReqContext) response.Response RouteConvertPrometheusDeleteNamespace(*contextmodel.ReqContext) response.Response RouteConvertPrometheusDeleteRuleGroup(*contextmodel.ReqContext) response.Response RouteConvertPrometheusGetNamespace(*contextmodel.ReqContext) response.Response @@ -27,6 +33,36 @@ type ConvertPrometheusApi interface { RouteConvertPrometheusPostRuleGroup(*contextmodel.ReqContext) response.Response } +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexDeleteNamespace(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + return f.handleRouteConvertPrometheusCortexDeleteNamespace(ctx, namespaceTitleParam) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexDeleteRuleGroup(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + groupParam := web.Params(ctx.Req)[":Group"] + return f.handleRouteConvertPrometheusCortexDeleteRuleGroup(ctx, namespaceTitleParam, groupParam) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexGetNamespace(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + return f.handleRouteConvertPrometheusCortexGetNamespace(ctx, namespaceTitleParam) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexGetRuleGroup(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + groupParam := web.Params(ctx.Req)[":Group"] + return f.handleRouteConvertPrometheusCortexGetRuleGroup(ctx, namespaceTitleParam, groupParam) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexGetRules(ctx *contextmodel.ReqContext) response.Response { + return f.handleRouteConvertPrometheusCortexGetRules(ctx) +} +func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexPostRuleGroup(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] + return f.handleRouteConvertPrometheusCortexPostRuleGroup(ctx, namespaceTitleParam) +} func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteNamespace(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"] @@ -60,6 +96,78 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusPostRuleGroup(ctx *c func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { + group.Delete( + toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodDelete, "/api/convert/api/prom/rules/{NamespaceTitle}"), + metrics.Instrument( + http.MethodDelete, + "/api/convert/api/prom/rules/{NamespaceTitle}", + api.Hooks.Wrap(srv.RouteConvertPrometheusCortexDeleteNamespace), + m, + ), + ) + group.Delete( + toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodDelete, "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"), + metrics.Instrument( + http.MethodDelete, + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}", + api.Hooks.Wrap(srv.RouteConvertPrometheusCortexDeleteRuleGroup), + m, + ), + ) + group.Get( + toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodGet, "/api/convert/api/prom/rules/{NamespaceTitle}"), + metrics.Instrument( + http.MethodGet, + "/api/convert/api/prom/rules/{NamespaceTitle}", + api.Hooks.Wrap(srv.RouteConvertPrometheusCortexGetNamespace), + m, + ), + ) + group.Get( + toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodGet, "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}"), + metrics.Instrument( + http.MethodGet, + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}", + api.Hooks.Wrap(srv.RouteConvertPrometheusCortexGetRuleGroup), + m, + ), + ) + group.Get( + toMacaronPath("/api/convert/api/prom/rules"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodGet, "/api/convert/api/prom/rules"), + metrics.Instrument( + http.MethodGet, + "/api/convert/api/prom/rules", + api.Hooks.Wrap(srv.RouteConvertPrometheusCortexGetRules), + m, + ), + ) + group.Post( + toMacaronPath("/api/convert/api/prom/rules/{NamespaceTitle}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodPost, "/api/convert/api/prom/rules/{NamespaceTitle}"), + metrics.Instrument( + http.MethodPost, + "/api/convert/api/prom/rules/{NamespaceTitle}", + api.Hooks.Wrap(srv.RouteConvertPrometheusCortexPostRuleGroup), + m, + ), + ) group.Delete( toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"), requestmeta.SetOwner(requestmeta.TeamAlerting), diff --git a/pkg/services/ngalert/api/prometheus_conversion.go b/pkg/services/ngalert/api/prometheus_conversion.go index 354f6de2a38..d48aa5444b9 100644 --- a/pkg/services/ngalert/api/prometheus_conversion.go +++ b/pkg/services/ngalert/api/prometheus_conversion.go @@ -20,6 +20,7 @@ func NewConvertPrometheusApi(svc *ConvertPrometheusSrv) *ConvertPrometheusApiHan } } +// mimirtool func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusGetRules(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteConvertPrometheusGetRules(ctx) } @@ -54,3 +55,28 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostRuleGroup( return f.svc.RouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle, promGroup) } + +// cortextool +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetRules(ctx *contextmodel.ReqContext) response.Response { + return f.handleRouteConvertPrometheusGetRules(ctx) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexDeleteNamespace(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response { + return f.handleRouteConvertPrometheusDeleteNamespace(ctx, namespaceTitle) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexDeleteRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { + return f.handleRouteConvertPrometheusDeleteRuleGroup(ctx, namespaceTitle, group) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetNamespace(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response { + return f.handleRouteConvertPrometheusGetNamespace(ctx, namespaceTitle) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexGetRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { + return f.handleRouteConvertPrometheusGetRuleGroup(ctx, namespaceTitle, group) +} + +func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusCortexPostRuleGroup(ctx *contextmodel.ReqContext, namespaceTitle string) response.Response { + return f.handleRouteConvertPrometheusPostRuleGroup(ctx, namespaceTitle) +} diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 7fc64a8291f..7fc77c7ce19 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -4493,7 +4493,6 @@ "type": "object" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "properties": { "ForceQuery": { "type": "boolean" @@ -4529,7 +4528,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "type": "object" }, "UpdateRuleGroupResponse": { 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 9442ba9fc0f..f0202486e46 100644 --- a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go +++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go @@ -4,6 +4,7 @@ import ( "github.com/prometheus/common/model" ) +// Route for mimirtool // swagger:route GET /convert/prometheus/config/v1/rules convert_prometheus RouteConvertPrometheusGetRules // // Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace. @@ -16,6 +17,20 @@ import ( // 403: ForbiddenError // 404: NotFound +// Route for cortextool +// swagger:route GET /convert/api/prom/rules convert_prometheus RouteConvertPrometheusCortexGetRules +// +// Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace. +// +// Produces: +// - application/yaml +// +// Responses: +// 200: PrometheusNamespace +// 403: ForbiddenError +// 404: NotFound + +// Route for mimirtool // swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusGetNamespace // // Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder). @@ -28,6 +43,20 @@ import ( // 403: ForbiddenError // 404: NotFound +// Route for cortextool +// swagger:route GET /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexGetNamespace +// +// Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder). +// +// Produces: +// - application/yaml +// +// Responses: +// 200: PrometheusNamespace +// 403: ForbiddenError +// 404: NotFound + +// Route for mimirtool // swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusGetRuleGroup // // Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source. @@ -40,6 +69,20 @@ import ( // 403: ForbiddenError // 404: NotFound +// Route for cortextool +// swagger:route GET /convert/api/prom/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusCortexGetRuleGroup +// +// Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source. +// +// Produces: +// - application/yaml +// +// Responses: +// 200: PrometheusRuleGroup +// 403: ForbiddenError +// 404: NotFound + +// Route for mimirtool // swagger:route POST /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusPostRuleGroup // // Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace. @@ -59,6 +102,27 @@ import ( // Extensions: // x-raw-request: true +// Route for cortextool +// swagger:route POST /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexPostRuleGroup +// +// Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace. +// If the group already exists and was not imported from a Prometheus-compatible source initially, +// it will not be replaced and an error will be returned. +// +// Consumes: +// - application/yaml +// +// Produces: +// - application/json +// +// Responses: +// 202: ConvertPrometheusResponse +// 403: ForbiddenError +// +// Extensions: +// x-raw-request: true + +// Route for mimirtool // swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusDeleteNamespace // // Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace. @@ -70,6 +134,19 @@ import ( // 202: ConvertPrometheusResponse // 403: ForbiddenError +// Route for cortextool +// swagger:route DELETE /convert/api/prom/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusCortexDeleteNamespace +// +// Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace. +// +// Produces: +// - application/json +// +// Responses: +// 202: ConvertPrometheusResponse +// 403: ForbiddenError + +// Route for mimirtool // swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusDeleteRuleGroup // // Deletes a specific rule group if it was imported from a Prometheus-compatible source. @@ -81,7 +158,19 @@ import ( // 202: ConvertPrometheusResponse // 403: ForbiddenError -// swagger:parameters RouteConvertPrometheusPostRuleGroup +// Route for cortextool +// swagger:route DELETE /convert/api/prom/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusCortexDeleteRuleGroup +// +// Deletes a specific rule group if it was imported from a Prometheus-compatible source. +// +// Produces: +// - application/json +// +// Responses: +// 202: ConvertPrometheusResponse +// 403: ForbiddenError + +// swagger:parameters RouteConvertPrometheusPostRuleGroup RouteConvertPrometheusCortexPostRuleGroup type RouteConvertPrometheusPostRuleGroupParams struct { // in: path NamespaceTitle string @@ -119,7 +208,7 @@ type PrometheusRule struct { Record string `yaml:"record,omitempty"` } -// swagger:parameters RouteConvertPrometheusDeleteRuleGroup RouteConvertPrometheusGetRuleGroup +// swagger:parameters RouteConvertPrometheusDeleteRuleGroup RouteConvertPrometheusCortexDeleteRuleGroup RouteConvertPrometheusGetRuleGroup RouteConvertPrometheusCortexGetRuleGroup type RouteConvertPrometheusDeleteRuleGroupParams struct { // in: path NamespaceTitle string @@ -127,7 +216,7 @@ type RouteConvertPrometheusDeleteRuleGroupParams struct { Group string } -// swagger:parameters RouteConvertPrometheusDeleteNamespace RouteConvertPrometheusGetNamespace +// swagger:parameters RouteConvertPrometheusDeleteNamespace RouteConvertPrometheusCortexDeleteNamespace RouteConvertPrometheusGetNamespace RouteConvertPrometheusCortexGetNamespace type RouteConvertPrometheusDeleteNamespaceParams struct { // in: path NamespaceTitle string diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index b48c13e2d7e..ab2b7542381 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -4493,6 +4493,7 @@ "type": "object" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "properties": { "ForceQuery": { "type": "boolean" @@ -4528,7 +4529,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object" }, "UpdateRuleGroupResponse": { @@ -4770,6 +4771,7 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup", "type": "object" @@ -5056,6 +5058,7 @@ "type": "object" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence", "type": "object" @@ -6398,6 +6401,253 @@ ] } }, + "/convert/api/prom/rules": { + "get": { + "operationId": "RouteConvertPrometheusCortexGetRules", + "produces": [ + "application/yaml" + ], + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", + "tags": [ + "convert_prometheus" + ] + } + }, + "/convert/api/prom/rules/{NamespaceTitle}": { + "delete": { + "operationId": "RouteConvertPrometheusCortexDeleteNamespace", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusCortexGetNamespace", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/yaml" + ], + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", + "tags": [ + "convert_prometheus" + ] + }, + "post": { + "consumes": [ + "application/yaml" + ], + "description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.", + "operationId": "RouteConvertPrometheusCortexPostRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "x-grafana-alerting-datasource-uid", + "type": "string" + }, + { + "in": "header", + "name": "x-grafana-alerting-recording-rules-paused", + "type": "boolean" + }, + { + "in": "header", + "name": "x-grafana-alerting-alert-rules-paused", + "type": "boolean" + }, + { + "in": "body", + "name": "Body", + "schema": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", + "tags": [ + "convert_prometheus" + ], + "x-raw-request": "true" + } + }, + "/convert/api/prom/rules/{NamespaceTitle}/{Group}": { + "delete": { + "operationId": "RouteConvertPrometheusCortexDeleteRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.", + "tags": [ + "convert_prometheus" + ] + }, + "get": { + "operationId": "RouteConvertPrometheusCortexGetRuleGroup", + "parameters": [ + { + "in": "path", + "name": "NamespaceTitle", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/yaml" + ], + "responses": { + "200": { + "description": "PrometheusRuleGroup", + "schema": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", + "tags": [ + "convert_prometheus" + ] + } + }, "/convert/prometheus/config/v1/rules": { "get": { "operationId": "RouteConvertPrometheusGetRules", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 571a497cc6b..1b251c81f29 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1102,6 +1102,253 @@ } } }, + "/convert/api/prom/rules": { + "get": { + "produces": [ + "application/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", + "operationId": "RouteConvertPrometheusCortexGetRules", + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } + } + }, + "/convert/api/prom/rules/{NamespaceTitle}": { + "get": { + "produces": [ + "application/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", + "operationId": "RouteConvertPrometheusCortexGetNamespace", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "PrometheusNamespace", + "schema": { + "$ref": "#/definitions/PrometheusNamespace" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } + }, + "post": { + "description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.", + "consumes": [ + "application/yaml" + ], + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", + "operationId": "RouteConvertPrometheusCortexPostRuleGroup", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "x-grafana-alerting-datasource-uid", + "in": "header" + }, + { + "type": "boolean", + "name": "x-grafana-alerting-recording-rules-paused", + "in": "header" + }, + { + "type": "boolean", + "name": "x-grafana-alerting-alert-rules-paused", + "in": "header" + }, + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + } + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + }, + "x-raw-request": "true" + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.", + "operationId": "RouteConvertPrometheusCortexDeleteNamespace", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + } + } + }, + "/convert/api/prom/rules/{NamespaceTitle}/{Group}": { + "get": { + "produces": [ + "application/yaml" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", + "operationId": "RouteConvertPrometheusCortexGetRuleGroup", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "PrometheusRuleGroup", + "schema": { + "$ref": "#/definitions/PrometheusRuleGroup" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "convert_prometheus" + ], + "summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.", + "operationId": "RouteConvertPrometheusCortexDeleteRuleGroup", + "parameters": [ + { + "type": "string", + "name": "NamespaceTitle", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "ConvertPrometheusResponse", + "schema": { + "$ref": "#/definitions/ConvertPrometheusResponse" + } + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + } + } + } + }, "/convert/prometheus/config/v1/rules": { "get": { "produces": [ @@ -8434,8 +8681,9 @@ } }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "type": "object", - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "properties": { "ForceQuery": { "type": "boolean" @@ -8711,6 +8959,7 @@ } }, "alertGroups": { + "description": "AlertGroups alert groups", "type": "array", "items": { "type": "object", @@ -8997,6 +9246,7 @@ } }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "type": "object", diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 683ce31bb1a..f5e968fda0b 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -501,7 +501,7 @@ func queryDataResponseToExecutionResults(c models.Condition, execResp *backend.Q if frame.Fields[0].Len() == 1 { v = frame.At(0, 0).(*float64) // type checked above } - captureFn(frame.RefID, frame.Fields[0].Labels, v) + captureFn(refID, frame.Fields[0].Labels, v) } if refID == c.Condition { diff --git a/pkg/services/ngalert/eval/eval_test.go b/pkg/services/ngalert/eval/eval_test.go index 6710737891b..bde445c732c 100644 --- a/pkg/services/ngalert/eval/eval_test.go +++ b/pkg/services/ngalert/eval/eval_test.go @@ -739,243 +739,322 @@ func TestEvaluate(t *testing.T) { resp backend.QueryDataResponse expected Results error string - }{{ - name: "is no data with no frames", - cond: models.Condition{ - Data: []models.AlertQuery{{ - RefID: "A", - DatasourceUID: "test", - }, { - RefID: "B", - DatasourceUID: expr.DatasourceUID, - }, { - RefID: "C", - DatasourceUID: expr.OldDatasourceUID, - }, { - RefID: "D", - DatasourceUID: expr.MLDatasourceUID, + }{ + { + name: "is no data with no frames", + cond: models.Condition{ + Data: []models.AlertQuery{{ + RefID: "A", + DatasourceUID: "test", + }, { + RefID: "B", + DatasourceUID: expr.DatasourceUID, + }, { + RefID: "C", + DatasourceUID: expr.OldDatasourceUID, + }, { + RefID: "D", + DatasourceUID: expr.MLDatasourceUID, + }}, + }, + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": {Frames: nil}, + "B": {Frames: []*data.Frame{{Fields: nil}}}, + "C": {Frames: nil}, + "D": {Frames: []*data.Frame{{Fields: nil}}}, + }, + }, + expected: Results{{ + State: NoData, + Instance: data.Labels{ + "datasource_uid": "test", + "ref_id": "A", + }, }}, }, - resp: backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": {Frames: nil}, - "B": {Frames: []*data.Frame{{Fields: nil}}}, - "C": {Frames: nil}, - "D": {Frames: []*data.Frame{{Fields: nil}}}, + { + name: "is no data for one frame with no fields", + cond: models.Condition{ + Data: []models.AlertQuery{{ + RefID: "A", + DatasourceUID: "test", + }}, }, - }, - expected: Results{{ - State: NoData, - Instance: data.Labels{ - "datasource_uid": "test", - "ref_id": "A", + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": {Frames: []*data.Frame{{Fields: nil}}}, + }, }, - }}, - }, { - name: "is no data for one frame with no fields", - cond: models.Condition{ - Data: []models.AlertQuery{{ - RefID: "A", - DatasourceUID: "test", + expected: Results{{ + State: NoData, + Instance: data.Labels{ + "datasource_uid": "test", + "ref_id": "A", + }, }}, }, - resp: backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": {Frames: []*data.Frame{{Fields: nil}}}, + { + name: "results contains captured values for exact label matches", + cond: models.Condition{ + Condition: "B", }, + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": { + Frames: []*data.Frame{{ + RefID: "A", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(10.0)}, + ), + }, + }}, + }, + "B": { + Frames: []*data.Frame{{ + RefID: "B", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(1.0)}, + ), + }, + }}, + }, + }, + }, + expected: Results{{ + State: Alerting, + Instance: data.Labels{ + "foo": "bar", + }, + Values: map[string]NumberValueCapture{ + "A": { + Var: "A", + Labels: data.Labels{"foo": "bar"}, + Value: util.Pointer(10.0), + }, + "B": { + Var: "B", + Labels: data.Labels{"foo": "bar"}, + Value: util.Pointer(1.0), + }, + }, + EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={foo=bar} value=1 ]", + }}, }, - expected: Results{{ - State: NoData, - Instance: data.Labels{ - "datasource_uid": "test", - "ref_id": "A", + { + name: "results contains captured values for subset of labels", + cond: models.Condition{ + Condition: "B", }, - }}, - }, { - name: "results contains captured values for exact label matches", - cond: models.Condition{ - Condition: "B", + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": { + Frames: []*data.Frame{{ + RefID: "A", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(10.0)}, + ), + }, + }}, + }, + "B": { + Frames: []*data.Frame{{ + RefID: "B", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar", "bar": "baz"}, + []*float64{util.Pointer(1.0)}, + ), + }, + }}, + }, + }, + }, + expected: Results{{ + State: Alerting, + Instance: data.Labels{ + "foo": "bar", + "bar": "baz", + }, + Values: map[string]NumberValueCapture{ + "A": { + Var: "A", + Labels: data.Labels{"foo": "bar"}, + Value: util.Pointer(10.0), + }, + "B": { + Var: "B", + Labels: data.Labels{"foo": "bar", "bar": "baz"}, + Value: util.Pointer(1.0), + }, + }, + EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={bar=baz, foo=bar} value=1 ]", + }}, }, - resp: backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": { - Frames: []*data.Frame{{ - RefID: "A", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar"}, - []*float64{util.Pointer(10.0)}, - ), - }, - }}, - }, - "B": { - Frames: []*data.Frame{{ - RefID: "B", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar"}, - []*float64{util.Pointer(1.0)}, - ), - }, - }}, + { + name: "results contains error if condition frame has error", + cond: models.Condition{ + Condition: "B", + }, + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": { + Frames: []*data.Frame{{ + RefID: "A", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(10.0)}, + ), + }, + }}, + }, + "B": { + Frames: []*data.Frame{{ + RefID: "B", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar", "bar": "baz"}, + []*float64{util.Pointer(1.0)}, + ), + }, + }}, + Error: errors.New("some frame error"), + }, }, }, + expected: Results{{ + State: Error, + Error: errors.New("some frame error"), + EvaluationString: "", + }}, }, - expected: Results{{ - State: Alerting, - Instance: data.Labels{ - "foo": "bar", + { + name: "results contain underlying error if condition frame has error that depends on another node", + cond: models.Condition{ + Condition: "B", }, - Values: map[string]NumberValueCapture{ - "A": { - Var: "A", - Labels: data.Labels{"foo": "bar"}, - Value: util.Pointer(10.0), - }, - "B": { - Var: "B", - Labels: data.Labels{"foo": "bar"}, - Value: util.Pointer(1.0), + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": { + Frames: []*data.Frame{{ + RefID: "A", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(10.0)}, + ), + }, + }}, + Error: errors.New("another error depends on me"), + }, + "B": { + Frames: []*data.Frame{{ + RefID: "B", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar", "bar": "baz"}, + []*float64{util.Pointer(1.0)}, + ), + }, + }}, + Error: expr.MakeDependencyError("B", "A"), + }, }, }, - EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={foo=bar} value=1 ]", - }}, - }, { - name: "results contains captured values for subset of labels", - cond: models.Condition{ - Condition: "B", + expected: Results{{ + State: Error, + Error: errors.New("another error depends on me"), + EvaluationString: "", + }}, }, - resp: backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": { - Frames: []*data.Frame{{ - RefID: "A", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar"}, - []*float64{util.Pointer(10.0)}, - ), - }, - }}, - }, - "B": { - Frames: []*data.Frame{{ - RefID: "B", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar", "bar": "baz"}, - []*float64{util.Pointer(1.0)}, - ), - }, - }}, + { + name: "result values for all refIDs when a math no-op expression is used and two results share the same frame pointer", + cond: models.Condition{ + Condition: "C", + }, + resp: backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": { + Frames: []*data.Frame{{ + RefID: "A", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(10.0)}, + ), + }, + }}, + }, + "B": { + // in a math no-op expression the frame for B is the same as A + // e.g. A = some_query, B = `${A}` + Frames: []*data.Frame{{ + RefID: "A", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(10.0)}, + ), + }, + }}, + }, + "C": { + Frames: []*data.Frame{{ + RefID: "C", + Fields: []*data.Field{ + data.NewField( + "Value", + data.Labels{"foo": "bar"}, + []*float64{util.Pointer(1.0)}, + ), + }, + }}, + }, }, }, + expected: Results{{ + State: Alerting, + Instance: data.Labels{ + "foo": "bar", + }, + Values: map[string]NumberValueCapture{ + "A": { + Var: "A", + Labels: data.Labels{"foo": "bar"}, + Value: util.Pointer(10.0), + }, + "B": { + Var: "B", + Labels: data.Labels{"foo": "bar"}, + Value: util.Pointer(10.0), + }, + "C": { + Var: "C", + Labels: data.Labels{"foo": "bar"}, + Value: util.Pointer(1.0), + }, + }, + EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={foo=bar} value=10 ], [ var='C' labels={foo=bar} value=1 ]", + }}, }, - expected: Results{{ - State: Alerting, - Instance: data.Labels{ - "foo": "bar", - "bar": "baz", - }, - Values: map[string]NumberValueCapture{ - "A": { - Var: "A", - Labels: data.Labels{"foo": "bar"}, - Value: util.Pointer(10.0), - }, - "B": { - Var: "B", - Labels: data.Labels{"foo": "bar", "bar": "baz"}, - Value: util.Pointer(1.0), - }, - }, - EvaluationString: "[ var='A' labels={foo=bar} value=10 ], [ var='B' labels={bar=baz, foo=bar} value=1 ]", - }}, - }, { - name: "results contains error if condition frame has error", - cond: models.Condition{ - Condition: "B", - }, - resp: backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": { - Frames: []*data.Frame{{ - RefID: "A", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar"}, - []*float64{util.Pointer(10.0)}, - ), - }, - }}, - }, - "B": { - Frames: []*data.Frame{{ - RefID: "B", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar", "bar": "baz"}, - []*float64{util.Pointer(1.0)}, - ), - }, - }}, - Error: errors.New("some frame error"), - }, - }, - }, - expected: Results{{ - State: Error, - Error: errors.New("some frame error"), - EvaluationString: "", - }}, - }, { - name: "results contain underlying error if condition frame has error that depends on another node", - cond: models.Condition{ - Condition: "B", - }, - resp: backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": { - Frames: []*data.Frame{{ - RefID: "A", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar"}, - []*float64{util.Pointer(10.0)}, - ), - }, - }}, - Error: errors.New("another error depends on me"), - }, - "B": { - Frames: []*data.Frame{{ - RefID: "B", - Fields: []*data.Field{ - data.NewField( - "Value", - data.Labels{"foo": "bar", "bar": "baz"}, - []*float64{util.Pointer(1.0)}, - ), - }, - }}, - Error: expr.MakeDependencyError("B", "A"), - }, - }, - }, - expected: Results{{ - State: Error, - Error: errors.New("another error depends on me"), - EvaluationString: "", - }}, - }} + } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index 7e6f8387273..17b95ef314d 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/dashboards" @@ -29,6 +30,11 @@ type ruleAccessControlService interface { CanWriteAllRules(ctx context.Context, user identity.Requester) (bool, error) } +var errProvenanceMismatch = errutil.NewBase(errutil.StatusConflict, "alerting.provenanceMismatch").MustTemplate( + "cannot {{ .Public.Operation }} with provided provenance '{{ .Public.ProvidedProvenance }}', needs '{{ .Public.StoredProvenance }}'", + errutil.WithPublic("cannot {{ .Public.Operation }} with provided provenance '{{ .Public.ProvidedProvenance }}', needs '{{ .Public.StoredProvenance }}'"), +) + type NotificationSettingsValidatorProvider interface { Validator(ctx context.Context, orgID int64) (notifier.NotificationSettingsValidator, error) } @@ -445,27 +451,42 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, user iden } func (service *AlertRuleService) DeleteRuleGroup(ctx context.Context, user identity.Requester, namespaceUID, group string, provenance models.Provenance) error { - delta, err := store.CalculateRuleGroupDelete(ctx, service.ruleStore, models.AlertRuleGroupKey{ - OrgID: user.GetOrgID(), - NamespaceUID: namespaceUID, - RuleGroup: group, + return service.DeleteRuleGroups(ctx, user, provenance, &FilterOptions{ + NamespaceUIDs: []string{namespaceUID}, + RuleGroups: []string{group}, }) +} + +// DeleteRuleGroups deletes alert rule groups by the specified filter options. +func (service *AlertRuleService) DeleteRuleGroups(ctx context.Context, user identity.Requester, provenance models.Provenance, filterOpts *FilterOptions) error { + q := models.ListAlertRulesQuery{} + q = filterOpts.apply(q) + q.OrgID = user.GetOrgID() + + deltas, err := store.CalculateRuleGroupsDelete(ctx, service.ruleStore, user.GetOrgID(), &q) if err != nil { return err } - // check if the current user has permissions to all rules and can bypass the regular authorization validation. - can, err := service.authz.CanWriteAllRules(ctx, user) - if err != nil { - return err - } - if !can { - if err := service.authz.AuthorizeRuleGroupWrite(ctx, user, delta); err != nil { - return err + // Perform all deletions in a transaction + return service.xact.InTransaction(ctx, func(ctx context.Context) error { + for _, delta := range deltas { + can, err := service.authz.CanWriteAllRules(ctx, user) + if err != nil { + return err + } + if !can { + if err := service.authz.AuthorizeRuleGroupWrite(ctx, user, delta); err != nil { + return err + } + } + err = service.persistDelta(ctx, user, delta, provenance) + if err != nil { + return err + } } - } - - return service.persistDelta(ctx, user, delta, provenance) + return nil + }) } func (service *AlertRuleService) calcDelta(ctx context.Context, user identity.Requester, group models.AlertRuleGroup) (*store.GroupDelta, error) { @@ -526,7 +547,13 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity return err } if canUpdate := validation.CanUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate { - return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance) + return errProvenanceMismatch.Build(errutil.TemplateData{ + Public: map[string]interface{}{ + "ProvidedProvenance": provenance, + "StoredProvenance": storedProvenance, + "Operation": "delete", + }, + }) } } if err := service.deleteRules(ctx, user.GetOrgID(), delta.Delete...); err != nil { @@ -543,7 +570,13 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity return err } if canUpdate := validation.CanUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate { - return fmt.Errorf("cannot update with provided provenance '%s', needs '%s'", provenance, storedProvenance) + return errProvenanceMismatch.Build(errutil.TemplateData{ + Public: map[string]interface{}{ + "ProvidedProvenance": provenance, + "StoredProvenance": storedProvenance, + "Operation": "update", + }, + }) } updates = append(updates, models.UpdateRule{ Existing: update.Existing, @@ -689,7 +722,13 @@ func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, user ident return err } if storedProvenance != provenance && storedProvenance != models.ProvenanceNone { - return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance) + return errProvenanceMismatch.Build(errutil.TemplateData{ + Public: map[string]interface{}{ + "ProvidedProvenance": provenance, + "StoredProvenance": storedProvenance, + "Operation": "delete", + }, + }) } can, err := service.authz.CanWriteAllRules(ctx, user) diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index 3a10b26fd39..c269a92c664 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "math/rand" + "slices" "strconv" "strings" "testing" @@ -1577,7 +1578,7 @@ func TestDeleteRuleGroup(t *testing.T) { assert.Equal(t, u, user) assert.Equal(t, groupKey, change.GroupKey) assert.Contains(t, change.AffectedGroups, groupKey) - assert.EqualValues(t, rules, change.AffectedGroups[groupKey]) + assert.ElementsMatch(t, rules, change.AffectedGroups[groupKey]) assert.Empty(t, change.Update) assert.Empty(t, change.New) assert.Len(t, change.Delete, len(rules)) @@ -1597,6 +1598,228 @@ func TestDeleteRuleGroup(t *testing.T) { }) } +func TestDeleteRuleGroups(t *testing.T) { + orgID1 := rand.Int63() + orgID2 := rand.Int63() + u := &user.SignedInUser{OrgID: orgID1} + + // Create groups across different orgs and namespaces + groupKey1 := models.AlertRuleGroupKey{ + OrgID: orgID1, + NamespaceUID: "namespace1", + RuleGroup: "group1", + } + groupKey2 := models.AlertRuleGroupKey{ + OrgID: orgID1, + NamespaceUID: "namespace2", + RuleGroup: "group2", + } + groupKey3 := models.AlertRuleGroupKey{ + OrgID: orgID1, + NamespaceUID: "namespace3", + RuleGroup: "group3", + } + groupKey4 := models.AlertRuleGroupKey{ + OrgID: orgID2, // Different org + NamespaceUID: "namespace1", + RuleGroup: "group1", + } + + gen := models.RuleGen + // Create rules for each group + rules1 := gen.With(gen.WithGroupKey(groupKey1)).GenerateManyRef(2) + rules2 := gen.With(gen.WithGroupKey(groupKey2)).GenerateManyRef(3) + rules3 := gen.With(gen.WithGroupKey(groupKey3)).GenerateManyRef(2) + rules4 := gen.With(gen.WithGroupKey(groupKey4)).GenerateManyRef(2) + + org1Rules := slices.Concat(rules1, rules2, rules3) + org2Rules := rules4 + + initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) { + service, ruleStore, provenanceStore, ac := initService(t) + ruleStore.Rules = map[int64][]*models.AlertRule{ + orgID1: org1Rules, + orgID2: org2Rules, + } + // Set provenance for all rules + for _, rules := range []([]*models.AlertRule){org1Rules, org2Rules} { + for _, rule := range rules { + err := provenanceStore.SetProvenance(context.Background(), rule, rule.OrgID, models.ProvenanceAPI) + require.NoError(t, err) + } + } + return service, ruleStore, provenanceStore, ac + } + + getUIDs := func(rules []*models.AlertRule) []string { + uids := make([]string, 0, len(rules)) + for _, rule := range rules { + uids = append(uids, rule.UID) + } + return uids + } + + t.Run("when deleting specific groups", func(t *testing.T) { + filterOpts := &FilterOptions{ + NamespaceUIDs: []string{"namespace1"}, + RuleGroups: []string{"group1"}, + } + + t.Run("when user can write all rules", func(t *testing.T) { + service, ruleStore, _, ac := initServiceWithData(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.NoError(t, err) + + require.Len(t, ac.Calls, 1) + assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method) + + // Verify only rules from group1 in org1 were deleted + deletes := getDeletedRules(t, ruleStore) + require.Len(t, deletes, 1) + require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids) + }) + + t.Run("when user cannot write all rules", func(t *testing.T) { + t.Run("should not delete if not authorized", func(t *testing.T) { + service, ruleStore, _, ac := initServiceWithData(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return false, nil + } + expectedErr := errors.New("test error") + ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error { + return expectedErr + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.ErrorIs(t, err, expectedErr) + + require.Len(t, ac.Calls, 2) + assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method) + assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method) + + deletes := getDeletedRules(t, ruleStore) + require.Empty(t, deletes) + }) + + t.Run("should delete group1 when authorized", func(t *testing.T) { + service, ruleStore, _, ac := initServiceWithData(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return false, nil + } + ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error { + assert.Equal(t, u, user) + assert.Equal(t, groupKey1, change.GroupKey) + assert.ElementsMatch(t, rules1, change.AffectedGroups[groupKey1]) + assert.Empty(t, change.Update) + assert.Empty(t, change.New) + assert.Len(t, change.Delete, len(rules1)) + return nil + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.NoError(t, err) + + require.Len(t, ac.Calls, 2) + assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method) + assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method) + + deletes := getDeletedRules(t, ruleStore) + require.Len(t, deletes, 1) + require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids) + }) + }) + }) + + t.Run("when deleting multiple groups from multiple namespaces", func(t *testing.T) { + filterOpts := &FilterOptions{ + NamespaceUIDs: []string{"namespace1", "namespace2"}, + RuleGroups: []string{"group1", "group2"}, + } + + t.Run("should delete all matching groups from correct org", func(t *testing.T) { + service, ruleStore, _, ac := initServiceWithData(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.NoError(t, err) + + deletes := getDeletedRules(t, ruleStore) + require.Len(t, deletes, 2) + require.ElementsMatch( + t, + slices.Concat(getUIDs(rules1), getUIDs(rules2)), + slices.Concat(deletes[0].uids, deletes[1].uids), + ) + }) + }) + + t.Run("when filtering by imported Prometheus rules", func(t *testing.T) { + filterOpts := &FilterOptions{ + ImportedPrometheusRule: util.Pointer(true), + NamespaceUIDs: []string{"namespace1"}, + } + + t.Run("when the group is not imported", func(t *testing.T) { + filterOpts.RuleGroups = []string{groupKey1.RuleGroup} + service, _, _, ac := initServiceWithData(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound) + }) + + t.Run("when the group is imported", func(t *testing.T) { + importedGroup := models.AlertRuleGroupKey{ + OrgID: orgID1, + NamespaceUID: "namespace1", + RuleGroup: "newgroup", + } + importedRules := gen.With( + gen.WithGroupKey(importedGroup), + gen.WithPrometheusOriginalRuleDefinition("something"), + ).GenerateManyRef(2) + filterOpts.RuleGroups = []string{importedGroup.RuleGroup} + service, ruleStore, _, ac := initServiceWithData(t) + ruleStore.Rules[orgID1] = append(ruleStore.Rules[orgID1], importedRules...) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.NoError(t, err) + deletes := getDeletedRules(t, ruleStore) + require.Len(t, deletes, 1) + require.ElementsMatch(t, getUIDs(importedRules), deletes[0].uids) + }) + }) + + t.Run("with no matching rule groups", func(t *testing.T) { + filterOpts := &FilterOptions{ + NamespaceUIDs: []string{"non-existent"}, + RuleGroups: []string{"non-existent"}, + } + + service, ruleStore, _, ac := initServiceWithData(t) + ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { + return true, nil + } + + err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts) + require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound) + + deletes := getDeletedRules(t, ruleStore) + require.Empty(t, deletes) + }) +} + func TestProvisiongWithFullpath(t *testing.T) { tracer := tracing.InitializeTracerForTest() inProcBus := bus.ProvideBus(tracer) @@ -1696,6 +1919,31 @@ func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery { return result } +type deleteRuleOperation struct { + orgID int64 + uids []string +} + +func getDeletedRules(t *testing.T, ruleStore *fakes.RuleStore) []deleteRuleOperation { + t.Helper() + + queries := getDeleteQueries(ruleStore) + operations := make([]deleteRuleOperation, 0, len(queries)) + for _, q := range queries { + orgID, ok := q.Params[0].(int64) + require.True(t, ok, "orgID parameter should be int64") + + uids, ok := q.Params[1].([]string) + require.True(t, ok, "uids parameter should be []string") + + operations = append(operations, deleteRuleOperation{ + orgID: orgID, + uids: uids, + }) + } + return operations +} + func createAlertRuleService(t *testing.T, folderService folder.Service) AlertRuleService { t.Helper() sqlStore := db.InitTestDB(t) diff --git a/pkg/services/ngalert/store/deltas.go b/pkg/services/ngalert/store/deltas.go index a7b00d27adf..d59980604f1 100644 --- a/pkg/services/ngalert/store/deltas.go +++ b/pkg/services/ngalert/store/deltas.go @@ -221,15 +221,13 @@ func CalculateRuleUpdate(ctx context.Context, ruleReader RuleReader, rule *model return calculateChanges(ctx, ruleReader, rule.GetGroupKey(), existingGroupRules, newGroup) } -// CalculateRuleGroupDelete calculates GroupDelta that reflects an operation of removing entire group -func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey) (*GroupDelta, error) { - // List all rules in the group. - q := models.ListAlertRulesQuery{ - OrgID: groupKey.OrgID, - NamespaceUIDs: []string{groupKey.NamespaceUID}, - RuleGroups: []string{groupKey.RuleGroup}, +// CalculateRuleGroupsDelete calculates []*GroupDelta that reflects an operation of removing multiple groups +func CalculateRuleGroupsDelete(ctx context.Context, ruleReader RuleReader, orgID int64, query *models.ListAlertRulesQuery) ([]*GroupDelta, error) { + if query == nil { + query = &models.ListAlertRulesQuery{} } - ruleList, err := ruleReader.ListAlertRules(ctx, &q) + query.OrgID = orgID + ruleList, err := ruleReader.ListAlertRules(ctx, query) if err != nil { return nil, err } @@ -237,14 +235,40 @@ func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupK return nil, models.ErrAlertRuleGroupNotFound.Errorf("") } - delta := &GroupDelta{ - GroupKey: groupKey, - Delete: ruleList, - AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ - groupKey: ruleList, - }, + groups := models.GroupByAlertRuleGroupKey(ruleList) + deltas := make([]*GroupDelta, 0, len(groups)) + for groupKey := range groups { + delta := &GroupDelta{ + GroupKey: groupKey, + Delete: groups[groupKey], + AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ + groupKey: groups[groupKey], + }, + } + if err != nil { + return nil, err + } + deltas = append(deltas, delta) } - return delta, nil + + return deltas, nil +} + +// CalculateRuleGroupDelete calculates GroupDelta that reflects an operation of removing entire group +func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey) (*GroupDelta, error) { + q := &models.ListAlertRulesQuery{ + NamespaceUIDs: []string{groupKey.NamespaceUID}, + RuleGroups: []string{groupKey.RuleGroup}, + } + deltas, err := CalculateRuleGroupsDelete(ctx, ruleReader, groupKey.OrgID, q) + if err != nil { + return nil, err + } + if len(deltas) != 1 { + return nil, fmt.Errorf("expected to get a single group delta, got %d", len(deltas)) + } + + return deltas[0], nil } // CalculateRuleDelete calculates GroupDelta that reflects an operation of removing a rule from the group. diff --git a/pkg/services/ngalert/store/deltas_test.go b/pkg/services/ngalert/store/deltas_test.go index fde5a8e252f..fc27fbe9af2 100644 --- a/pkg/services/ngalert/store/deltas_test.go +++ b/pkg/services/ngalert/store/deltas_test.go @@ -428,6 +428,91 @@ func TestCalculateAutomaticChanges(t *testing.T) { }) } +func TestCalculateRuleGroupsDelete(t *testing.T) { + orgId := int64(rand.Int31()) + gen := models.RuleGen + + t.Run("returns ErrAlertRuleGroupNotFound when namespace has no rules", func(t *testing.T) { + fakeStore := fakes.NewRuleStore(t) + otherRules := gen.With(gen.WithOrgID(orgId), gen.WithNamespaceUID("ns-1")).GenerateManyRef(3) + fakeStore.Rules[orgId] = otherRules + + query := &models.ListAlertRulesQuery{ + NamespaceUIDs: []string{"ns-2"}, + } + deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, query) + require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound) + require.Nil(t, deltas) + }) + + t.Run("returns deltas for all affected groups in namespace", func(t *testing.T) { + fakeStore := fakes.NewRuleStore(t) + folder := randFolder() + + // Create rules in two groups in target namespace + group1Key := models.AlertRuleGroupKey{ + OrgID: orgId, + NamespaceUID: folder.UID, + RuleGroup: util.GenerateShortUID(), + } + group2Key := models.AlertRuleGroupKey{ + OrgID: orgId, + NamespaceUID: folder.UID, + RuleGroup: util.GenerateShortUID(), + } + + group1Rules := gen.With(gen.WithGroupKey(group1Key)).GenerateManyRef(3) + group2Rules := gen.With(gen.WithGroupKey(group2Key)).GenerateManyRef(2) + allNamespaceRules := append(group1Rules, group2Rules...) + + // Create rules in different namespace + otherRules := gen.With(gen.WithOrgID(orgId), gen.WithNamespaceUIDNotIn(folder.UID)).GenerateManyRef(3) + + fakeStore.Rules[orgId] = append(allNamespaceRules, otherRules...) + + query := &models.ListAlertRulesQuery{ + NamespaceUIDs: []string{folder.UID}, + } + + deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, query) + require.NoError(t, err) + + require.Len(t, deltas, 2, "expected deltas for two groups") + + // Verify each group's delta + for _, delta := range deltas { + require.True(t, delta.GroupKey == group1Key || delta.GroupKey == group2Key) + require.Empty(t, delta.Update) + require.Empty(t, delta.New) + + require.Contains(t, delta.AffectedGroups, delta.GroupKey) + if delta.GroupKey == group1Key { + require.ElementsMatch(t, group1Rules, delta.Delete) + require.ElementsMatch(t, group1Rules, delta.AffectedGroups[delta.GroupKey]) + } else { + require.ElementsMatch(t, group2Rules, delta.Delete) + require.ElementsMatch(t, group2Rules, delta.AffectedGroups[delta.GroupKey]) + } + } + }) + + t.Run("fails if store returns error", func(t *testing.T) { + fakeStore := fakes.NewRuleStore(t) + expectedErr := errors.New("store error") + fakeStore.Hook = func(cmd any) error { + switch cmd.(type) { + case models.ListAlertRulesQuery: + return expectedErr + } + return nil + } + + deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, nil) + require.ErrorIs(t, err, expectedErr) + require.Nil(t, deltas) + }) +} + func TestCalculateRuleGroupDelete(t *testing.T) { gen := models.RuleGen fakeStore := fakes.NewRuleStore(t) @@ -449,13 +534,13 @@ func TestCalculateRuleGroupDelete(t *testing.T) { require.NoError(t, err) assert.Equal(t, groupKey, delta.GroupKey) - assert.EqualValues(t, groupRules, delta.Delete) + assert.ElementsMatch(t, groupRules, delta.Delete) assert.Empty(t, delta.Update) assert.Empty(t, delta.New) assert.Len(t, delta.AffectedGroups, 1) - assert.Equal(t, models.RulesGroup(groupRules), delta.AffectedGroups[delta.GroupKey]) + assert.ElementsMatch(t, groupRules, delta.AffectedGroups[delta.GroupKey]) }) } diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index 2fa4714b19d..728ae6bd76f 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -214,6 +214,15 @@ func (f *RuleStore) ListAlertRules(_ context.Context, q *models.ListAlertRulesQu if len(q.RuleUIDs) > 0 && !slices.Contains(q.RuleUIDs, r.UID) { continue } + if q.ImportedPrometheusRule != nil { + hasOriginalRuleDefinition := r.PrometheusRuleDefinition() != "" + if *q.ImportedPrometheusRule && !hasOriginalRuleDefinition { + continue + } + if !*q.ImportedPrometheusRule && hasOriginalRuleDefinition { + continue + } + } ruleList = append(ruleList, r) } @@ -308,12 +317,21 @@ func (f *RuleStore) InsertAlertRules(_ context.Context, _ *models.UserUID, q []m defer f.mtx.Unlock() f.RecordedOps = append(f.RecordedOps, q) ids := make([]models.AlertRuleKeyWithId, 0, len(q)) + rulesPerOrg := map[int64][]models.AlertRule{} for _, rule := range q { ids = append(ids, models.AlertRuleKeyWithId{ AlertRuleKey: rule.GetKey(), ID: rand.Int63(), }) + rulesPerOrg[rule.OrgID] = append(rulesPerOrg[rule.OrgID], rule) } + + for orgID, rules := range rulesPerOrg { + for _, rule := range rules { + f.Rules[orgID] = append(f.Rules[orgID], &rule) + } + } + if err := f.Hook(q); err != nil { return ids, err } diff --git a/pkg/setting/setting_anonymous.go b/pkg/setting/setting_anonymous.go index 057b82e73a4..53e0830eb86 100644 --- a/pkg/setting/setting_anonymous.go +++ b/pkg/setting/setting_anonymous.go @@ -14,7 +14,12 @@ func (cfg *Cfg) readAnonymousSettings() { anonSettings := AnonymousSettings{} anonSettings.Enabled = anonSection.Key("enabled").MustBool(false) anonSettings.OrgName = valueAsString(anonSection, "org_name", "") + // Deprecated: + // only viewer role is supported anonSettings.OrgRole = valueAsString(anonSection, "org_role", "") + if anonSettings.OrgRole != "Viewer" { + cfg.Logger.Warn("auth.anonymous.org_role is deprecated, only viewer role is supported") + } anonSettings.HideVersion = anonSection.Key("hide_version").MustBool(false) anonSettings.DeviceLimit = anonSection.Key("device_limit").MustInt64(0) cfg.Anonymous = anonSettings diff --git a/pkg/storage/legacysql/dualwrite/mock.go b/pkg/storage/legacysql/dualwrite/mock.go index 130c5c0feff..3dbf71a126a 100644 --- a/pkg/storage/legacysql/dualwrite/mock.go +++ b/pkg/storage/legacysql/dualwrite/mock.go @@ -26,7 +26,7 @@ type mockService struct { } // NewStorage implements Service. -func (m *mockService) NewStorage(gr schema.GroupResource, legacy rest.LegacyStorage, storage rest.Storage) (rest.Storage, error) { +func (m *mockService) NewStorage(gr schema.GroupResource, legacy rest.Storage, storage rest.Storage) (rest.Storage, error) { return nil, fmt.Errorf("not implemented") } diff --git a/pkg/storage/legacysql/dualwrite/runtime.go b/pkg/storage/legacysql/dualwrite/runtime.go index 5fe705c3409..33896e669e2 100644 --- a/pkg/storage/legacysql/dualwrite/runtime.go +++ b/pkg/storage/legacysql/dualwrite/runtime.go @@ -15,7 +15,7 @@ import ( ) func (m *service) NewStorage(gr schema.GroupResource, - legacy grafanarest.LegacyStorage, + legacy grafanarest.Storage, storage grafanarest.Storage, ) (grafanarest.Storage, error) { status, err := m.Status(context.Background(), gr) @@ -53,7 +53,7 @@ func (m *service) NewStorage(gr schema.GroupResource, // When a resource is marked as "migrating", all write requests will be 503 unavailable type runtimeDualWriter struct { service Service - legacy grafanarest.LegacyStorage + legacy grafanarest.Storage unified grafanarest.Storage dualwrite grafanarest.Storage gr schema.GroupResource diff --git a/pkg/storage/legacysql/dualwrite/runtime_test.go b/pkg/storage/legacysql/dualwrite/runtime_test.go index c97139948f2..9c148eaf07f 100644 --- a/pkg/storage/legacysql/dualwrite/runtime_test.go +++ b/pkg/storage/legacysql/dualwrite/runtime_test.go @@ -76,10 +76,10 @@ func TestManagedMode3_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (rest.LegacyStorage)(nil) + l := (rest.Storage)(nil) s := (rest.Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -148,10 +148,10 @@ func TestManagedMode3_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (rest.LegacyStorage)(nil) + l := (rest.Storage)(nil) s := (rest.Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -249,10 +249,10 @@ func TestManagedMode3_CreateWhileMigrating(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (rest.LegacyStorage)(nil) + l := (rest.Storage)(nil) s := (rest.Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { diff --git a/pkg/storage/legacysql/dualwrite/static.go b/pkg/storage/legacysql/dualwrite/static.go index 64e8c14a85a..d74e2a5b914 100644 --- a/pkg/storage/legacysql/dualwrite/static.go +++ b/pkg/storage/legacysql/dualwrite/static.go @@ -14,7 +14,7 @@ type staticService struct { cfg *setting.Cfg } -func (m *staticService) NewStorage(gr schema.GroupResource, legacy rest.LegacyStorage, storage rest.Storage) (rest.Storage, error) { +func (m *staticService) NewStorage(gr schema.GroupResource, legacy rest.Storage, storage rest.Storage) (rest.Storage, error) { return nil, fmt.Errorf("not implemented") } diff --git a/pkg/storage/legacysql/dualwrite/storage_mocks_test.go b/pkg/storage/legacysql/dualwrite/storage_mocks_test.go index 62ce9be2344..b3905d1c3d3 100644 --- a/pkg/storage/legacysql/dualwrite/storage_mocks_test.go +++ b/pkg/storage/legacysql/dualwrite/storage_mocks_test.go @@ -13,102 +13,11 @@ import ( grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" ) -type legacyStoreMock struct { - *mock.Mock - grafanarest.LegacyStorage -} - type storageMock struct { *mock.Mock grafanarest.Storage } -func (m legacyStoreMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, name, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, obj, createValidation, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) NewList() runtime.Object { - return nil -} - -func (m legacyStoreMock) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - select { - case <-ctx.Done(): - return nil, false, errors.New("context canceled") - default: - } - args := m.Called(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) - if err := args.Get(2); err != nil { - return nil, false, err.(error) - } - return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) -} - -func (m legacyStoreMock) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - select { - case <-ctx.Done(): - return nil, false, errors.New("context canceled") - default: - } - - args := m.Called(ctx, name, deleteValidation, options) - if err := args.Get(2); err != nil { - return nil, false, err.(error) - } - return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) -} - -func (m legacyStoreMock) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - args := m.Called(ctx, deleteValidation, options, listOptions) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - // Unified Store func (m storageMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { select { diff --git a/pkg/storage/legacysql/dualwrite/types.go b/pkg/storage/legacysql/dualwrite/types.go index 4b43f0d39a8..0478aa5438e 100644 --- a/pkg/storage/legacysql/dualwrite/types.go +++ b/pkg/storage/legacysql/dualwrite/types.go @@ -36,7 +36,7 @@ type Service interface { ShouldManage(gr schema.GroupResource) bool // Create a managed k8s storage instance - NewStorage(gr schema.GroupResource, legacy grafanarest.LegacyStorage, storage grafanarest.Storage) (grafanarest.Storage, error) + NewStorage(gr schema.GroupResource, legacy grafanarest.Storage, storage grafanarest.Storage) (grafanarest.Storage, error) // Check if the dual writes is reading from unified storage (mode3++) ReadFromUnified(ctx context.Context, gr schema.GroupResource) (bool, error) diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 748b5e929ff..c515663008a 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -134,7 +134,7 @@ require ( github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect @@ -183,7 +183,7 @@ require ( github.com/google/cel-go v0.22.1 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.8 // indirect @@ -199,7 +199,7 @@ require ( github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect github.com/grafana/grafana-aws-sdk v0.31.5 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect - github.com/grafana/grafana-plugin-sdk-go v0.266.0 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.267.0 // indirect github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 // indirect github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 // indirect @@ -382,7 +382,7 @@ require ( google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // 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/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 35a9215a782..11a44305649 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -334,8 +334,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -515,8 +515,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 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/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -584,8 +585,8 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 h1:9qOLpC21AmXZqZ6rUhrBWl2mVqS3CzV53pzw0BCuHt0= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:deLQ/ywLvpVGbncRGUA4UDGt8a5Ei9sivOP+x6AQ2ko= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= @@ -1552,8 +1553,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 42bb07c9e45..75b69ced369 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -15,7 +15,7 @@ require ( github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible - github.com/grafana/grafana-plugin-sdk-go v0.266.0 + github.com/grafana/grafana-plugin-sdk-go v0.267.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295 github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 @@ -28,7 +28,7 @@ require ( gocloud.dev v0.40.0 golang.org/x/sync v0.11.0 google.golang.org/grpc v1.70.0 - google.golang.org/protobuf v1.36.4 + google.golang.org/protobuf v1.36.5 k8s.io/apimachinery v0.32.1 ) @@ -82,7 +82,7 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -110,7 +110,7 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/google/wire v0.6.0 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 2bc9e773911..83d766f41aa 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -216,8 +216,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -361,8 +361,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 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/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-replayers/grpcreplay v1.3.0 h1:1Keyy0m1sIpqstQmgz307zhiJ1pV4uIlFds5weTmxbo= @@ -413,8 +414,8 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= 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.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= @@ -1021,8 +1022,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= diff --git a/pkg/tests/api/alerting/api_convert_prometheus_test.go b/pkg/tests/api/alerting/api_convert_prometheus_test.go index 674d708250f..cb248fba7f3 100644 --- a/pkg/tests/api/alerting/api_convert_prometheus_test.go +++ b/pkg/tests/api/alerting/api_convert_prometheus_test.go @@ -1,6 +1,7 @@ package alerting import ( + "encoding/json" "net/http" "testing" "time" @@ -8,6 +9,7 @@ import ( prommodel "github.com/prometheus/common/model" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/org" @@ -97,178 +99,315 @@ var ( ) func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { - testinfra.SQLiteIntegrationTest(t) + runTest := func(t *testing.T, enableLokiPaths bool) { + testinfra.SQLiteIntegrationTest(t) - // Setup Grafana and its Database - dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - DisableAnonymous: true, - AppModeProduction: true, - EnableFeatureToggles: []string{"alertingConversionAPI"}, + // Setup Grafana and its Database + dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + EnableFeatureToggles: []string{"alertingConversionAPI"}, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath) + + // Create users to make authenticated requests + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "password", + Login: "admin", + }) + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") + apiClient.prometheusConversionUseLokiPaths = enableLokiPaths + + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + Password: "password", + Login: "viewer", + }) + viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "password") + + namespace1 := "test-namespace-1" + namespace2 := "test-namespace-2" + + ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) + + t.Run("create rule groups and get them back", func(t *testing.T) { + _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + + // create a third group in a different namespace + _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + + // And a non-provisioned rule in another namespace + namespace3UID := util.GenerateShortUID() + apiClient.CreateFolder(t, namespace3UID, "folder") + createRule(t, apiClient, namespace3UID) + + // Now get the first group + group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup1.Name) + require.Equal(t, promGroup1, group1) + + // Get namespace1 + ns1 := apiClient.ConvertPrometheusGetNamespaceRules(t, namespace1) + expectedNs1 := map[string][]apimodels.PrometheusRuleGroup{ + namespace1: {promGroup1, promGroup2}, + } + require.Equal(t, expectedNs1, ns1) + + // Get all namespaces + namespaces := apiClient.ConvertPrometheusGetAllRules(t) + expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{ + namespace1: {promGroup1, promGroup2}, + namespace2: {promGroup3}, + } + require.Equal(t, expectedNamespaces, namespaces) + }) + + t.Run("without permissions to create folders cannot create rule groups either", func(t *testing.T) { + _, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) + requireStatusCode(t, http.StatusForbidden, status, raw) + }) + + t.Run("delete one rule group", func(t *testing.T) { + _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + + apiClient.ConvertPrometheusDeleteRuleGroup(t, namespace1, promGroup1.Name) + + // Check that the promGroup2 and promGroup3 are still there + namespaces := apiClient.ConvertPrometheusGetAllRules(t) + expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{ + namespace1: {promGroup2}, + namespace2: {promGroup3}, + } + require.Equal(t, expectedNamespaces, namespaces) + + // Delete the second namespace + apiClient.ConvertPrometheusDeleteNamespace(t, namespace2) + + // Check that only the first namespace is left + namespaces = apiClient.ConvertPrometheusGetAllRules(t) + expectedNamespaces = map[string][]apimodels.PrometheusRuleGroup{ + namespace1: {promGroup2}, + } + require.Equal(t, expectedNamespaces, namespaces) + }) + } + + t.Run("with the mimirtool paths", func(t *testing.T) { + runTest(t, false) }) - grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - - // Create users to make authenticated requests - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleAdmin), - Password: "password", - Login: "admin", + t.Run("with the cortextool Loki paths", func(t *testing.T) { + runTest(t, true) }) - apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") +} - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleViewer), - Password: "password", - Login: "viewer", - }) - viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "password") +func TestIntegrationConvertPrometheusEndpoints_Conflict(t *testing.T) { + runTest := func(t *testing.T, enableLokiPaths bool) { + testinfra.SQLiteIntegrationTest(t) - namespace1 := "test-namespace-1" - namespace2 := "test-namespace-2" + // Setup Grafana and its Database + dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + EnableFeatureToggles: []string{"alertingConversionAPI"}, + }) - ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath) - t.Run("create rule groups and get them back", func(t *testing.T) { - _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) - requireStatusCode(t, http.StatusAccepted, status, body) - _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil) - requireStatusCode(t, http.StatusAccepted, status, body) + // Create users to make authenticated requests + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "password", + Login: "admin", + }) + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") + apiClient.prometheusConversionUseLokiPaths = enableLokiPaths - // create a third group in a different namespace - _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil) - requireStatusCode(t, http.StatusAccepted, status, body) + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + Password: "password", + Login: "viewer", + }) - // And a non-provisioned rule in another namespace - namespace3UID := util.GenerateShortUID() - apiClient.CreateFolder(t, namespace3UID, "folder") - createRule(t, apiClient, namespace3UID) + namespace1 := "test-namespace-1" - // Now get the first group - group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup1.Name) - require.Equal(t, promGroup1, group1) + ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) - // Get namespace1 - ns1 := apiClient.ConvertPrometheusGetNamespaceRules(t, namespace1) - expectedNs1 := map[string][]apimodels.PrometheusRuleGroup{ - namespace1: {promGroup1, promGroup2}, - } - require.Equal(t, expectedNs1, ns1) + t.Run("cannot overwrite a rule group with different provenance", func(t *testing.T) { + // Create a rule group using the provisioning API and then try to overwrite it + // using the Prometheus Conversion API. It should fail because the provenance + // we set for rules in these two APIs is different and we check that when updating. + provisionedRuleGroup := apimodels.AlertRuleGroup{ + Title: promGroup1.Name, + Interval: 60, + FolderUID: namespace1, + Rules: []apimodels.ProvisionedAlertRule{ + { + Title: "Rule1", + OrgID: 1, + RuleGroup: promGroup1.Name, + Condition: "A", + NoDataState: apimodels.Alerting, + ExecErrState: apimodels.AlertingErrState, + For: prommodel.Duration(time.Duration(60) * time.Second), + Data: []apimodels.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: apimodels.RelativeTimeRange{ + From: apimodels.Duration(time.Duration(5) * time.Hour), + To: apimodels.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)), + }, + }, + }, + }, + } - // Get all namespaces - namespaces := apiClient.ConvertPrometheusGetAllRules(t) - expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{ - namespace1: {promGroup1, promGroup2}, - namespace2: {promGroup3}, - } - require.Equal(t, expectedNamespaces, namespaces) + // Create the folder + apiClient.CreateFolder(t, namespace1, namespace1) + // Create rule in the root folder using another API + _, status, response := apiClient.CreateOrUpdateRuleGroupProvisioning(t, provisionedRuleGroup) + require.Equalf(t, http.StatusOK, status, response) + + // Should fail to post the group + _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) + requireStatusCode(t, http.StatusConflict, status, body) + }) + } + + t.Run("with the mimirtool paths", func(t *testing.T) { + runTest(t, false) }) - t.Run("without permissions to create folders cannot create rule groups either", func(t *testing.T) { - _, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) - requireStatusCode(t, http.StatusForbidden, status, raw) + t.Run("with the cortextool Loki paths", func(t *testing.T) { + runTest(t, true) }) } func TestIntegrationConvertPrometheusEndpoints_CreatePausedRules(t *testing.T) { - testinfra.SQLiteIntegrationTest(t) + runTest := func(t *testing.T, enableLokiPaths bool) { + testinfra.SQLiteIntegrationTest(t) - // Setup Grafana and its Database - dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - DisableAnonymous: true, - AppModeProduction: true, - EnableFeatureToggles: []string{"alertingConversionAPI"}, - }) + // Setup Grafana and its Database + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + EnableFeatureToggles: []string{"alertingConversionAPI"}, + }) - grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - // Create users to make authenticated requests - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleAdmin), - Password: "password", - Login: "admin", - }) - apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") + // Create users to make authenticated requests + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "password", + Login: "admin", + }) + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") - ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) + ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) - namespace1 := "test-namespace-1" + namespace1 := "test-namespace-1" - namespace1UID := util.GenerateShortUID() - apiClient.CreateFolder(t, namespace1UID, namespace1) + namespace1UID := util.GenerateShortUID() + apiClient.CreateFolder(t, namespace1UID, namespace1) - t.Run("when pausing header is set, rules should be paused", func(t *testing.T) { - tests := []struct { - name string - recordingPaused bool - alertPaused bool - }{ - { - name: "do not pause rules", - recordingPaused: false, - alertPaused: false, - }, - { - name: "pause recording rules", - recordingPaused: true, - alertPaused: false, - }, - { - name: "pause alert rules", - recordingPaused: false, - alertPaused: true, - }, - { - name: "pause both recording and alert rules", - recordingPaused: true, - alertPaused: true, - }, - } + t.Run("when pausing header is set, rules should be paused", func(t *testing.T) { + tests := []struct { + name string + recordingPaused bool + alertPaused bool + }{ + { + name: "do not pause rules", + recordingPaused: false, + alertPaused: false, + }, + { + name: "pause recording rules", + recordingPaused: true, + alertPaused: false, + }, + { + name: "pause alert rules", + recordingPaused: false, + alertPaused: true, + }, + { + name: "pause both recording and alert rules", + recordingPaused: true, + alertPaused: true, + }, + } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - headers := map[string]string{} - if tc.recordingPaused { - headers["X-Grafana-Alerting-Recording-Rules-Paused"] = "true" - } - if tc.alertPaused { - headers["X-Grafana-Alerting-Alert-Rules-Paused"] = "true" - } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + headers := map[string]string{} + if tc.recordingPaused { + headers["X-Grafana-Alerting-Recording-Rules-Paused"] = "true" + } + if tc.alertPaused { + headers["X-Grafana-Alerting-Alert-Rules-Paused"] = "true" + } - apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, headers) + apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, headers) - gr, _, _ := apiClient.GetRulesGroupWithStatus(t, namespace1UID, promGroup1.Name) + gr, _, _ := apiClient.GetRulesGroupWithStatus(t, namespace1UID, promGroup1.Name) - require.Len(t, gr.Rules, 3) + require.Len(t, gr.Rules, 3) - pausedRecordingRules := 0 - pausedAlertRules := 0 + pausedRecordingRules := 0 + pausedAlertRules := 0 - for _, rule := range gr.Rules { - if rule.GrafanaManagedAlert.IsPaused { - if rule.GrafanaManagedAlert.Record != nil { - pausedRecordingRules++ - } else { - pausedAlertRules++ + for _, rule := range gr.Rules { + if rule.GrafanaManagedAlert.IsPaused { + if rule.GrafanaManagedAlert.Record != nil { + pausedRecordingRules++ + } else { + pausedAlertRules++ + } } } - } - if tc.recordingPaused { - require.Equal(t, 1, pausedRecordingRules) - } else { - require.Equal(t, 0, pausedRecordingRules) - } + if tc.recordingPaused { + require.Equal(t, 1, pausedRecordingRules) + } else { + require.Equal(t, 0, pausedRecordingRules) + } - if tc.alertPaused { - require.Equal(t, 2, pausedAlertRules) - } else { - require.Equal(t, 0, pausedAlertRules) - } - }) - } + if tc.alertPaused { + require.Equal(t, 2, pausedAlertRules) + } else { + require.Equal(t, 0, pausedAlertRules) + } + }) + } + }) + } + + t.Run("with the mimirtool paths", func(t *testing.T) { + runTest(t, false) + }) + + t.Run("with the cortextool Loki paths", func(t *testing.T) { + runTest(t, true) }) } diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index 6594eaa4bb6..5e1e34f63bf 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -254,7 +254,8 @@ func convertGettableGrafanaRuleToPostable(gettable *apimodels.GettableGrafanaRul } type apiClient struct { - url string + url string + prometheusConversionUseLokiPaths bool } type LegacyApiClient struct { @@ -1121,7 +1122,13 @@ func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle, func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTitle, groupName string) apimodels.PrometheusRuleGroup { t.Helper() - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s/%s", a.url, namespaceTitle, groupName), nil) + + path := "%s/api/convert/prometheus/config/v1/rules/%s/%s" + if a.prometheusConversionUseLokiPaths { + path = "%s/api/convert/api/prom/rules/%s/%s" + } + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url, namespaceTitle, groupName), nil) require.NoError(t, err) rule, status, raw := sendRequestYAML[apimodels.PrometheusRuleGroup](t, req, http.StatusOK) requireStatusCode(t, http.StatusOK, status, raw) @@ -1130,7 +1137,13 @@ func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTit func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTitle string) map[string][]apimodels.PrometheusRuleGroup { t.Helper() - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), nil) + + path := "%s/api/convert/prometheus/config/v1/rules/%s" + if a.prometheusConversionUseLokiPaths { + path = "%s/api/convert/api/prom/rules/%s" + } + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url, namespaceTitle), nil) require.NoError(t, err) ns, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK) requireStatusCode(t, http.StatusOK, status, raw) @@ -1139,13 +1152,47 @@ func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTit func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T) map[string][]apimodels.PrometheusRuleGroup { t.Helper() - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules", a.url), nil) + + path := "%s/api/convert/prometheus/config/v1/rules" + if a.prometheusConversionUseLokiPaths { + path = "%s/api/convert/api/prom/rules" + } + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url), nil) require.NoError(t, err) result, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK) requireStatusCode(t, http.StatusOK, status, raw) return result } +func (a apiClient) ConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle, groupName string) { + t.Helper() + + path := "%s/api/convert/prometheus/config/v1/rules/%s/%s" + if a.prometheusConversionUseLokiPaths { + path = "%s/api/convert/api/prom/rules/%s/%s" + } + + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(path, a.url, namespaceTitle, groupName), nil) + require.NoError(t, err) + _, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted) + requireStatusCode(t, http.StatusAccepted, status, raw) +} + +func (a apiClient) ConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle string) { + t.Helper() + + path := "%s/api/convert/prometheus/config/v1/rules/%s" + if a.prometheusConversionUseLokiPaths { + path = "%s/api/convert/api/prom/rules/%s" + } + + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(path, a.url, namespaceTitle), nil) + require.NoError(t, err) + _, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted) + requireStatusCode(t, http.StatusAccepted, status, raw) +} + func sendRequestRaw(t *testing.T, req *http.Request) ([]byte, int, error) { t.Helper() client := &http.Client{} diff --git a/public/app/features/alerting/unified/Templates.test.tsx b/public/app/features/alerting/unified/Templates.test.tsx index 0e524270497..ac2ede75e76 100644 --- a/public/app/features/alerting/unified/Templates.test.tsx +++ b/public/app/features/alerting/unified/Templates.test.tsx @@ -5,7 +5,7 @@ import { Props } from 'react-virtualized-auto-sizer'; import { render, screen, waitFor, within } from 'test/test-utils'; import { byLabelText, byRole } from 'testing-library-selector'; -import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types'; +import { CodeEditor } from '@grafana/ui'; import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils'; @@ -14,6 +14,8 @@ import { AccessControlAction } from 'app/types'; import Templates from './Templates'; import { grantUserPermissions } from './mocks'; +type CodeEditorProps = React.ComponentProps; + jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) =>
{actions}
, })); diff --git a/public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPoint.tsx b/public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPoint.tsx index 5f64ef13e70..78e5eda0c3f 100644 --- a/public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPoint.tsx +++ b/public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPoint.tsx @@ -4,10 +4,10 @@ import { PluginExtensionLink, PluginExtensionPoints } from '@grafana/data'; import { usePluginLinks } from '@grafana/runtime'; import { Dropdown, IconButton } from '@grafana/ui'; import { ConfirmNavigationModal } from 'app/features/explore/extensions/ConfirmNavigationModal'; +// We might want to customise this in future but right now the toolbar menu from the Explore view is fine. +import { ToolbarExtensionPointMenu as AlertExtensionPointMenu } from 'app/features/explore/extensions/ToolbarExtensionPointMenu'; import { Alert, CombinedRule } from 'app/types/unified-alerting'; -import { AlertExtensionPointMenu } from './AlertInstanceExtensionPointMenu'; - interface AlertInstanceExtensionPointProps { rule?: CombinedRule; instance: Alert; diff --git a/public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPointMenu.tsx b/public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPointMenu.tsx deleted file mode 100644 index e9f270d7129..00000000000 --- a/public/app/features/alerting/unified/components/extensions/AlertInstanceExtensionPointMenu.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// We might want to customise this in future but right now the toolbar menu from the Explore view is fine. -export { ToolbarExtensionPointMenu as AlertExtensionPointMenu } from 'app/features/explore/extensions/ToolbarExtensionPointMenu'; diff --git a/public/app/features/alerting/unified/components/receivers/TemplateEditor.tsx b/public/app/features/alerting/unified/components/receivers/TemplateEditor.tsx index 6b10eae73ad..b2d003ea865 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateEditor.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateEditor.tsx @@ -4,16 +4,15 @@ * It includes auto-complete for template data and syntax highlighting */ import { IDisposable, editor } from 'monaco-editor'; -import { useEffect, useRef } from 'react'; +import { ComponentProps, useEffect, useRef } from 'react'; import { CodeEditor } from '@grafana/ui'; -import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types'; import { registerGoTemplateAutocomplete } from './editor/autocomplete'; import goTemplateLanguageDefinition, { GO_TEMPLATE_LANGUAGE_ID } from './editor/definition'; import { registerLanguage } from './editor/register'; -type TemplateEditorProps = Omit & { +type TemplateEditorProps = Omit, 'language' | 'theme'> & { autoHeight?: boolean; }; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/KeyValueMapInput.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/KeyValueMapInput.tsx index 5a1bccdd34e..b69a9dce091 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/KeyValueMapInput.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/KeyValueMapInput.tsx @@ -1,8 +1,9 @@ import { css } from '@emotion/css'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Button, Input, useStyles2 } from '@grafana/ui'; +import { Button, Input, Stack, useStyles2 } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; import { ActionIcon } from '../../../rules/ActionIcon'; @@ -15,7 +16,7 @@ interface Props { export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) => { const styles = useStyles2(getStyles); const [pairs, setPairs] = useState(recordToPairs(value)); - useEffect(() => setPairs(recordToPairs(value)), [value]); + const [currentNewPair, setCurrentNewPair] = useState<[string, string] | undefined>(undefined); const emitChange = (pairs: Array<[string, string]>) => { onChange(pairsToRecord(pairs)); @@ -30,15 +31,6 @@ export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) = } }; - const updatePair = (values: [string, string], index: number) => { - const old = pairs[index]; - const newPairs = pairs.map((pair, i) => (i === index ? values : pair)); - setPairs(newPairs); - if (values[0] || old[0]) { - emitChange(newPairs); - } - }; - return (
{!!pairs.length && ( @@ -54,22 +46,18 @@ export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) = {pairs.map(([key, value], index) => ( - updatePair([e.currentTarget.value, value], index)} - /> + - updatePair([key, e.currentTarget.value], index)} - /> + {!readOnly && ( - deleteItem(index)} /> + deleteItem(index)} + /> )} @@ -77,6 +65,44 @@ export const KeyValueMapInput = ({ value, onChange, readOnly = false }: Props) = )} + {currentNewPair && ( + + + + + + + + +
+ setCurrentNewPair([e.currentTarget.value, currentNewPair[1]])} + /> + + setCurrentNewPair([currentNewPair[0], e.currentTarget.value])} + /> + + + { + setPairs([...pairs, currentNewPair]); + setCurrentNewPair(undefined); + emitChange([...pairs, currentNewPair]); + }} + /> + setCurrentNewPair(undefined)} + /> + +
+ )} {!readOnly && ( )}
diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx index 7912649ae80..42ac031d381 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx @@ -1,7 +1,7 @@ -import { ReactNode } from 'react'; +import { ComponentProps, ReactNode } from 'react'; import { render, screen, userEvent } from 'test/test-utils'; -import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types'; +import { CodeEditor } from '@grafana/ui'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; @@ -15,6 +15,8 @@ import { AccessControlAction, NotificationChannelOption } from 'app/types'; import { TemplatesPicker, getTemplateOptions } from './TemplateSelector'; import { parseTemplates } from './utils'; +type CodeEditorProps = ComponentProps; + const alertmanagerConfigMock = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME); jest.mock('@grafana/ui', () => ({ diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx index 72093ecc490..71c049e661b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx @@ -168,7 +168,7 @@ export const getPayloadToExport = ( } else { // we have to create a new group with the updated rule return { - name: existingGroup?.name ?? '', + name: existingGroup?.name ?? formValues.group, rules: [updatedRule], }; } 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 2ecd8131b3b..20d3359a03e 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 @@ -17,10 +17,10 @@ import { Menu, MenuItem, Stack, + Text, Tooltip, useStyles2, } from '@grafana/ui'; -import { Text } from '@grafana/ui/src/components/Text/Text'; import { Trans, t } from 'app/core/internationalization'; import { isExpressionQuery } from 'app/features/expressions/guards'; import { ExpressionDatasourceUID, ExpressionQueryType, expressionTypes } from 'app/features/expressions/types'; diff --git a/public/app/features/alerting/unified/components/rules/ActionButton.tsx b/public/app/features/alerting/unified/components/rules/ActionButton.tsx index 2613be36cb1..8ccf3338f00 100644 --- a/public/app/features/alerting/unified/components/rules/ActionButton.tsx +++ b/public/app/features/alerting/unified/components/rules/ActionButton.tsx @@ -1,8 +1,7 @@ import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; -import { Button, ButtonProps } from '@grafana/ui/src/components/Button'; +import { Button, ButtonProps, useStyles2 } from '@grafana/ui'; type Props = Omit; diff --git a/public/app/features/alerting/unified/components/rules/ActionIcon.tsx b/public/app/features/alerting/unified/components/rules/ActionIcon.tsx index 8d198286ad8..5a3dbcc9870 100644 --- a/public/app/features/alerting/unified/components/rules/ActionIcon.tsx +++ b/public/app/features/alerting/unified/components/rules/ActionIcon.tsx @@ -1,11 +1,14 @@ +import { ComponentProps } from 'react'; + import { Button, IconName, LinkButton, Tooltip } from '@grafana/ui'; -import { PopoverContent, TooltipPlacement } from '@grafana/ui/src/components/Tooltip'; + +type TooltipProps = ComponentProps; interface Props { - tooltip: PopoverContent; + tooltip: TooltipProps['content']; icon: IconName; className?: string; - tooltipPlacement?: TooltipPlacement; + tooltipPlacement?: TooltipProps['placement']; to?: string; target?: string; onClick?: () => void; diff --git a/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx b/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx index cb543108054..29b714770d8 100644 --- a/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx +++ b/public/app/features/alerting/unified/rule-list/components/RuleListIcon.tsx @@ -1,13 +1,14 @@ -import { memo } from 'react'; +import { ComponentProps, memo } from 'react'; import type { RequireAtLeastOne } from 'type-fest'; import { Icon, type IconName, Text, Tooltip } from '@grafana/ui'; -import type { TextProps } from '@grafana/ui/src/components/Text/Text'; import type { RuleHealth } from 'app/types/unified-alerting'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { isErrorHealth } from '../../components/rule-viewer/RuleViewer'; +type TextProps = ComponentProps; + interface RuleListIconProps { recording?: boolean; state?: PromAlertingRuleState; diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 5b03e36fe6b..f45b7ff1dde 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -1,8 +1,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { isEmpty } from 'lodash'; -import { locationService } from '@grafana/runtime'; -import { logMeasurement } from '@grafana/runtime/src/utils/logging'; +import { locationService, logMeasurement } from '@grafana/runtime'; import { AlertManagerCortexConfig, AlertmanagerGroup, diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index fbaf5aa5260..51cd262b24a 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -18,6 +18,7 @@ import { isInCloneChain } from '../utils/clone'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardAddPane } from './DashboardAddPane'; +import { DashboardOutline } from './DashboardOutline'; import { ElementEditPane } from './ElementEditPane'; import { ElementSelection } from './ElementSelection'; import { useEditableElement } from './useEditableElement'; @@ -181,6 +182,8 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla return null; } + const { typeId } = editableElement.getEditableElementInfo(); + if (isCollapsed) { return ( <> @@ -197,7 +200,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla {openOverlay && ( - + )} @@ -225,8 +228,8 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
{tab === 'add' && } - {tab === 'configure' && } - {tab === 'outline' &&
} + {tab === 'configure' && } + {tab === 'outline' && }
); diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx index b2fc279ccde..f51540115c5 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx @@ -73,38 +73,40 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls return (
-
{ - if (evt.shiftKey) { - return; - } + +
{ + if (evt.shiftKey) { + return; + } - editPane.clearSelection(); - }} - > - -
{controls}
-
-
- {body} + editPane.clearSelection(); + }} + > + +
{controls}
+
+
+ {body} +
-
- {isEditing && ( - <> -
-
- 0} - /> -
- - )} + {isEditing && ( + <> +
+
+ 0} + /> +
+ + )} +
); } diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index 4699e8c1c3f..77255075393 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -7,14 +7,17 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { DashboardScene } from '../scene/DashboardScene'; import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector'; -import { EditableDashboardElement } from '../scene/types/EditableDashboardElement'; +import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; export class DashboardEditableElement implements EditableDashboardElement { public readonly isEditableDashboardElement = true; - public readonly typeName = 'Dashboard'; public constructor(private dashboard: DashboardScene) {} + public getEditableElementInfo(): EditableDashboardElementInfo { + return { typeId: 'dashboard', icon: 'apps', name: t('dashboard.edit-pane.elements.dashboard', 'Dashboard') }; + } + public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { const dashboard = this.dashboard; diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx new file mode 100644 index 00000000000..4f302a638fd --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx @@ -0,0 +1,159 @@ +import { css, cx } from '@emotion/css'; +import { useMemo, useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { SceneObject, VizPanel } from '@grafana/scenes'; +import { Box, Icon, IconButton, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; + +import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; +import { isInCloneChain } from '../utils/clone'; +import { getDashboardSceneFor } from '../utils/utils'; + +import { DashboardEditPane } from './DashboardEditPane'; +import { getEditableElementFor, hasEditableElement } from './shared'; + +export interface Props { + editPane: DashboardEditPane; +} + +export function DashboardOutline({ editPane }: Props) { + const dashboard = getDashboardSceneFor(editPane); + + return ( + + + + ); +} + +function DashboardOutlineNode({ sceneObject, expandable }: { sceneObject: SceneObject; expandable: boolean }) { + const [isExpanded, setIsExpanded] = useState(true); + const { key } = sceneObject.useState(); + const styles = useStyles2(getStyles); + const { isSelected, onSelect } = useElementSelection(key); + const isCloned = useMemo(() => isInCloneChain(key!), [key]); + const editableElement = useMemo(() => getEditableElementFor(sceneObject)!, [sceneObject]); + + const children = collectEditableElementChildren(sceneObject); + const elementInfo = editableElement.getEditableElementInfo(); + + return ( + <> + + {expandable && ( + setIsExpanded(!isExpanded)} + aria-label={ + isExpanded + ? t('dashboard.outline.tree.item.collapse', 'Collapse item') + : t('dashboard.outline.tree.item.expand', 'Expand item') + } + /> + )} + + + {expandable && isExpanded && ( +
+ {children.length > 0 ? ( + children.map((child) => ( + + )) + ) : ( + + (empty) + + )} +
+ )} + + ); +} + +function getStyles(theme: GrafanaTheme2) { + return { + container: css({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + marginLeft: theme.spacing(1), + paddingLeft: theme.spacing(1.5), + borderLeft: `1px solid ${theme.colors.border.medium}`, + }), + nodeButton: css({ + boxShadow: 'none', + border: 'none', + background: 'transparent', + padding: theme.spacing(0.25, 1), + borderRadius: theme.shape.radius.default, + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + overflow: 'hidden', + '&:hover': { + backgroundColor: theme.colors.action.hover, + }, + '> span': { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }), + nodeButtonSelected: css({ + color: theme.colors.primary.text, + }), + nodeButtonClone: css({ + color: theme.colors.text.secondary, + cursor: 'not-allowed', + }), + }; +} + +interface EditableElementConfig { + sceneObject: SceneObject; + expandable: boolean; +} + +function collectEditableElementChildren( + sceneObject: SceneObject, + children: EditableElementConfig[] = [] +): EditableElementConfig[] { + sceneObject.forEachChild((child) => { + if (child instanceof DashboardGridItem) { + // DashboardGridItem is a special case as it can contain repeated panels + // In this case, we want to show the repeated panels as separate items, otherwise show the body panel + if (child.state.repeatedPanels?.length) { + children.push(...child.state.repeatedPanels.map((panel) => ({ sceneObject: panel, expandable: false }))); + } else { + children.push({ sceneObject: child.state.body, expandable: false }); + } + } else if (child instanceof VizPanel) { + children.push({ sceneObject: child, expandable: false }); + } else if (hasEditableElement(child)) { + children.push({ sceneObject: child, expandable: true }); + } else { + collectEditableElementChildren(child, children); + } + }); + + return children; +} diff --git a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx index 16adfe8004a..47cd84ade4b 100644 --- a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx @@ -14,13 +14,14 @@ export interface Props { export function ElementEditPane({ element }: Props) { const categories = element.useEditPaneOptions ? element.useEditPaneOptions() : []; const styles = useStyles2(getStyles); + const elementInfo = element.getEditableElementInfo(); return ( {element.renderActions && ( diff --git a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts index cecd28020cf..2141781408d 100644 --- a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts +++ b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts @@ -1,15 +1,14 @@ import { SceneObject, SceneObjectRef, VizPanel } from '@grafana/scenes'; import { ElementSelectionContextItem } from '@grafana/ui'; -import { DashboardScene } from '../scene/DashboardScene'; import { isBulkActionElement } from '../scene/types/BulkActionElement'; import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement'; import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement'; -import { DashboardEditableElement } from './DashboardEditableElement'; import { MultiSelectedObjectsEditableElement } from './MultiSelectedObjectsEditableElement'; import { MultiSelectedVizPanelsEditableElement } from './MultiSelectedVizPanelsEditableElement'; import { VizPanelEditableElement } from './VizPanelEditableElement'; +import { getEditableElementFor } from './shared'; export class ElementSelection { private selectedObjects?: Map>; @@ -121,24 +120,7 @@ export class ElementSelection { private createSingleSelectedElement(): EditableDashboardElement | undefined { const sceneObj = this.selectedObjects?.values().next().value?.resolve(); - - if (!sceneObj) { - return undefined; - } - - if (isEditableDashboardElement(sceneObj)) { - return sceneObj; - } - - if (sceneObj instanceof VizPanel) { - return new VizPanelEditableElement(sceneObj); - } - - if (sceneObj instanceof DashboardScene) { - return new DashboardEditableElement(sceneObj); - } - - return undefined; + return getEditableElementFor(sceneObj); } private createMultiSelectedElement(): MultiSelectedEditableDashboardElement | undefined { @@ -161,6 +143,7 @@ export class ElementSelection { } const bulkActionElements = []; + for (const sceneObject of sceneObjects) { if (sceneObject instanceof VizPanel) { const editableElement = new VizPanelEditableElement(sceneObject); diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx index 3278deb3a17..a55b5f0d8c9 100644 --- a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx @@ -2,20 +2,24 @@ import { ReactNode } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { Stack, Text, Button } from '@grafana/ui'; -import { Trans } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; import { BulkActionElement } from '../scene/types/BulkActionElement'; +import { EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement'; export class MultiSelectedObjectsEditableElement implements MultiSelectedEditableDashboardElement { public readonly isMultiSelectedEditableDashboardElement = true; - public readonly typeName = 'Objects'; public readonly key: string; constructor(private _elements: BulkActionElement[]) { this.key = uuidv4(); } + public getEditableElementInfo(): EditableDashboardElementInfo { + return { name: t('dashboard.edit-pane.elements.objects', 'Objects'), typeId: 'objects', icon: 'folder' }; + } + public renderActions(): ReactNode { return ( diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx index 259ae9d91cc..afa29772adc 100644 --- a/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx @@ -3,20 +3,24 @@ import { v4 as uuidv4 } from 'uuid'; import { VizPanel } from '@grafana/scenes'; import { Button, Stack, Text } from '@grafana/ui'; -import { Trans } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; +import { EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; export class MultiSelectedVizPanelsEditableElement implements MultiSelectedEditableDashboardElement { public readonly isMultiSelectedEditableDashboardElement = true; - public readonly typeName = 'Panels'; public readonly key: string; constructor(private _panels: VizPanel[]) { this.key = uuidv4(); } + public getEditableElementInfo(): EditableDashboardElementInfo { + return { name: t('dashboard.edit-pane.elements.panels', 'Panels'), typeId: 'panels', icon: 'folder' }; + } + renderActions(): ReactNode { return ( diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx index 4d97803b8db..c20c61113fa 100644 --- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx @@ -14,15 +14,22 @@ import { } from '../panel-edit/getPanelFrameOptions'; import { BulkActionElement } from '../scene/types/BulkActionElement'; import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem'; -import { EditableDashboardElement } from '../scene/types/EditableDashboardElement'; +import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; export class VizPanelEditableElement implements EditableDashboardElement, BulkActionElement { public readonly isEditableDashboardElement = true; - public readonly typeName = 'Panel'; public constructor(private panel: VizPanel) {} + public getEditableElementInfo(): EditableDashboardElementInfo { + return { + typeId: 'panel', + icon: 'chart-line', + name: sceneGraph.interpolate(this.panel, this.panel.state.title, undefined, 'text'), + }; + } + public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { const panel = this.panel; const layoutElement = panel.parent!; diff --git a/public/app/features/dashboard-scene/edit-pane/shared.ts b/public/app/features/dashboard-scene/edit-pane/shared.ts index 59ee48dde75..72c44d5dc2b 100644 --- a/public/app/features/dashboard-scene/edit-pane/shared.ts +++ b/public/app/features/dashboard-scene/edit-pane/shared.ts @@ -1,5 +1,55 @@ import { useSessionStorage } from 'react-use'; +import { SceneGridRow, SceneObject, VizPanel } from '@grafana/scenes'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { SceneGridRowEditableElement } from '../scene/layout-default/SceneGridRowEditableElement'; +import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement'; + +import { DashboardEditableElement } from './DashboardEditableElement'; +import { VizPanelEditableElement } from './VizPanelEditableElement'; + export function useEditPaneCollapsed() { return useSessionStorage('grafana.dashboards.edit-pane.isCollapsed', false); } + +export function getEditableElementFor(sceneObj: SceneObject | undefined): EditableDashboardElement | undefined { + if (!sceneObj) { + return undefined; + } + + if (isEditableDashboardElement(sceneObj)) { + return sceneObj; + } + + if (sceneObj instanceof VizPanel) { + return new VizPanelEditableElement(sceneObj); + } + + if (sceneObj instanceof SceneGridRow) { + return new SceneGridRowEditableElement(sceneObj); + } + + if (sceneObj instanceof DashboardScene) { + return new DashboardEditableElement(sceneObj); + } + + return undefined; +} + +export function hasEditableElement(sceneObj: SceneObject | undefined): boolean { + if (!sceneObj) { + return false; + } + + if ( + isEditableDashboardElement(sceneObj) || + sceneObj instanceof VizPanel || + sceneObj instanceof SceneGridRow || + sceneObj instanceof DashboardScene + ) { + return true; + } + + return false; +} diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index b9f37706bad..63da7075646 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -151,7 +151,7 @@ abstract class DashboardScenePageStateManagerBase const queryController = sceneGraph.getQueryController(dashboard); trackDashboardSceneLoaded(dashboard, measure?.duration); - queryController?.startProfile(dashboard); + queryController?.startProfile('DashboardScene'); if (options.route !== DashboardRoutes.New) { emitDashboardViewEvent({ diff --git a/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts b/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts index 93b81e735ce..ccde822ce13 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts +++ b/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts @@ -1,6 +1,10 @@ -import { sceneGraph } from '@grafana/scenes'; +import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes'; import { ScopesFacade } from 'app/features/scopes'; +import { getDashboardSceneFor } from '../utils/utils'; + +import { convertScopesToAdHocFilters } from './convertScopesToAdHocFilters'; + export interface DashboardScopesFacadeState { reloadOnParamsChange?: boolean; uid?: string; @@ -13,7 +17,32 @@ export class DashboardScopesFacade extends ScopesFacade { if (!reloadOnParamsChange || !uid) { sceneGraph.getTimeRange(facade).onRefresh(); } + + // push filters as soon as they come + this.pushScopeFiltersToAdHocVariable(); }, }); + + this.addActivationHandler(() => { + // also try to push filters on activation, for + // when the dashboard is changed + this.pushScopeFiltersToAdHocVariable(); + }); + } + + private pushScopeFiltersToAdHocVariable() { + const dashboard = getDashboardSceneFor(this); + + const adhoc = dashboard.state.$variables?.state.variables.find((v) => v instanceof AdHocFiltersVariable); + + if (!adhoc) { + return; + } + + const filters = convertScopesToAdHocFilters(this.value); + + adhoc.setState({ + baseFilters: filters, + }); } } diff --git a/public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.test.ts b/public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.test.ts new file mode 100644 index 00000000000..c6b84a4d39f --- /dev/null +++ b/public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.test.ts @@ -0,0 +1,288 @@ +import { Scope, ScopeSpecFilter } from '@grafana/data'; +import { FilterOrigin } from '@grafana/scenes'; + +import { convertScopesToAdHocFilters } from './convertScopesToAdHocFilters'; + +describe('convertScopesToAdHocFilters', () => { + it('should return empty filters when no scopes are provided', () => { + let scopes = generateScopes([]); + + expect(scopes).toEqual([]); + expect(convertScopesToAdHocFilters(scopes)).toEqual([]); + + scopes = generateScopes([[], []]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([]); + }); + + it('should return filters formatted for adHoc from a single scope', () => { + let scopes = generateScopes([ + [ + { key: 'key1', value: 'value1', operator: 'equals' }, + { key: 'key2', value: 'value2', operator: 'not-equals' }, + { key: 'key3', value: 'value3', operator: 'regex-not-match' }, + ], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { key: 'key1', value: 'value1', operator: '=', origin: FilterOrigin.Scopes, values: ['value1'] }, + { key: 'key2', value: 'value2', operator: '!=', origin: FilterOrigin.Scopes, values: ['value2'] }, + { key: 'key3', value: 'value3', operator: '!~', origin: FilterOrigin.Scopes, values: ['value3'] }, + ]); + + scopes = generateScopes([[{ key: 'key3', value: 'value3', operator: 'regex-match' }]]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { key: 'key3', value: 'value3', operator: '=~', origin: FilterOrigin.Scopes, values: ['value3'] }, + ]); + }); + + it('should return filters formatted for adHoc from multiple scopes with single values', () => { + let scopes = generateScopes([ + [{ key: 'key1', value: 'value1', operator: 'equals' }], + [{ key: 'key2', value: 'value2', operator: 'regex-match' }], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { key: 'key1', value: 'value1', operator: '=', origin: FilterOrigin.Scopes, values: ['value1'] }, + { key: 'key2', value: 'value2', operator: '=~', origin: FilterOrigin.Scopes, values: ['value2'] }, + ]); + }); + + it('should return filters formatted for adHoc from multiple scopes with multiple values', () => { + let scopes = generateScopes([ + [ + { key: 'key1', value: 'value1', operator: 'equals' }, + { key: 'key2', value: 'value2', operator: 'not-equals' }, + ], + [ + { key: 'key3', value: 'value3', operator: 'regex-match' }, + { key: 'key4', value: 'value4', operator: 'regex-match' }, + ], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { key: 'key1', value: 'value1', operator: '=', origin: FilterOrigin.Scopes, values: ['value1'] }, + { key: 'key2', value: 'value2', operator: '!=', origin: FilterOrigin.Scopes, values: ['value2'] }, + { key: 'key3', value: 'value3', operator: '=~', origin: FilterOrigin.Scopes, values: ['value3'] }, + { key: 'key4', value: 'value4', operator: '=~', origin: FilterOrigin.Scopes, values: ['value4'] }, + ]); + }); + + it('should return formatted filters and concat values of the same key, coming from different scopes, if operator supports multi-value', () => { + let scopes = generateScopes([ + [ + { key: 'key1', value: 'value1', operator: 'equals' }, + { key: 'key2', value: 'value2', operator: 'not-equals' }, + ], + [ + { key: 'key1', value: 'value3', operator: 'equals' }, + { key: 'key2', value: 'value4', operator: 'not-equals' }, + ], + [{ key: 'key1', value: 'value5', operator: 'equals' }], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { + key: 'key1', + value: 'value1', + operator: '=|', + origin: FilterOrigin.Scopes, + values: ['value1', 'value3', 'value5'], + }, + { key: 'key2', value: 'value2', operator: '!=|', origin: FilterOrigin.Scopes, values: ['value2', 'value4'] }, + ]); + }); + + it('should ignore the rest of the duplicate filters, if they are a combination of equals and not-equals', () => { + let scopes = generateScopes([ + [{ key: 'key1', value: 'value1', operator: 'equals' }], + [{ key: 'key1', value: 'value2', operator: 'not-equals' }], + [{ key: 'key1', value: 'value3', operator: 'equals' }], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { + key: 'key1', + value: 'value1', + operator: '=|', + origin: FilterOrigin.Scopes, + values: ['value1', 'value3'], + }, + { + key: 'key1', + value: 'value2', + operator: '!=', + origin: FilterOrigin.Scopes, + values: ['value2'], + }, + ]); + }); + + it('should return formatted filters and keep only the first filter of the same key if operator is not multi-value', () => { + let scopes = generateScopes([ + [ + { key: 'key1', value: 'value1', operator: 'regex-match' }, + { key: 'key2', value: 'value2', operator: 'not-equals' }, + ], + [ + { key: 'key1', value: 'value3', operator: 'regex-match' }, + { key: 'key2', value: 'value4', operator: 'not-equals' }, + ], + [{ key: 'key1', value: 'value5', operator: 'equals' }], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { + key: 'key1', + value: 'value1', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value1'], + }, + { key: 'key2', value: 'value2', operator: '!=|', origin: FilterOrigin.Scopes, values: ['value2', 'value4'] }, + { + key: 'key1', + value: 'value3', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value3'], + }, + { + key: 'key1', + value: 'value5', + operator: '=', + origin: FilterOrigin.Scopes, + values: ['value5'], + }, + ]); + + scopes = generateScopes([ + [{ key: 'key1', value: 'value1', operator: 'regex-match' }], + [{ key: 'key1', value: 'value5', operator: 'equals' }], + [{ key: 'key1', value: 'value3', operator: 'regex-match' }], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { + key: 'key1', + value: 'value1', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value1'], + }, + { + key: 'key1', + value: 'value5', + operator: '=', + origin: FilterOrigin.Scopes, + values: ['value5'], + }, + { + key: 'key1', + value: 'value3', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value3'], + }, + ]); + }); + + it('should return formatted filters and concat values that are multi-value and drop duplicates with non multi-value operator', () => { + let scopes = generateScopes([ + [{ key: 'key1', value: 'value1', operator: 'equals' }], + [{ key: 'key1', value: 'value2', operator: 'regex-match' }], + [{ key: 'key1', value: 'value3', operator: 'equals' }], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { + key: 'key1', + value: 'value1', + operator: '=|', + origin: FilterOrigin.Scopes, + values: ['value1', 'value3'], + }, + { + key: 'key1', + value: 'value2', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value2'], + }, + ]); + + scopes = generateScopes([ + [ + { key: 'key1', value: 'value1', operator: 'equals' }, + { key: 'key2', value: 'value2', operator: 'equals' }, + ], + [ + { key: 'key1', value: 'value3', operator: 'equals' }, + { key: 'key2', value: 'value4', operator: 'equals' }, + ], + [ + { key: 'key1', value: 'value5', operator: 'regex-match' }, + { key: 'key2', value: 'value6', operator: 'equals' }, + ], + [ + { key: 'key1', value: 'value7', operator: 'equals' }, + { key: 'key2', value: 'value8', operator: 'regex-match' }, + ], + [ + { key: 'key1', value: 'value9', operator: 'equals' }, + { key: 'key2', value: 'value10', operator: 'equals' }, + ], + ]); + + expect(convertScopesToAdHocFilters(scopes)).toEqual([ + { + key: 'key1', + value: 'value1', + operator: '=|', + origin: FilterOrigin.Scopes, + values: ['value1', 'value3', 'value7', 'value9'], + }, + { + key: 'key2', + value: 'value2', + operator: '=|', + origin: FilterOrigin.Scopes, + values: ['value2', 'value4', 'value6', 'value10'], + }, + { + key: 'key1', + value: 'value5', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value5'], + }, + { + key: 'key2', + value: 'value8', + operator: '=~', + origin: FilterOrigin.Scopes, + values: ['value8'], + }, + ]); + }); +}); + +function generateScopes(filtersSpec: ScopeSpecFilter[][]) { + const scopes: Scope[] = []; + + for (let i = 0; i < filtersSpec.length; i++) { + scopes.push({ + metadata: { name: `name-${i}` }, + spec: { + title: `scope-${i}`, + type: '', + description: 'desc', + category: '', + filters: filtersSpec[i], + }, + }); + } + + return scopes; +} diff --git a/public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.ts b/public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.ts new file mode 100644 index 00000000000..1066a8f7b15 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/convertScopesToAdHocFilters.ts @@ -0,0 +1,92 @@ +import { + Scope, + ScopeSpecFilter, + isEqualityOrMultiOperator, + reverseScopeFilterOperatorMap, + scopeFilterOperatorMap, +} from '@grafana/data'; +import { AdHocFilterWithLabels, FilterOrigin } from '@grafana/scenes'; + +export function convertScopesToAdHocFilters(scopes: Scope[]): AdHocFilterWithLabels[] { + const formattedFilters: Map = new Map(); + // duplicated filters that could not be processed in any way are just appended to the list + const duplicatedFilters: AdHocFilterWithLabels[] = []; + const allFilters = scopes.flatMap((scope) => scope.spec.filters); + + for (const filter of allFilters) { + processFilter(formattedFilters, duplicatedFilters, filter); + } + + return [...formattedFilters.values(), ...duplicatedFilters]; +} + +function processFilter( + formattedFilters: Map, + duplicatedFilters: AdHocFilterWithLabels[], + filter: ScopeSpecFilter +) { + const existingFilter = formattedFilters.get(filter.key); + + if (existingFilter && canValueBeMerged(existingFilter.operator, filter.operator)) { + mergeFilterValues(existingFilter, filter); + } else if (!existingFilter) { + // Add filter to map either only if it is new. + // Otherwise it is an existing filter that cannot be converted to multi-value + // and thus will be moved to the duplicatedFilters list + formattedFilters.set(filter.key, { + key: filter.key, + operator: reverseScopeFilterOperatorMap[filter.operator], + value: filter.value, + values: filter.values ?? [filter.value], + origin: FilterOrigin.Scopes, + }); + } else { + duplicatedFilters.push({ + key: filter.key, + operator: reverseScopeFilterOperatorMap[filter.operator], + value: filter.value, + values: filter.values ?? [filter.value], + origin: FilterOrigin.Scopes, + }); + } +} + +function mergeFilterValues(adHocFilter: AdHocFilterWithLabels, filter: ScopeSpecFilter) { + const values = filter.values ?? [filter.value]; + + for (const value of values) { + if (!adHocFilter.values?.includes(value)) { + adHocFilter.values?.push(value); + } + } + + // If there's only one value, there's no need to update the + // operator to its multi-value equivalent + if (adHocFilter.values?.length === 1) { + return; + } + + // Otherwise update it to the equivalent multi-value operator + if (filter.operator === 'equals' && adHocFilter.operator === reverseScopeFilterOperatorMap['equals']) { + adHocFilter.operator = reverseScopeFilterOperatorMap['one-of']; + } else if (filter.operator === 'not-equals' && adHocFilter.operator === reverseScopeFilterOperatorMap['not-equals']) { + adHocFilter.operator = reverseScopeFilterOperatorMap['not-one-of']; + } +} + +function canValueBeMerged(adHocFilterOperator: string, filterOperator: string) { + const scopeConvertedOperator = scopeFilterOperatorMap[adHocFilterOperator]; + + if (!isEqualityOrMultiOperator(scopeConvertedOperator) || !isEqualityOrMultiOperator(filterOperator)) { + return false; + } + + if ( + (scopeConvertedOperator.includes('not') && !filterOperator.includes('not')) || + (!scopeConvertedOperator.includes('not') && filterOperator.includes('not')) + ) { + return false; + } + + return true; +} diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx index 4d8163724b5..58ff8dd2082 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx @@ -1,4 +1,5 @@ import { isEqual } from 'lodash'; +import { Unsubscribable } from 'rxjs'; import { VizPanel, @@ -49,17 +50,12 @@ export class DashboardGridItem private _prevRepeatValues?: VariableValueSingle[]; + private _gridSizeSub: Unsubscribable | undefined; + public constructor(state: DashboardGridItemState) { super(state); - this.addActivationHandler(() => this._activationHandler()); - } - - private _activationHandler() { - if (this.state.variableName) { - this._subs.add(this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState))); - this.performRepeat(); - } + this.addActivationHandler(() => this.handleVariableName()); } private _handleGridResize(newState: DashboardGridItemState, prevState: DashboardGridItemState) { @@ -197,6 +193,23 @@ export class DashboardGridItem this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true); } + public handleVariableName() { + if (this.state.variableName) { + if (!this._gridSizeSub) { + this._gridSizeSub = this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState)); + this._subs.add(this._gridSizeSub); + } + } else { + if (this._gridSizeSub) { + this._gridSizeSub.unsubscribe(); + this._subs.remove(this._gridSizeSub); + this._gridSizeSub = undefined; + } + } + + this.performRepeat(); + } + public setRepeatByVariable(variableName: string | undefined) { const stateUpdate: Partial = { variableName }; diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx index 1b9e7c5fd2c..4a80df34331 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx @@ -1,4 +1,5 @@ import { SelectableValue } from '@grafana/data'; +import { sceneGraph, SceneGridLayout } from '@grafana/scenes'; import { RadioButtonGroup, Select } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; @@ -88,14 +89,24 @@ function MaxPerRowOption({ gridItem }: OptionComponentProps) { } function RepeatByOption({ gridItem }: OptionComponentProps) { - const { variableName } = gridItem.useState(); + const { variableName, width } = gridItem.useState(); return ( gridItem.setRepeatByVariable(value)} + onChange={(value?: string) => { + if (value !== variableName) { + gridItem.setRepeatByVariable(value); + gridItem.handleVariableName(); + + if (width !== 24) { + gridItem.setState({ width: 24 }); + sceneGraph.getAncestor(gridItem, SceneGridLayout).forceRender(); + } + } + }} /> ); } diff --git a/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx b/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx new file mode 100644 index 00000000000..497b4087da9 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx @@ -0,0 +1,149 @@ +import { ReactNode, useMemo } from 'react'; + +import { selectors } from '@grafana/e2e-selectors'; +import { sceneGraph, SceneGridRow, VizPanel } from '@grafana/scenes'; +import { Alert, Button, Input, TextLink } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; +import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect'; +import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants'; +import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; + +import { getDashboardSceneFor, getLayoutManagerFor, getQueryRunnerFor } from '../../utils/utils'; +import { DashboardScene } from '../DashboardScene'; +import { BulkActionElement } from '../types/BulkActionElement'; +import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement'; + +import { DefaultGridLayoutManager } from './DefaultGridLayoutManager'; +import { RowRepeaterBehavior } from './RowRepeaterBehavior'; + +export class SceneGridRowEditableElement implements EditableDashboardElement, BulkActionElement { + public readonly isEditableDashboardElement = true; + + public constructor(private _row: SceneGridRow) {} + + public getEditableElementInfo(): EditableDashboardElementInfo { + return { + typeId: 'panel', + icon: 'line-alt', + name: sceneGraph.interpolate(this._row, this._row.state.title, undefined, 'text'), + }; + } + + public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { + const row = this._row; + + const rowOptions = useMemo(() => { + return new OptionsPaneCategoryDescriptor({ + title: t('dashboard.default-layout.row-options.title', 'Row options'), + id: 'row-options', + isOpenDefault: true, + }).addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.default-layout.row-options.form.title', 'Title'), + render: () => , + }) + ); + }, [row]); + + const rowRepeatOptions = useMemo(() => { + const dashboard = getDashboardSceneFor(row); + + return new OptionsPaneCategoryDescriptor({ + title: t('dashboard.default-layout.row-options.repeat.title', 'Repeat options'), + id: 'row-repeat-options', + isOpenDefault: true, + }).addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.default-layout.row-options.repeat.variable.title', 'Variable'), + render: () => , + }) + ); + }, [row]); + + return [rowOptions, rowRepeatOptions]; + } + + public onDelete() { + const layoutManager = getLayoutManagerFor(this._row); + + if (layoutManager instanceof DefaultGridLayoutManager) { + layoutManager.removeRow(this._row); + } + } + + public renderActions(): ReactNode { + return ( + <> + + + ); + if (pluginStatus === PluginStatus.UNINSTALL) { return ( - <> - - - - - + + {uninstallControls} + ); } @@ -162,9 +166,7 @@ export function InstallControlsButton({ {isInstalling ? 'Updating' : 'Update'} )} - + {uninstallControls} ); } diff --git a/public/app/features/plugins/admin/components/UpdateAllModal.tsx b/public/app/features/plugins/admin/components/UpdateAllModal.tsx index f12113499b9..7830b1b253e 100644 --- a/public/app/features/plugins/admin/components/UpdateAllModal.tsx +++ b/public/app/features/plugins/admin/components/UpdateAllModal.tsx @@ -5,7 +5,7 @@ import { ConfirmModal } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { useInstall, useInstallStatus } from '../state/hooks'; -import { CatalogPlugin } from '../types'; +import { CatalogPlugin, PluginStatus } from '../types'; import { UpdateModalBody } from './UpdateAllModalBody'; const PLUGINS_UPDATE_ALL_INTERACTION_EVENT_NAME = 'plugins_update_all_clicked'; @@ -100,13 +100,13 @@ export const UpdateAllModal = ({ isOpen, onDismiss, isLoading, plugins }: Props) if (config.pluginAdminExternalManageEnabled) { for (let plugin of plugins) { if (selectedPlugins?.has(plugin.id)) { - await install(plugin.id, plugin.latestVersion, true); + await install(plugin.id, plugin.latestVersion, PluginStatus.UPDATE); } } } else { plugins.forEach((plugin) => { if (selectedPlugins?.has(plugin.id)) { - install(plugin.id, plugin.latestVersion, true); + install(plugin.id, plugin.latestVersion, PluginStatus.UPDATE); } }); } diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx index 38511a2dfea..16ec1d3ca70 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx @@ -177,6 +177,46 @@ describe('VersionInstallButton', () => { ); expect(screen.getByText('Downgrade')).not.toBeVisible(); }); + + it('should show the installation button if invalid semver version is provided', () => { + const version: Version = { + version: '1.0.a', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.1'; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Install')).toBeInTheDocument(); + }); + + it('should show the installation button if invalid semver installed version is provided', () => { + const version: Version = { + version: '1.0.0', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.a'; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Install')).toBeInTheDocument(); + }); }); function renderWithStore(component: JSX.Element) { diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.tsx index 1da8bfe01f7..7792ffc06a8 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { useEffect, useState } from 'react'; -import { gt } from 'semver'; +import { gt, valid } from 'semver'; import { GrafanaTheme2 } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; @@ -9,11 +9,10 @@ import { t } from 'app/core/internationalization'; import { isPreinstalledPlugin } from '../helpers'; import { useInstall } from '../state/hooks'; -import { Version } from '../types'; +import { PluginStatus, Version } from '../types'; const PLUGINS_VERSION_PAGE_UPGRADE_INTERACTION_EVENT_NAME = 'plugins_upgrade_clicked'; const PLUGINS_VERSION_PAGE_CHANGE_INTERACTION_EVENT_NAME = 'plugins_downgrade_clicked'; - interface Props { pluginId: string; version: Version; @@ -38,7 +37,7 @@ export const VersionInstallButton = ({ const [isModalOpen, setIsModalOpen] = useState(false); const styles = useStyles2(getStyles); - const isDowngrade = installedVersion && gt(installedVersion, version.version); + const installState = getInstallState(installedVersion, version.version); useEffect(() => { if (installedVersion === version.version) { @@ -61,7 +60,7 @@ export const VersionInstallButton = ({ schema_version: '1.0.0', }; - if (!installedVersion || gt(version.version, installedVersion)) { + if (installState === PluginStatus.UPDATE) { reportInteraction(PLUGINS_VERSION_PAGE_UPGRADE_INTERACTION_EVENT_NAME, trackProps); } else { reportInteraction(PLUGINS_VERSION_PAGE_CHANGE_INTERACTION_EVENT_NAME, { @@ -70,13 +69,13 @@ export const VersionInstallButton = ({ }); } - install(pluginId, version.version, true); + install(pluginId, version.version, installState); setIsInstalling(true); onConfirmInstallation(); }; const onInstallClick = () => { - if (isDowngrade) { + if (installState === PluginStatus.DOWNGRADE) { setIsModalOpen(true); } else { performInstallation(); @@ -91,24 +90,9 @@ export const VersionInstallButton = ({ setIsModalOpen(false); }; - let label = 'Downgrade'; - let hidden = false; const isPreinstalled = isPreinstalledPlugin(pluginId); - if (!installedVersion) { - label = 'Install'; - } else if (gt(version.version, installedVersion)) { - label = 'Upgrade'; - if (isPreinstalled.withVersion) { - // Hide button if the plugin is preinstalled with a specific version - hidden = true; - } - } else { - if (isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate)) { - // Hide the downgrade button if the plugin is preinstalled since it will be auto-updated - hidden = true; - } - } + const hidden = getButtonHiddenState(installState, isPreinstalled); return ( <> @@ -124,7 +108,8 @@ export const VersionInstallButton = ({ tooltip={tooltip} tooltipPlacement="bottom-start" > - {label} {isInstalling ? : getIcon(label)} + {getLabel(installState)}{' '} + {isInstalling ? : getIcon(installState)} ; } - if (label === 'Upgrade') { + if (installState === PluginStatus.UPDATE) { return ; } return ''; } +function getInstallState(installedVersion?: string, version?: string): PluginStatus { + if (!installedVersion || !version || !valid(installedVersion) || !valid(version)) { + return PluginStatus.INSTALL; + } + return gt(installedVersion, version) ? PluginStatus.DOWNGRADE : PluginStatus.UPDATE; +} + +function getButtonHiddenState(installState: PluginStatus, isPreinstalled: { found: boolean; withVersion: boolean }) { + // Default state for initial install + if (installState === PluginStatus.INSTALL) { + return false; + } + + // Handle downgrade case + if (installState === PluginStatus.DOWNGRADE) { + return isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate); + } + + // Handle upgrade case + return isPreinstalled.withVersion; +} + const getStyles = (theme: GrafanaTheme2) => ({ spinner: css({ marginLeft: theme.spacing(1), diff --git a/public/app/features/plugins/admin/state/actions.ts b/public/app/features/plugins/admin/state/actions.ts index eab93de18fa..e08e7241c42 100644 --- a/public/app/features/plugins/admin/state/actions.ts +++ b/public/app/features/plugins/admin/state/actions.ts @@ -19,7 +19,7 @@ import { } from '../api'; import { STATE_PREFIX } from '../constants'; import { mapLocalToCatalog, mergeLocalsAndRemotes, updatePanels } from '../helpers'; -import { CatalogPlugin, RemotePlugin, LocalPlugin, InstancePlugin, ProvisionedPlugin } from '../types'; +import { CatalogPlugin, RemotePlugin, LocalPlugin, InstancePlugin, ProvisionedPlugin, PluginStatus } from '../types'; // Fetches export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, thunkApi) => { @@ -188,17 +188,23 @@ export const install = createAsyncThunk< { id: string; version?: string; - isUpdating?: boolean; + installType?: PluginStatus; } ->(`${STATE_PREFIX}/install`, async ({ id, version, isUpdating = false }, thunkApi) => { - const changes = isUpdating - ? { isInstalled: true, installedVersion: version, hasUpdate: false } - : { isInstalled: true, installedVersion: version }; +>(`${STATE_PREFIX}/install`, async ({ id, version, installType = PluginStatus.INSTALL }, thunkApi) => { + const changes: Partial = { isInstalled: true, installedVersion: version }; + + if (installType === PluginStatus.UPDATE) { + changes.hasUpdate = false; + } + if (installType === PluginStatus.DOWNGRADE) { + changes.hasUpdate = true; + } + try { await installPlugin(id, version); await updatePanels(); - if (isUpdating) { + if (installType !== PluginStatus.INSTALL) { invalidatePluginInCache(id); } diff --git a/public/app/features/plugins/admin/state/hooks.ts b/public/app/features/plugins/admin/state/hooks.ts index da0ba859a3a..457f2f1d73e 100644 --- a/public/app/features/plugins/admin/state/hooks.ts +++ b/public/app/features/plugins/admin/state/hooks.ts @@ -4,7 +4,7 @@ import { PluginError, PluginType } from '@grafana/data'; import { useDispatch, useSelector } from 'app/types'; import { sortPlugins, Sorters, isPluginUpdatable } from '../helpers'; -import { CatalogPlugin } from '../types'; +import { CatalogPlugin, PluginStatus } from '../types'; import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall, fetchAllLocal, unsetInstall } from './actions'; import { @@ -64,7 +64,7 @@ export const useGetErrors = (filterByPluginType?: PluginType): PluginError[] => export const useInstall = () => { const dispatch = useDispatch(); - return (id: string, version?: string, isUpdating?: boolean) => dispatch(install({ id, version, isUpdating })); + return (id: string, version?: string, installType?: PluginStatus) => dispatch(install({ id, version, installType })); }; export const useUnsetInstall = () => { diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index f3d5783ae3c..96e1d2451d2 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -257,6 +257,7 @@ export enum PluginStatus { UNINSTALL = 'UNINSTALL', UPDATE = 'UPDATE', REINSTALL = 'REINSTALL', + DOWNGRADE = 'DOWNGRADE', } export enum PluginTabLabels { diff --git a/public/app/features/visualization/data-hover/DataHoverView.tsx b/public/app/features/visualization/data-hover/DataHoverView.tsx index adc593a9a14..79785c94eb2 100644 --- a/public/app/features/visualization/data-hover/DataHoverView.tsx +++ b/public/app/features/visualization/data-hover/DataHoverView.tsx @@ -23,6 +23,7 @@ export interface Props { mode?: TooltipDisplayMode | null; header?: string; padding?: number; + maxHeight?: number; } export interface DisplayValue { @@ -92,7 +93,16 @@ export function getDisplayValuesAndLinks( return { displayValues, links }; } -export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode, header, padding = 0 }: Props) => { +export const DataHoverView = ({ + data, + rowIndex, + columnIndex, + sortOrder, + mode, + header, + padding = 0, + maxHeight, +}: Props) => { const styles = useStyles2(getStyles, padding); if (!data || rowIndex == null) { @@ -108,7 +118,7 @@ export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode, he const { displayValues, links } = dispValuesAndLinks; if (header === 'Exemplar') { - return ; + return ; } return ( diff --git a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx index b353b08abba..b2f17d17333 100644 --- a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx +++ b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx @@ -11,10 +11,11 @@ export interface Props { displayValues: DisplayValue[]; links?: LinkModel[]; header?: string; + maxHeight?: number; } -export const ExemplarHoverView = ({ displayValues, links, header = 'Exemplar' }: Props) => { - const styles = useStyles2(getStyles); +export const ExemplarHoverView = ({ displayValues, links, header = 'Exemplar', maxHeight }: Props) => { + const styles = useStyles2(getStyles, 0, maxHeight); const time = displayValues.find((val) => val.name === 'Time'); displayValues = displayValues.filter((val) => val.name !== 'Time'); // time? @@ -49,7 +50,7 @@ export const ExemplarHoverView = ({ displayValues, links, header = 'Exemplar' }: ); }; -const getStyles = (theme: GrafanaTheme2, padding = 0) => { +const getStyles = (theme: GrafanaTheme2, padding = 0, maxHeight?: number) => { return { exemplarWrapper: css({ display: 'flex', @@ -79,6 +80,8 @@ const getStyles = (theme: GrafanaTheme2, padding = 0) => { gap: 4, borderTop: `1px solid ${theme.colors.border.medium}`, padding: theme.spacing(1), + overflowY: 'auto', + maxHeight: maxHeight, }), exemplarFooter: css({ display: 'flex', diff --git a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx index f2a5959edc6..6234a4a7693 100644 --- a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx +++ b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx @@ -49,7 +49,7 @@ export const CanvasTooltip = ({ scene }: Props) => { } // Retrieve timestamp of the last data point if available - const timeField = scene.data?.series[0].fields?.find((field) => field.type === FieldType.time); + const timeField = scene.data?.series[0]?.fields?.find((field) => field.type === FieldType.time); const lastTimeValue = timeField?.values[timeField.values.length - 1]; const shouldDisplayTimeContentItem = timeField && lastTimeValue && element.data.field && getFieldDisplayName(timeField) !== element.data.field; diff --git a/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx index 896dbeb1feb..d15b13cd50c 100644 --- a/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx @@ -56,6 +56,7 @@ export const HeatmapTooltip = (props: HeatmapTooltipProps) => { rowIndex={props.dataIdxs[2]} header={'Exemplar'} padding={8} + maxHeight={props.maxHeight} /> ); } diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index 6ec45e27d9a..ff3dfb38214 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -1,4 +1,4 @@ -import { FieldConfigProperty, FieldType, identityOverrideProcessor, PanelPlugin } from '@grafana/data'; +import { DataFrame, FieldConfigProperty, FieldType, identityOverrideProcessor, PanelPlugin } from '@grafana/data'; import { config } from '@grafana/runtime'; import { AxisPlacement, @@ -442,7 +442,9 @@ export const plugin = new PanelPlugin(HeatmapPanel) settings: { integer: true, }, - showIf: (options) => options.tooltip?.mode === TooltipDisplayMode.Multi, + showIf: (options: Options, data: DataFrame[] | undefined, annotations: DataFrame[] | undefined) => + options.tooltip?.mode === TooltipDisplayMode.Multi || + annotations?.some((df) => df.meta?.custom?.resultType === 'exemplar'), }); category = ['Legend']; @@ -459,6 +461,8 @@ export const plugin = new PanelPlugin(HeatmapPanel) name: 'Color', defaultValue: defaultOptions.exemplars.color, category, + showIf: (options: Options, data: DataFrame[] | undefined, annotations: DataFrame[] | undefined) => + annotations?.some((df) => df.meta?.custom?.resultType === 'exemplar'), }); }) .setSuggestionsSupplier(new HeatmapSuggestionsSupplier()) diff --git a/public/app/plugins/panel/logs-new/LogsPanel.tsx b/public/app/plugins/panel/logs-new/LogsPanel.tsx index 44dea0e5f54..9b0b26b4c23 100644 --- a/public/app/plugins/panel/logs-new/LogsPanel.tsx +++ b/public/app/plugins/panel/logs-new/LogsPanel.tsx @@ -102,6 +102,7 @@ export const LogsPanel = ({ )} {((canEditThresholds && onThresholdsChange) || showThresholds) && ( diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx index 54e79e06743..f41e5e3af7a 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx @@ -29,6 +29,7 @@ interface ExemplarMarkerProps { exemplarColor?: string; clickedExemplarFieldIndex: DataFrameFieldIndex | undefined; setClickedExemplarFieldIndex: React.Dispatch; + maxHeight?: number; } export const ExemplarMarker = ({ @@ -39,6 +40,7 @@ export const ExemplarMarker = ({ exemplarColor, clickedExemplarFieldIndex, setClickedExemplarFieldIndex, + maxHeight, }: ExemplarMarkerProps) => { const styles = useStyles2(getExemplarMarkerStyles); const [isOpen, setIsOpen] = useState(false); @@ -163,7 +165,7 @@ export const ExemplarMarker = ({ return (
{isLocked && } - +
); }, [ @@ -175,6 +177,7 @@ export const ExemplarMarker = ({ floatingStyles, getFloatingProps, refs.setFloating, + maxHeight, ]); const seriesColor = config diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index c741f821f88..673f2288dc7 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -18,9 +18,10 @@ interface ExemplarsPluginProps { exemplars: DataFrame[]; timeZone: TimeZone; visibleSeries?: VisibleExemplarLabels; + maxHeight?: number; } -export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries }: ExemplarsPluginProps) => { +export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries, maxHeight }: ExemplarsPluginProps) => { const plotInstance = useRef(); const [lockedExemplarFieldIndex, setLockedExemplarFieldIndex] = useState(); @@ -83,10 +84,11 @@ export const ExemplarsPlugin = ({ exemplars, timeZone, config, visibleSeries }: dataFrameFieldIndex={dataFrameFieldIndex} config={config} exemplarColor={markerColor} + maxHeight={maxHeight} /> ); }, - [config, timeZone, visibleSeries, setLockedExemplarFieldIndex, lockedExemplarFieldIndex] + [config, timeZone, visibleSeries, setLockedExemplarFieldIndex, lockedExemplarFieldIndex, maxHeight] ); return ( diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 1222cec2022..355610c995d 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -592,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1688,10 +1706,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1699,8 +1726,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1720,32 +1754,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "Schließen" @@ -1769,6 +1848,10 @@ "modal": { "close-tooltip": "Schließen" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1782,6 +1865,9 @@ "no-options-label": "Keine Optionen gefunden", "placeholder": "Auswählen" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1801,6 +1887,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3637,6 +3726,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "Kürzlich verwendete absolute Bereiche", @@ -3693,10 +3792,13 @@ "example": "", "example-details": "", "example-title": "Zeitbereiche-Beispiel", + "from-label": "", "from-to": "", "more-info": "", "specify": "Zeitbereich festlegen <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "Zeitzonen-Auswähler", diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2a6465e6357..fe2fea8fc02 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -315,6 +315,10 @@ "empty-state": { "title": "You don't have any contact points yet" }, + "key-value-map": { + "add": "Add", + "confirm-add": "Confirm to add" + }, "last-delivery-attempt": "Last delivery attempt", "last-delivery-failed": "Last delivery attempt failed", "no-contact-points-found": "No contact points found", @@ -1043,10 +1047,24 @@ }, "modal": { "title": "Row options" - } + }, + "repeat": { + "title": "Repeat options", + "variable": { + "title": "Variable" + } + }, + "title": "Row options" } }, "edit-pane": { + "elements": { + "dashboard": "Dashboard", + "objects": "Objects", + "panels": "Panels", + "rows": "Rows", + "tabs": "Tabs" + }, "objects": { "multi-select": { "selection-number": "No. of objects selected: {{length}}" @@ -1151,6 +1169,15 @@ "title": "Dashboard options", "title-option": "Title" }, + "outline": { + "tree": { + "item": { + "collapse": "Collapse item", + "empty": "(empty)", + "expand": "Expand item" + } + } + }, "panel-edit": { "alerting-tab": { "dashboard-not-saved": "Dashboard must be saved before alerts can be added.", @@ -1161,6 +1188,12 @@ "description": "CSS layout that adjusts to the available space", "item-options": { "hide-no-data": "Hide when no data", + "repeat": { + "variable": { + "description": "Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.", + "title": "Repeat by variable" + } + }, "title": "Layout options" }, "name": "Responsive grid", @@ -2224,6 +2257,14 @@ "log-line": "Log line", "no-details": "No details available" }, + "log-line-menu": { + "copy-link": "Copy link to log line", + "copy-log": "Copy log line", + "icon-label": "Log menu", + "pin-to-outline": "Pin log", + "show-context": "Show context", + "unpin-from-outline": "Unpin log" + }, "log-row-message": { "ellipsis": "… ", "more": "more", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index e6d9efb5f9b..1fa664da890 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -592,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1688,10 +1706,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1699,8 +1726,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1720,32 +1754,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "Cerrar" @@ -1769,6 +1848,10 @@ "modal": { "close-tooltip": "Cerrar" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1782,6 +1865,9 @@ "no-options-label": "No se ha encontrado ninguna opción", "placeholder": "Elegir" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1801,6 +1887,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3637,6 +3726,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "Intervalos absolutos utilizados recientemente", @@ -3693,10 +3792,13 @@ "example": "", "example-details": "", "example-title": "Ejemplos de intervalos de tiempo", + "from-label": "", "from-to": "", "more-info": "", "specify": "Especificar el intervalo de tiempo <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "Selector de huso horario", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index f6ab1a8be74..4def0fc0e73 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -592,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1688,10 +1706,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1699,8 +1726,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1720,32 +1754,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "Fermer" @@ -1769,6 +1848,10 @@ "modal": { "close-tooltip": "Fermer" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1782,6 +1865,9 @@ "no-options-label": "Aucune option trouvée", "placeholder": "Choisir" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1801,6 +1887,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3637,6 +3726,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "Périodes absolues récemment utilisées", @@ -3693,10 +3792,13 @@ "example": "", "example-details": "", "example-title": "Exemple de plages de temps", + "from-label": "", "from-to": "", "more-info": "", "specify": "Spécifiez la plage de temps <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "Outil de sélection du fuseau horaire", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 3eea1ed364a..d6af162cbf2 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -315,6 +315,10 @@ "empty-state": { "title": "Ÿőū đőʼn'ŧ ĥävę äʼny čőʼnŧäčŧ pőįʼnŧş yęŧ" }, + "key-value-map": { + "add": "Åđđ", + "confirm-add": "Cőʼnƒįřm ŧő äđđ" + }, "last-delivery-attempt": "Ŀäşŧ đęľįvęřy äŧŧęmpŧ", "last-delivery-failed": "Ŀäşŧ đęľįvęřy äŧŧęmpŧ ƒäįľęđ", "no-contact-points-found": "Ńő čőʼnŧäčŧ pőįʼnŧş ƒőūʼnđ", @@ -1043,10 +1047,24 @@ }, "modal": { "title": "Ŗőŵ őpŧįőʼnş" - } + }, + "repeat": { + "title": "Ŗępęäŧ őpŧįőʼnş", + "variable": { + "title": "Väřįäþľę" + } + }, + "title": "Ŗőŵ őpŧįőʼnş" } }, "edit-pane": { + "elements": { + "dashboard": "Đäşĥþőäřđ", + "objects": "Øþĵęčŧş", + "panels": "Päʼnęľş", + "rows": "Ŗőŵş", + "tabs": "Ŧäþş" + }, "objects": { "multi-select": { "selection-number": "Ńő. őƒ őþĵęčŧş şęľęčŧęđ: {{length}}" @@ -1151,6 +1169,15 @@ "title": "Đäşĥþőäřđ őpŧįőʼnş", "title-option": "Ŧįŧľę" }, + "outline": { + "tree": { + "item": { + "collapse": "Cőľľäpşę įŧęm", + "empty": "(ęmpŧy)", + "expand": "Ēχpäʼnđ įŧęm" + } + } + }, "panel-edit": { "alerting-tab": { "dashboard-not-saved": "Đäşĥþőäřđ mūşŧ þę şävęđ þęƒőřę äľęřŧş čäʼn þę äđđęđ.", @@ -1161,6 +1188,12 @@ "description": "CŜŜ ľäyőūŧ ŧĥäŧ äđĵūşŧş ŧő ŧĥę äväįľäþľę şpäčę", "item-options": { "hide-no-data": "Ħįđę ŵĥęʼn ʼnő đäŧä", + "repeat": { + "variable": { + "description": "Ŗępęäŧ ŧĥįş päʼnęľ ƒőř ęäčĥ väľūę įʼn ŧĥę şęľęčŧęđ väřįäþľę. Ŧĥįş įş ʼnőŧ vįşįþľę ŵĥįľę įʼn ęđįŧ mőđę. Ÿőū ʼnęęđ ŧő ģő þäčĸ ŧő đäşĥþőäřđ äʼnđ ŧĥęʼn ūpđäŧę ŧĥę väřįäþľę őř řęľőäđ ŧĥę đäşĥþőäřđ.", + "title": "Ŗępęäŧ þy väřįäþľę" + } + }, "title": "Ŀäyőūŧ őpŧįőʼnş" }, "name": "Ŗęşpőʼnşįvę ģřįđ", @@ -2224,6 +2257,14 @@ "log-line": "Ŀőģ ľįʼnę", "no-details": "Ńő đęŧäįľş äväįľäþľę" }, + "log-line-menu": { + "copy-link": "Cőpy ľįʼnĸ ŧő ľőģ ľįʼnę", + "copy-log": "Cőpy ľőģ ľįʼnę", + "icon-label": "Ŀőģ męʼnū", + "pin-to-outline": "Pįʼn ľőģ", + "show-context": "Ŝĥőŵ čőʼnŧęχŧ", + "unpin-from-outline": "Ůʼnpįʼn ľőģ" + }, "log-row-message": { "ellipsis": "… ", "more": "mőřę", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index c10c3c0eaa4..6e0f3ba0687 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -592,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1688,10 +1706,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1699,8 +1726,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1720,32 +1754,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "Fechar" @@ -1769,6 +1848,10 @@ "modal": { "close-tooltip": "Fechar" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1782,6 +1865,9 @@ "no-options-label": "Nenhuma opção encontrada", "placeholder": "Escolher" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1801,6 +1887,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3637,6 +3726,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "Intervalos absolutos usados recentemente", @@ -3693,10 +3792,13 @@ "example": "", "example-details": "", "example-title": "Exemplos de intervalos de tempo", + "from-label": "", "from-to": "", "more-info": "", "specify": "Especifique o intervalo de tempo <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "Seletor de fuso horário", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index eb52a53913b..d1f6b954db5 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -402,6 +405,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -588,6 +605,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1679,10 +1697,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1690,8 +1717,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1711,32 +1745,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "关闭" @@ -1760,6 +1839,10 @@ "modal": { "close-tooltip": "关闭" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1773,6 +1856,9 @@ "no-options-label": "未找到选项", "placeholder": "选择" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1792,6 +1878,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3623,6 +3712,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "最近使用的绝对范围", @@ -3679,10 +3778,13 @@ "example": "", "example-details": "", "example-title": "示例时间范围", + "from-label": "", "from-to": "", "more-info": "", "specify": "指定时间范围 <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "时区选择器", diff --git a/yarn.lock b/yarn.lock index eb8360924ac..d5a786396d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3772,11 +3772,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:6.0.2": - version: 6.0.2 - resolution: "@grafana/scenes-react@npm:6.0.2" +"@grafana/scenes-react@npm:6.1.4": + version: 6.1.4 + resolution: "@grafana/scenes-react@npm:6.1.4" dependencies: - "@grafana/scenes": "npm:6.0.2" + "@grafana/scenes": "npm:6.1.4" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3788,13 +3788,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/9744e01f2ff912229e43cedfa41d626ccdfd034f5b9718b57c593bc90edadade960f76baf1d8ad19eed03709c17c62397df1871b89acc635172aa14f6a20e096 + checksum: 10/69a344f30937a80e25201c8ce1261f85c10663e68d70103c28603abbee2496c7855d999b2a212132f48af597f3f528de65bade66c05b02c719ca25f8f8c682bd languageName: node linkType: hard -"@grafana/scenes@npm:6.0.2": - version: 6.0.2 - resolution: "@grafana/scenes@npm:6.0.2" +"@grafana/scenes@npm:6.1.4": + version: 6.1.4 + resolution: "@grafana/scenes@npm:6.1.4" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3812,7 +3812,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/2584f296db6299ef0a09d51f5c267ebcf7e44bd17b4d6516e38d3220f8f1d7aebc63c5fc6523979c4ac4d3f555416ca573e85e03bd36eb33a11941a5b3497149 + checksum: 10/708652236c3b4a5bb0e1cd84739bef530b06244e2798e754347eabd8ab040b24c7f39376fd7b93d1049da6c0ee40785294882cba0125ce8171d5627a71ae63db languageName: node linkType: hard @@ -18126,8 +18126,8 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:6.0.2" - "@grafana/scenes-react": "npm:6.0.2" + "@grafana/scenes": "npm:6.1.4" + "@grafana/scenes-react": "npm:6.1.4" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^2.0.0"